From a5597c27eecdc927290af2d18a4d94e1c75f2e79 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Sun, 2 Aug 2026 09:49:40 +0200 Subject: [PATCH 01/97] feat(tui): Add V2 quota sidebar --- src/index.ts | 67 +------------- src/tui-v2.tsx | 213 ++++++++++++++++++++++++++++++++++++++++++++ tests/index.test.ts | 29 +++--- 3 files changed, 227 insertions(+), 82 deletions(-) create mode 100644 src/tui-v2.tsx diff --git a/src/index.ts b/src/index.ts index 38216b1c..c90dd402 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,66 +1,5 @@ -/** - * OpenCode Quota Plugin - * - * Shows quota status in OpenCode without LLM invocation. - * - * @packageDocumentation - */ +import "@opentui/solid/preload"; -import { QuotaToastPlugin } from "./plugin.js"; +const { default: plugin } = await import("./tui-v2.js"); -type V1PluginModule = { - id: string; - server: typeof QuotaToastPlugin; -}; - -// V1 plugin format: default export with id + server. -// This avoids the legacy getLegacyPlugins fallback path in OpenCode's plugin -// loader, which iterates Object.values(mod) and can conflict with other -// plugins that also use the legacy path. -const pluginModule = { - id: "@slkiser/opencode-quota", - server: QuotaToastPlugin, -} satisfies V1PluginModule; - -export default pluginModule; - -export type { - JsonV1Adapter, - JsonV1Mapping, - JsonV1Metric, - JsonV1NumberSource, - JsonV1Path, - JsonV1TextSource, - JsonV1TimestampEncoding, - JsonV1TimestampSource, - LocalEstimateQuotaProviderDefinition, - LocalEstimateWindow, - QuotaProviderDefinition, - QuotaProviderRemoteFormat, - RemoteApiQuotaProviderDefinition, -} from "./lib/quota-providers.js"; - -// Re-export types for consumers (types are erased at runtime, so safe to export) -export { - QUOTA_PROVIDER_MODES, - QUOTA_PROVIDER_REMOTE_FORMATS, - QUOTA_PROVIDER_WINDOW_TYPES, - validateQuotaProviders, -} from "./lib/quota-providers.js"; -export type { - CopilotEnterpriseUsageResult, - CopilotOrganizationUsageResult, - CopilotQuotaResult, - GoogleModelId, - GoogleModelQuota, - GoogleQuotaResult, - MaintainerAnnouncementsConfig, - MiniMaxResult, - MiniMaxResultEntry, - PricingSnapshotSource, - QuotaToastConfig, - SessionTokenScope, -} from "./lib/types.js"; -// Keep the named export for backward compatibility with consumers that import -// { QuotaToastPlugin } directly. -export { QuotaToastPlugin } from "./plugin.js"; +export default plugin; diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx new file mode 100644 index 00000000..50477f47 --- /dev/null +++ b/src/tui-v2.tsx @@ -0,0 +1,213 @@ +/** @jsxImportSource @opentui/solid */ +import type { JSX } from "@opentui/solid"; +import { RGBA } from "@opentui/core"; +import { Show, createSignal, onCleanup } from "solid-js"; + +import { sanitizeDisplayText } from "./lib/display-sanitize.js"; +import { formatQuotaRows } from "./lib/format.js"; +import { collectQuotaRenderData } from "./lib/quota-render-data.js"; +import { + createQuotaRuntimeRequestContext, + resolveQuotaRuntimeContext, + type QuotaSessionModelContext, +} from "./lib/quota-runtime-context.js"; +import { resolveQuotaFormatStyle } from "./lib/quota-format-style.js"; +import { buildSidebarQuotaPanelLines } from "./lib/tui-sidebar-format.js"; + +const terminalForeground = RGBA.defaultForeground(); + +type TuiEvent = { data?: Record }; +type Toast = { + variant?: "info" | "success" | "warning" | "error"; + title?: string; + message: string; + duration?: number; +}; +type TuiContext = { + client: unknown; + data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void }; + ui: { + slot: { + (name: "app", render: () => null): () => void; + (name: "sidebar.content", render: (props: { sessionID: string }) => JSX.Element): () => void; + }; + toast: { show: (toast: Toast) => void }; + }; +}; + +function getSessionID(event: TuiEvent): string | undefined { + const sessionID = event.data?.sessionID; + return typeof sessionID === "string" && sessionID ? sessionID : undefined; +} + +async function getSessionModelMeta( + client: unknown, + sessionID: string, +): Promise { + const session = ( + client as { session?: { get?: (input: { sessionID: string }) => Promise<{ data?: unknown }> } } + ).session; + const response = await session?.get?.({ sessionID }); + const model = (response?.data as { model?: { id?: string; providerID?: string } } | undefined) + ?.model; + return model ? { modelID: model.id, providerID: model.providerID } : {}; +} + +async function getQuotaMessage( + context: TuiContext, + sessionID: string, + surface: "sidebar" | "idle" | "compacted" | "question", +): Promise<{ message: string; duration: number } | undefined> { + const runtime = await resolveQuotaRuntimeContext({ + client: context.client as never, + roots: { fallbackDirectory: process.cwd() }, + sessionID, + resolveSessionMeta: (id) => getSessionModelMeta(context.client, id), + includeSessionMeta: (config) => config.onlyCurrentModel, + }); + const config = runtime.config; + if (!config.enabled) return; + if (surface === "sidebar") { + if (!config.tuiSidebarPanel.enabled) return; + } else if (!config.enableToast) { + return; + } + if ( + (surface === "idle" && !config.showOnIdle) || + (surface === "compacted" && !config.showOnCompact) || + (surface === "question" && !config.showOnQuestion) + ) { + return; + } + + const formatStyle = resolveQuotaFormatStyle(config.formatStyle); + const result = await collectQuotaRenderData({ + client: runtime.client, + resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, + config, + configMeta: runtime.configMeta, + request: createQuotaRuntimeRequestContext(runtime), + surfaceExplicitProviderIssues: true, + formatStyle, + providers: runtime.providers, + }); + const data = result.data; + const message = + surface === "sidebar" + ? data + ? buildSidebarQuotaPanelLines({ + data, + config: { + ...config, + formatStyle: config.tuiSidebarPanel.formatStyle + ? resolveQuotaFormatStyle(config.tuiSidebarPanel.formatStyle) + : formatStyle, + }, + }).join("\n") + : undefined + : data?.entries.length || data?.sessionTokens + ? formatQuotaRows({ + version: "2.0.0", + layout: config.layout, + entries: data?.entries ?? [], + errors: data?.errors ?? [], + style: resolveQuotaFormatStyle(config.formatStyle), + percentDisplayMode: config.percentDisplayMode, + resetTimeDecimals: config.resetTimeDecimals, + sessionTokens: data?.sessionTokens, + }) + : config.showOnBothFail && data?.errors.length + ? data.errors.map((error) => `${error.label}: ${error.message}`).join("\n") + : undefined; + return message + ? { message: sanitizeDisplayText(message), duration: config.toastDurationMs } + : undefined; +} + +function reportFailure(error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + console.warn(`[opencode-quota] failed to load quota: ${message}`); +} + +function SidebarQuotaView(props: { context: TuiContext; sessionID: string }): JSX.Element { + const [quota, setQuota] = createSignal<{ message: string; duration: number } | undefined>( + undefined, + ); + const refresh = () => { + void getQuotaMessage(props.context, props.sessionID, "sidebar") + .then((result) => setQuota(result)) + .catch(reportFailure); + }; + refresh(); + const unsubscribe = props.context.data.on("session.step.ended", (event) => { + if (getSessionID(event) === props.sessionID) refresh(); + }); + onCleanup(unsubscribe); + + return ( + + + Quota + + No quota data available}> + {(value: () => { message: string; duration: number }) => + value() + .message.split("\n") + .map((line) => ( + + {line || " "} + + )) + } + + + ); +} + +const plugin = { + id: "@slkiser/opencode-quota", + setup(context: TuiContext) { + let disposeEvents: (() => void) | undefined; + const disposeApp = context.ui.slot("app", () => { + if (disposeEvents) return null; + const trigger = (event: TuiEvent, reason: "idle" | "compacted" | "question") => { + const sessionID = getSessionID(event); + if (!sessionID) return; + void getQuotaMessage(context, sessionID, reason) + .then((quota) => { + if (!quota) return; + context.ui.toast.show({ + variant: "info", + title: "OpenCode Quota", + message: quota.message, + duration: quota.duration, + }); + }) + .catch(reportFailure); + }; + const onStepEnded = context.data.on("session.step.ended", (event) => trigger(event, "idle")); + const onCompacted = context.data.on("session.compacted", (event) => + trigger(event, "compacted"), + ); + const onQuestion = context.data.on("session.tool.success", (event) => { + if (event.data?.tool === "question") trigger(event, "question"); + }); + disposeEvents = () => { + onStepEnded(); + onCompacted(); + onQuestion(); + }; + return null; + }); + const disposeSidebar = context.ui.slot("sidebar.content", (props) => ( + + )); + return () => { + disposeEvents?.(); + disposeApp(); + disposeSidebar(); + }; + }, +}; + +export default plugin; diff --git a/tests/index.test.ts b/tests/index.test.ts index cdbe2345..dcdba516 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,29 +1,22 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; const pluginMocks = vi.hoisted(() => ({ - QuotaToastPlugin: vi.fn(), + tui: { + id: "@slkiser/opencode-quota", + setup: vi.fn(), + }, })); -vi.mock("../src/plugin.js", () => ({ - QuotaToastPlugin: pluginMocks.QuotaToastPlugin, +vi.mock("../src/tui-v2.js", () => ({ + default: pluginMocks.tui, })); -describe("package entrypoint", () => { - beforeEach(() => { - vi.resetModules(); - }); +vi.mock("@opentui/solid/preload", () => ({})); - it("exports the V1 plugin module shape on the default export", async () => { +describe("package entrypoint", () => { + it("exports the V2 TUI plugin on the default export", async () => { const mod = await import("../src/index.js"); - expect(mod.default).toEqual({ - id: "@slkiser/opencode-quota", - server: pluginMocks.QuotaToastPlugin, - }); - expect(mod.QuotaToastPlugin).toBe(pluginMocks.QuotaToastPlugin); - expect(mod.QUOTA_PROVIDER_REMOTE_FORMATS).toEqual(["quota-v1", "openrouter-key-v1", "json-v1"]); - expect(JSON.stringify(mod.QUOTA_PROVIDER_REMOTE_FORMATS)).not.toContain( - ["accounting", "v1"].join("-"), - ); + expect(mod.default).toBe(pluginMocks.tui); }); }); From a01a06954a927b342564e342f3236c2b1b392492 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Sun, 2 Aug 2026 10:06:50 +0200 Subject: [PATCH 02/97] feat(auth): fall back to OpenCode credential database - Extend `src/lib/opencode-auth.ts` with a credential database reader - Add `CredentialDatabase` and `CredentialDatabaseConstructor` types - Add `runtimeRequire` to conditionally load `better-sqlite3` - Implement `getCredentialDbPaths`, `readCredentialDatabase`, `openCredentialDatabase`, and `parseCredentialValue` - Modify `readAuthFile` and add `readAuthFiles` + `readCredentialDatabases` to merge credentials from auth.json and OpenCode's database - Preserve precedence: entries in auth.json override database credentials; database serves as a fallback for OAuth credentials - Add tests covering database fallback and auth.json precedence in `tests/lib.opencode-auth.test.ts` --- src/lib/opencode-auth.ts | 94 ++++++++++++++++++++++++++++++++- tests/lib.opencode-auth.test.ts | 83 +++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 tests/lib.opencode-auth.test.ts diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 2391bf9e..083a53bb 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -6,8 +6,10 @@ * file/path parsing. */ +import { existsSync } from "fs"; import { readFile } from "fs/promises"; -import { join } from "path"; +import { createRequire } from "module"; +import { isAbsolute, join, resolve } from "path"; import { getOpencodeRuntimeDirCandidates, @@ -17,6 +19,14 @@ import { import type { AuthData } from "./types.js"; const DEFAULT_AUTH_CACHE_MAX_AGE_MS = 5_000; +const runtimeRequire = createRequire(import.meta.url); + +type CredentialDatabase = { + close(): void; + prepare(sql: string): { all(...params: string[]): unknown[] }; +}; + +type CredentialDatabaseConstructor = new (path: string, options?: Record) => CredentialDatabase; type AuthCacheEntry = { timestamp: number; @@ -45,8 +55,16 @@ export function getAuthPath(): string { } export async function readAuthFile(): Promise { - const paths = getAuthPaths(); + const { dataDirs } = getOpencodeRuntimeDirCandidates(); + const fileAuth = await readAuthFiles(dataDirs.map((dataDir) => join(dataDir, "auth.json"))); + const databaseAuth = readCredentialDatabases(dataDirs); + if (!fileAuth && !databaseAuth) return null; + // auth.json is a user-managed compatibility source, so it wins over the current database. + return { ...databaseAuth, ...fileAuth }; +} + +async function readAuthFiles(paths: string[]): Promise { for (const path of paths) { try { const content = await readFile(path, "utf-8"); @@ -59,6 +77,78 @@ export async function readAuthFile(): Promise { return null; } +function readCredentialDatabases(dataDirs: string[]): AuthData | null { + for (const path of getCredentialDbPaths(dataDirs)) { + const auth = readCredentialDatabase(path); + if (auth) return auth; + } + + return null; +} + +function getCredentialDbPaths(dataDirs: string[]): string[] { + const override = process.env.OPENCODE_DB?.trim(); + if (override) { + if (override === ":memory:") return []; + return [isAbsolute(override) ? override : resolve(dataDirs[0] ?? getOpencodeRuntimeDirs().dataDir, override)]; + } + return dataDirs.map((dataDir) => join(dataDir, "opencode-next.db")); +} + +function readCredentialDatabase(path: string): AuthData | null { + if (!existsSync(path)) return null; + + let database: CredentialDatabase | undefined; + try { + database = openCredentialDatabase(path); + const rows = database + .prepare("SELECT integration_id, value FROM credential WHERE integration_id IS NOT NULL ORDER BY time_updated DESC, id DESC") + .all() as Array<{ integration_id?: unknown; value?: unknown }>; + const auth: Record = {}; + + for (const row of rows) { + if (typeof row.integration_id !== "string" || typeof row.value !== "string" || row.integration_id in auth) continue; + const value = parseCredentialValue(row.value); + if (value) auth[row.integration_id] = value; + } + + return Object.keys(auth).length > 0 ? (auth as AuthData) : null; + } catch { + return null; + } finally { + database?.close(); + } +} + +function openCredentialDatabase(path: string): CredentialDatabase { + if ("Bun" in globalThis) { + const { Database } = runtimeRequire("bun:sqlite") as { Database: CredentialDatabaseConstructor }; + return new Database(path, { readonly: true }); + } + + const { DatabaseSync } = runtimeRequire("node:sqlite") as { DatabaseSync: CredentialDatabaseConstructor }; + return new DatabaseSync(path, { readOnly: true, timeout: 5_000 }); +} + +function parseCredentialValue(value: string): Record | null { + try { + const parsed = JSON.parse(value); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; + const credential = parsed as Record; + const metadata = credential.metadata; + const authEntry = { + ...(metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {}), + ...credential, + }; + if (authEntry.type === "key" && typeof authEntry.key === "string") { + authEntry.type = "api"; + } + return authEntry; + } catch { + return null; + } +} + /** * Cached auth reader for frequently triggered code paths (e.g. per-question hooks). * This avoids repeated filesystem reads while keeping auth updates visible quickly. diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts new file mode 100644 index 00000000..177b09a8 --- /dev/null +++ b/tests/lib.opencode-auth.test.ts @@ -0,0 +1,83 @@ +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { clearReadAuthFileCacheForTests, readAuthFile } from "../src/lib/opencode-auth.js"; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + clearReadAuthFileCacheForTests(); + vi.unstubAllEnvs(); + await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); +}); + +async function createCredentialDatabase(): Promise<{ dataDir: string; databasePath: string }> { + const root = await mkdtemp(join(tmpdir(), "opencode-quota-auth-")); + temporaryDirectories.push(root); + const dataDir = join(root, "opencode"); + await mkdir(dataDir, { recursive: true }); + const databasePath = join(dataDir, "opencode-next.db"); + const database = new DatabaseSync(databasePath); + database.exec(`CREATE TABLE credential ( + id TEXT PRIMARY KEY, + integration_id TEXT, + label TEXT NOT NULL, + value TEXT NOT NULL, + connector_id TEXT, + method_id TEXT, + active INTEGER, + time_created INTEGER NOT NULL, + time_updated INTEGER NOT NULL + )`); + database + .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, NULL, 1, ?)") + .run( + "copilot", + "github-copilot", + JSON.stringify({ type: "oauth", access: "copilot-access", refresh: "copilot-refresh", expires: 10, metadata: { enterpriseUrl: "example.ghe.com" } }), + 1, + ); + database + .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, NULL, 1, ?)") + .run( + "openai", + "openai", + JSON.stringify({ type: "oauth", access: "openai-access", refresh: "openai-refresh", expires: 20 }), + 2, + ); + database + .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, NULL, 1, ?)") + .run("deepseek", "deepseek", JSON.stringify({ type: "key", key: "deepseek-key" }), 3); + database.close(); + return { dataDir, databasePath }; +} + +describe("OpenCode auth reader", () => { + it("falls back to OAuth credentials stored in OpenCode's database", async () => { + const { dataDir } = await createCredentialDatabase(); + vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); + + await expect(readAuthFile()).resolves.toMatchObject({ + "github-copilot": { access: "copilot-access", enterpriseUrl: "example.ghe.com" }, + deepseek: { type: "api", key: "deepseek-key" }, + openai: { access: "openai-access" }, + }); + }); + + it("keeps auth.json entries ahead of database credentials", async () => { + const { dataDir } = await createCredentialDatabase(); + vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); + await writeFile( + join(dataDir, "auth.json"), + JSON.stringify({ openai: { type: "oauth", access: "file-access" } }), + ); + + await expect(readAuthFile()).resolves.toMatchObject({ + "github-copilot": { access: "copilot-access" }, + openai: { access: "file-access" }, + }); + }); +}); From 65874b9e96d13842e0cd498dda7759f73fae4c81 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Sun, 2 Aug 2026 10:12:19 +0200 Subject: [PATCH 03/97] feat(tui): Add local quota slash commands --- src/tui-v2.tsx | 104 +++++++++++++++++++++++++++++++++- tests/tui-v2-commands.test.ts | 40 +++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 tests/tui-v2-commands.test.ts diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 50477f47..ff400472 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -5,6 +5,11 @@ import { Show, createSignal, onCleanup } from "solid-js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { formatQuotaRows } from "./lib/format.js"; +import { + buildQuotaDialogCommandOutput, + QUOTA_DIALOG_COMMANDS, + type QuotaDialogCommandId, +} from "./lib/quota-dialog-commands.js"; import { collectQuotaRenderData } from "./lib/quota-render-data.js"; import { createQuotaRuntimeRequestContext, @@ -26,12 +31,30 @@ type Toast = { type TuiContext = { client: unknown; data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void }; + keymap: { + layer: (build: () => { + mode: "global"; + commands: Array<{ + id: string; + title: string; + group: string; + palette: true; + slash: { name: string }; + run: (input?: unknown) => void; + }>; + }) => void; + }; ui: { slot: { (name: "app", render: () => null): () => void; (name: "sidebar.content", render: (props: { sessionID: string }) => JSX.Element): () => void; }; toast: { show: (toast: Toast) => void }; + dialog: { + alert: (params: { title: string; message: string }) => Promise; + prompt: (params: { title: string; placeholder?: string }) => Promise; + set: (params: { size: "medium" | "large" | "xlarge" }) => void; + }; }; }; @@ -129,7 +152,75 @@ function reportFailure(error: unknown): void { console.warn(`[opencode-quota] failed to load quota: ${message}`); } -function SidebarQuotaView(props: { context: TuiContext; sessionID: string }): JSX.Element { +function getCommandArguments(input: unknown): string | undefined { + if (!input || typeof input !== "object") return undefined; + const record = input as Record; + for (const key of ["arguments", "args", "query"] as const) { + const value = record[key]; + if (typeof value === "string" && value.trim()) return value.trim(); + } + return undefined; +} + +async function runQuotaCommand( + context: TuiContext, + command: QuotaDialogCommandId, + sessionID: string | undefined, + input?: unknown, +): Promise { + const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.id === command)!; + let argumentsText = getCommandArguments(input); + if (spec.acceptsArguments && argumentsText === undefined) { + const value = await context.ui.dialog.prompt({ + title: spec.title, + placeholder: command === "tokens_between" ? "YYYY-MM-DD YYYY-MM-DD" : "Optional arguments", + }); + if (value === undefined) return; + argumentsText = value.trim() || undefined; + } + + try { + const result = await buildQuotaDialogCommandOutput({ + command, + arguments: argumentsText, + client: context.client as never, + roots: { fallbackDirectory: process.cwd() }, + sessionID, + resolveSessionMeta: (id) => getSessionModelMeta(context.client, id), + }); + if (result.state === "noop") return; + const alert = context.ui.dialog.alert({ title: result.title, message: result.output }); + context.ui.dialog.set({ size: result.dialogSize }); + await alert; + } catch (error) { + context.ui.toast.show({ + variant: "error", + title: "OpenCode Quota", + message: sanitizeDisplayText(error instanceof Error ? error.message : String(error)), + }); + } +} + +function registerQuotaCommands(context: TuiContext, getSessionID: () => string | undefined): void { + context.keymap.layer(() => ({ + mode: "global", + commands: QUOTA_DIALOG_COMMANDS.map((spec) => ({ + id: `quota.${spec.id}`, + title: spec.title, + group: "OpenCode Quota", + palette: true, + slash: { name: spec.slashName }, + run: (input?: unknown) => void runQuotaCommand(context, spec.id, getSessionID(), input), + })), + })); +} + +function SidebarQuotaView(props: { + context: TuiContext; + sessionID: string; + setActiveSessionID: (sessionID: string) => void; +}): JSX.Element { + props.setActiveSessionID(props.sessionID); const [quota, setQuota] = createSignal<{ message: string; duration: number } | undefined>( undefined, ); @@ -168,11 +259,14 @@ const plugin = { id: "@slkiser/opencode-quota", setup(context: TuiContext) { let disposeEvents: (() => void) | undefined; + let activeSessionID: string | undefined; const disposeApp = context.ui.slot("app", () => { if (disposeEvents) return null; + registerQuotaCommands(context, () => activeSessionID); const trigger = (event: TuiEvent, reason: "idle" | "compacted" | "question") => { const sessionID = getSessionID(event); if (!sessionID) return; + activeSessionID = sessionID; void getQuotaMessage(context, sessionID, reason) .then((quota) => { if (!quota) return; @@ -200,7 +294,13 @@ const plugin = { return null; }); const disposeSidebar = context.ui.slot("sidebar.content", (props) => ( - + { + activeSessionID = sessionID; + }} + /> )); return () => { disposeEvents?.(); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts new file mode 100644 index 00000000..d72050c6 --- /dev/null +++ b/tests/tui-v2-commands.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from "vitest"; + +import plugin from "../src/tui-v2.js"; + +describe("V2 quota TUI commands", () => { + it("registers every quota command as a local slash command", () => { + let layer: { commands: Array<{ slash: { name: string }; palette: boolean; run: () => void }> } | undefined; + const context = { + client: {}, + data: { on: vi.fn(() => vi.fn()) }, + keymap: { layer: vi.fn((build) => (layer = build())) }, + ui: { + slot: vi.fn((name, render) => { + if (name === "app") render({}); + return vi.fn(); + }), + toast: { show: vi.fn() }, + dialog: { alert: vi.fn(), prompt: vi.fn(), set: vi.fn() }, + }, + }; + + plugin.setup(context as any); + + expect(layer?.commands.map((command) => command.slash.name)).toEqual([ + "quota", + "quota_status", + "quota_announcements", + "pricing_refresh", + "tokens_today", + "tokens_daily", + "tokens_weekly", + "tokens_monthly", + "tokens_all", + "tokens_session", + "tokens_session_all", + "tokens_between", + ]); + expect(layer?.commands.every((command) => command.palette)).toBe(true); + }); +}); From 6ce71cc164e83001a2eab56547e3e3dfad93a4ad Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Sun, 2 Aug 2026 10:27:26 +0200 Subject: [PATCH 04/97] feat(tui): Add collapsible quota sidebar --- src/tui-v2.tsx | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index ff400472..099b4143 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -80,7 +80,7 @@ async function getQuotaMessage( context: TuiContext, sessionID: string, surface: "sidebar" | "idle" | "compacted" | "question", -): Promise<{ message: string; duration: number } | undefined> { +): Promise<{ message: string; duration: number; activeProviderCount: number } | undefined> { const runtime = await resolveQuotaRuntimeContext({ client: context.client as never, roots: { fallbackDirectory: process.cwd() }, @@ -143,7 +143,11 @@ async function getQuotaMessage( ? data.errors.map((error) => `${error.label}: ${error.message}`).join("\n") : undefined; return message - ? { message: sanitizeDisplayText(message), duration: config.toastDurationMs } + ? { + message: sanitizeDisplayText(message), + duration: config.toastDurationMs, + activeProviderCount: result.active.length, + } : undefined; } @@ -221,9 +225,12 @@ function SidebarQuotaView(props: { setActiveSessionID: (sessionID: string) => void; }): JSX.Element { props.setActiveSessionID(props.sessionID); - const [quota, setQuota] = createSignal<{ message: string; duration: number } | undefined>( - undefined, - ); + const [open, setOpen] = createSignal(true); + const [quota, setQuota] = createSignal< + { message: string; duration: number; activeProviderCount: number } | undefined + >(undefined); + const lines = () => quota()?.message.split("\n") ?? []; + const expandable = () => lines().length > 2; const refresh = () => { void getQuotaMessage(props.context, props.sessionID, "sidebar") .then((result) => setQuota(result)) @@ -237,19 +244,25 @@ function SidebarQuotaView(props: { return ( - - Quota - + expandable() && setOpen((value) => !value)}> + + {open() ? "▼" : "▶"} + + + Quota + + {(count) => ` (${count()} active)`} + + + No quota data available}> - {(value: () => { message: string; duration: number }) => - value() - .message.split("\n") - .map((line) => ( - - {line || " "} - - )) - } + + {lines().map((line) => ( + + {line || " "} + + ))} + ); From 64f6c9766c94d8855d46507021f4b817c285250c Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Sun, 2 Aug 2026 10:30:57 +0200 Subject: [PATCH 05/97] fix(tui): Type collapsible quota count --- src/tui-v2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 099b4143..1a72f9a0 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -251,7 +251,7 @@ function SidebarQuotaView(props: { Quota - {(count) => ` (${count()} active)`} + {(count: () => number) => ` (${count()} active)`} From b6c0ae8146c8c02f13e6b2173c53872128bf98c2 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Sun, 2 Aug 2026 10:37:18 +0200 Subject: [PATCH 06/97] feat(auth)!: Remove auth.json credential fallback --- src/lib/opencode-auth.ts | 28 ++++------------------------ tests/lib.opencode-auth.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 083a53bb..16488765 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -1,13 +1,11 @@ /** - * OpenCode auth.json reader + * OpenCode credential database reader * - * Shared helper to read auth from ~/.local/share/opencode/auth.json - * (or platform equivalent). Providers should prefer this to duplicating - * file/path parsing. + * Shared helper to read credentials from OpenCode's SQLite database. + * Providers should prefer this to duplicating database/path parsing. */ import { existsSync } from "fs"; -import { readFile } from "fs/promises"; import { createRequire } from "module"; import { isAbsolute, join, resolve } from "path"; @@ -56,25 +54,7 @@ export function getAuthPath(): string { export async function readAuthFile(): Promise { const { dataDirs } = getOpencodeRuntimeDirCandidates(); - const fileAuth = await readAuthFiles(dataDirs.map((dataDir) => join(dataDir, "auth.json"))); - const databaseAuth = readCredentialDatabases(dataDirs); - - if (!fileAuth && !databaseAuth) return null; - // auth.json is a user-managed compatibility source, so it wins over the current database. - return { ...databaseAuth, ...fileAuth }; -} - -async function readAuthFiles(paths: string[]): Promise { - for (const path of paths) { - try { - const content = await readFile(path, "utf-8"); - return JSON.parse(content) as AuthData; - } catch { - // Try next path - } - } - - return null; + return readCredentialDatabases(dataDirs); } function readCredentialDatabases(dataDirs: string[]): AuthData | null { diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 177b09a8..3fe7aa72 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -67,7 +67,7 @@ describe("OpenCode auth reader", () => { }); }); - it("keeps auth.json entries ahead of database credentials", async () => { + it("ignores legacy auth.json entries", async () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); await writeFile( @@ -77,7 +77,7 @@ describe("OpenCode auth reader", () => { await expect(readAuthFile()).resolves.toMatchObject({ "github-copilot": { access: "copilot-access" }, - openai: { access: "file-access" }, + openai: { access: "openai-access" }, }); }); }); From 870e22bc66cdb26b458d640e34f1c533e0ba1e9d Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Thu, 6 Aug 2026 10:24:29 +0200 Subject: [PATCH 07/97] refactor(auth): update credential DB path resolution - src/lib/opencode-auth.ts: update `getCredentialDbPaths` to compute credential database paths from a single base path and return them in a consistent format. - tests/lib.opencode-auth.test.ts: update the `createCredentialDatabase` helper to construct expected paths using the same base-path logic. Why: path construction was duplicated across callers, which made it easy for the test helper to drift from production behavior. Centralizing the path calculation keeps runtime and tests aligned on where credential databases are located. --- src/lib/opencode-auth.ts | 2 +- tests/lib.opencode-auth.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 16488765..4119a751 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -72,7 +72,7 @@ function getCredentialDbPaths(dataDirs: string[]): string[] { if (override === ":memory:") return []; return [isAbsolute(override) ? override : resolve(dataDirs[0] ?? getOpencodeRuntimeDirs().dataDir, override)]; } - return dataDirs.map((dataDir) => join(dataDir, "opencode-next.db")); + return dataDirs.map((dataDir) => join(dataDir, "opencode.db")); } function readCredentialDatabase(path: string): AuthData | null { diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 3fe7aa72..3e23c0fb 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -19,7 +19,7 @@ async function createCredentialDatabase(): Promise<{ dataDir: string; databasePa temporaryDirectories.push(root); const dataDir = join(root, "opencode"); await mkdir(dataDir, { recursive: true }); - const databasePath = join(dataDir, "opencode-next.db"); + const databasePath = join(dataDir, "opencode.db"); const database = new DatabaseSync(databasePath); database.exec(`CREATE TABLE credential ( id TEXT PRIMARY KEY, From 5009ac033a84d76715b764944bb224af561755bb Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Tue, 18 Aug 2026 12:14:36 +0200 Subject: [PATCH 08/97] chore(deps): bump @opentui/core and @opentui/solid to ^0.5.3 - package.json: update @opentui/core and @opentui/solid from ^0.4.3 to ^0.5.3 - pnpm-lock.yaml: refresh lockfile for updated dependency resolution - src/tui-v2.tsx: update `TuiContext` and `plugin::setup` to align with the @opentui 0.5 API; `trigger` helper has a cosmetic-only change - tests/tui-v2-commands.test.ts: update "registers every quota command as a local slash command" test to cover the updated setup behavior --- package.json | 6 +- pnpm-lock.yaml | 122 +++++++++++++++++----------------- src/tui-v2.tsx | 96 +++++++++++++------------- tests/tui-v2-commands.test.ts | 6 +- 4 files changed, 119 insertions(+), 111 deletions(-) diff --git a/package.json b/package.json index e7c4b9ad..ae301724 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@babel/core": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@biomejs/biome": "2.3.11", - "@opencode-ai/plugin": "1.18.11", + "@opencode-ai/plugin": "1.18.1", "@opentelemetry/sdk-metrics": "2.10.0", "@types/node": "^22.0.0", "babel-preset-solid": "^1.9.12", @@ -98,8 +98,8 @@ "dependencies": { "@clack/prompts": "^1.2.0", "@opentelemetry/api": "^1.9.1", - "@opentui/core": "0.4.5", - "@opentui/solid": "0.4.5", + "@opentui/core": "^0.5.3", + "@opentui/solid": "^0.5.3", "comment-json": "^5.0.0", "jsonc-parser": "^3.3.1", "solid-js": "^1.9.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dd66f95..60c8eef5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,11 @@ importers: specifier: ^1.9.1 version: 1.9.1 '@opentui/core': - specifier: 0.4.5 - version: 0.4.5(typescript@7.0.2)(web-tree-sitter@0.25.10) + specifier: ^0.5.3 + version: 0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10) '@opentui/solid': - specifier: 0.4.5 - version: 0.4.5(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) + specifier: ^0.5.3 + version: 0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) comment-json: specifier: ^5.0.0 version: 5.0.0 @@ -43,8 +43,8 @@ importers: specifier: 2.3.11 version: 2.3.11 '@opencode-ai/plugin': - specifier: 1.18.11 - version: 1.18.11(@opentui/core@0.4.5(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.4.5(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)) + specifier: 1.18.1 + version: 1.18.1(@opentui/core@0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)) '@opentelemetry/sdk-metrics': specifier: 2.10.0 version: 2.10.0(@opentelemetry/api@1.9.1) @@ -476,12 +476,12 @@ packages: cpu: [x64] os: [win32] - '@opencode-ai/plugin@1.18.11': - resolution: {integrity: sha512-mgn7+YO2J5tM2sWtV8pPBz6Lz6vWZswBOjUWssLvGaxKPuHR60sZ3Py4/tjI9RNl74aB4lhI0NUlanuWZS8NfA==} + '@opencode-ai/plugin@1.18.1': + resolution: {integrity: sha512-qE2Jg4Jh7NU18Wl69zoCz7fOvHQxKmUXkSq4akAqCixYDrGO7heYG/wfvafVSsx0n6jWhCdIN8cToNU6G4lKfw==} peerDependencies: - '@opentui/core': '>=0.4.5' - '@opentui/keymap': '>=0.4.5' - '@opentui/solid': '>=0.4.5' + '@opentui/core': '>=0.4.3' + '@opentui/keymap': '>=0.4.3' + '@opentui/solid': '>=0.4.3' peerDependenciesMeta: '@opentui/core': optional: true @@ -490,8 +490,8 @@ packages: '@opentui/solid': optional: true - '@opencode-ai/sdk@1.18.11': - resolution: {integrity: sha512-yDImmNv4PhxdMgtiHVNWQWEVwQlAm7Dr0y4XU7CT4dOIbzgO+VP+9I02lAP7Zva1FhGeyI7oKMI2tzB9RUsWaQ==} + '@opencode-ai/sdk@1.18.1': + resolution: {integrity: sha512-2mX9SXSDVtZ9i2yCOiXDXKx981EJp/ObUtaThUNn16jkjfwX/1j8r+UR/BKUmBLYq+zle7TtpXreXuO+gtVjKA==} '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} @@ -519,55 +519,55 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} - '@opentui/core-darwin-arm64@0.4.5': - resolution: {integrity: sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg==} + '@opentui/core-darwin-arm64@0.5.4': + resolution: {integrity: sha512-oETbn6tg/0g+mOvGXy3iot+1Zv7CGr65U1lRaPJ7kpsKGnOxftCpDD1qA0I6eQwfPJa9k7h9D/9yGB3IbweGcg==} cpu: [arm64] os: [darwin] - '@opentui/core-darwin-x64@0.4.5': - resolution: {integrity: sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA==} + '@opentui/core-darwin-x64@0.5.4': + resolution: {integrity: sha512-TIeqCNfAV8xvNAv6oVBYsoGBz/p8CxcYm668OQIeBPUO+irqbQ72vJJa/SwFZrUEAHFmsDELaB6y80oHU5Jm6g==} cpu: [x64] os: [darwin] - '@opentui/core-linux-arm64-musl@0.4.5': - resolution: {integrity: sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw==} + '@opentui/core-linux-arm64-musl@0.5.4': + resolution: {integrity: sha512-8vFWd1dsPZj9fHQKlsGHue3ukp7MRjTSpmIrdneIruOcoK2etccHfd6bqIo4FcNr+HA0eI2FP4ZIL9xhE3r9/w==} cpu: [arm64] os: [linux] libc: [musl] - '@opentui/core-linux-arm64@0.4.5': - resolution: {integrity: sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ==} + '@opentui/core-linux-arm64@0.5.4': + resolution: {integrity: sha512-UrOsX3D5BOO9TI30WvRwEK/lyPPhY75+LwSqeRRe8SV2ON+Ez5QqY4oryTyYC6+yNahTJufz34n0YgwnQaxTNA==} cpu: [arm64] os: [linux] - '@opentui/core-linux-x64-musl@0.4.5': - resolution: {integrity: sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q==} + '@opentui/core-linux-x64-musl@0.5.4': + resolution: {integrity: sha512-YYREqUB3v5K0qWij3A5YWzJkkVXp/EqIiuHZKsAjrUAY/0+Bp8Hn0baLvvvkuklpG9whW+GfwIeUsmp4fxqmCA==} cpu: [x64] os: [linux] libc: [musl] - '@opentui/core-linux-x64@0.4.5': - resolution: {integrity: sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w==} + '@opentui/core-linux-x64@0.5.4': + resolution: {integrity: sha512-1RXzSl6d347O7mUXviXFWlFFyILr7qsVt4jwD3k0UiKtENeEDNi+glM1bKHbXwCdQjfA835QgFA2mbILybK+aQ==} cpu: [x64] os: [linux] - '@opentui/core-win32-arm64@0.4.5': - resolution: {integrity: sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ==} + '@opentui/core-win32-arm64@0.5.4': + resolution: {integrity: sha512-F9sB6suPJmwkLF2MwKEC+OtwP6cn5QspIm4MCh2hmu3mVqSz9GDpYID1X3sAh9/V79EVocqiOSqI3KeCTRouKQ==} cpu: [arm64] os: [win32] - '@opentui/core-win32-x64@0.4.5': - resolution: {integrity: sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg==} + '@opentui/core-win32-x64@0.5.4': + resolution: {integrity: sha512-2/6dPPPJ9xL/bWz9jh+lZeV2g/tbxZ9N4FBIqwhnqSfIYy5jOnscsPZpcN4X6PstfJAo2yf0Ebk/tLTOzOS6TQ==} cpu: [x64] os: [win32] - '@opentui/core@0.4.5': - resolution: {integrity: sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig==} + '@opentui/core@0.5.4': + resolution: {integrity: sha512-czcJKQ72QhTWvu1eWfKg4EPN1GLLND6cP9MOhDyqYzCdIZgxQSJVWYzz6c4/CQGOu/qQLRyce2y1efVu4lgQ0w==} peerDependencies: web-tree-sitter: 0.25.10 - '@opentui/solid@0.4.5': - resolution: {integrity: sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g==} + '@opentui/solid@0.5.4': + resolution: {integrity: sha512-1injDvieTyYXD3RLC2EclaJK8XZ2+qyJ7wGeQPSzpcYEEcanb7G79KVWFlN7YUzqVAhTIictZDoQ6SRvIwL4Ww==} peerDependencies: solid-js: 1.9.12 @@ -933,8 +933,8 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bun-ffi-structs@0.2.4: - resolution: {integrity: sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg==} + bun-ffi-structs@0.3.1: + resolution: {integrity: sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ==} peerDependencies: typescript: ^5 @@ -1966,17 +1966,17 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': optional: true - '@opencode-ai/plugin@1.18.11(@opentui/core@0.4.5(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.4.5(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))': + '@opencode-ai/plugin@1.18.1(@opentui/core@0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))': dependencies: '@ai-sdk/provider': 3.0.8 - '@opencode-ai/sdk': 1.18.11 + '@opencode-ai/sdk': 1.18.1 effect: 4.0.0-beta.83 zod: 4.1.8 optionalDependencies: - '@opentui/core': 0.4.5(typescript@7.0.2)(web-tree-sitter@0.25.10) - '@opentui/solid': 0.4.5(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) + '@opentui/core': 0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10) + '@opentui/solid': 0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) - '@opencode-ai/sdk@1.18.11': + '@opencode-ai/sdk@1.18.1': dependencies: cross-spawn: 7.0.6 @@ -2001,55 +2001,55 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} - '@opentui/core-darwin-arm64@0.4.5': + '@opentui/core-darwin-arm64@0.5.4': optional: true - '@opentui/core-darwin-x64@0.4.5': + '@opentui/core-darwin-x64@0.5.4': optional: true - '@opentui/core-linux-arm64-musl@0.4.5': + '@opentui/core-linux-arm64-musl@0.5.4': optional: true - '@opentui/core-linux-arm64@0.4.5': + '@opentui/core-linux-arm64@0.5.4': optional: true - '@opentui/core-linux-x64-musl@0.4.5': + '@opentui/core-linux-x64-musl@0.5.4': optional: true - '@opentui/core-linux-x64@0.4.5': + '@opentui/core-linux-x64@0.5.4': optional: true - '@opentui/core-win32-arm64@0.4.5': + '@opentui/core-win32-arm64@0.5.4': optional: true - '@opentui/core-win32-x64@0.4.5': + '@opentui/core-win32-x64@0.5.4': optional: true - '@opentui/core@0.4.5(typescript@7.0.2)(web-tree-sitter@0.25.10)': + '@opentui/core@0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10)': dependencies: - bun-ffi-structs: 0.2.4(typescript@7.0.2) + bun-ffi-structs: 0.3.1(typescript@7.0.2) diff: 9.0.0 marked: 17.0.1 string-width: 7.2.0 strip-ansi: 7.1.2 web-tree-sitter: 0.25.10 optionalDependencies: - '@opentui/core-darwin-arm64': 0.4.5 - '@opentui/core-darwin-x64': 0.4.5 - '@opentui/core-linux-arm64': 0.4.5 - '@opentui/core-linux-arm64-musl': 0.4.5 - '@opentui/core-linux-x64': 0.4.5 - '@opentui/core-linux-x64-musl': 0.4.5 - '@opentui/core-win32-arm64': 0.4.5 - '@opentui/core-win32-x64': 0.4.5 + '@opentui/core-darwin-arm64': 0.5.4 + '@opentui/core-darwin-x64': 0.5.4 + '@opentui/core-linux-arm64': 0.5.4 + '@opentui/core-linux-arm64-musl': 0.5.4 + '@opentui/core-linux-x64': 0.5.4 + '@opentui/core-linux-x64-musl': 0.5.4 + '@opentui/core-win32-arm64': 0.5.4 + '@opentui/core-win32-x64': 0.5.4 transitivePeerDependencies: - typescript - '@opentui/solid@0.4.5(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)': + '@opentui/solid@0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)': dependencies: '@babel/core': 7.28.0 '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) - '@opentui/core': 0.4.5(typescript@7.0.2)(web-tree-sitter@0.25.10) + '@opentui/core': 0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10) babel-plugin-module-resolver: 5.0.2 babel-preset-solid: 1.9.12(@babel/core@7.28.0)(solid-js@1.9.12) entities: 7.0.1 @@ -2322,7 +2322,7 @@ snapshots: ieee754: 1.2.1 optional: true - bun-ffi-structs@0.2.4(typescript@7.0.2): + bun-ffi-structs@0.3.1(typescript@7.0.2): dependencies: typescript: 7.0.2 diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 1a72f9a0..ff74a86b 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -45,10 +45,10 @@ type TuiContext = { }) => void; }; ui: { - slot: { - (name: "app", render: () => null): () => void; - (name: "sidebar.content", render: (props: { sessionID: string }) => JSX.Element): () => void; - }; + slot: (claim: + | { append: "app"; render: () => null } + | { append: "sidebar.content"; render: (props: { sessionID: string }) => JSX.Element } + ) => () => void; toast: { show: (toast: Toast) => void }; dialog: { alert: (params: { title: string; message: string }) => Promise; @@ -273,48 +273,54 @@ const plugin = { setup(context: TuiContext) { let disposeEvents: (() => void) | undefined; let activeSessionID: string | undefined; - const disposeApp = context.ui.slot("app", () => { - if (disposeEvents) return null; - registerQuotaCommands(context, () => activeSessionID); - const trigger = (event: TuiEvent, reason: "idle" | "compacted" | "question") => { - const sessionID = getSessionID(event); - if (!sessionID) return; - activeSessionID = sessionID; - void getQuotaMessage(context, sessionID, reason) - .then((quota) => { - if (!quota) return; - context.ui.toast.show({ - variant: "info", - title: "OpenCode Quota", - message: quota.message, - duration: quota.duration, - }); - }) - .catch(reportFailure); - }; - const onStepEnded = context.data.on("session.step.ended", (event) => trigger(event, "idle")); - const onCompacted = context.data.on("session.compacted", (event) => - trigger(event, "compacted"), - ); - const onQuestion = context.data.on("session.tool.success", (event) => { - if (event.data?.tool === "question") trigger(event, "question"); - }); - disposeEvents = () => { - onStepEnded(); - onCompacted(); - onQuestion(); - }; - return null; - }); - const disposeSidebar = context.ui.slot("sidebar.content", (props) => ( - { + const disposeApp = context.ui.slot({ + append: "app", + render: () => { + if (disposeEvents) return null; + registerQuotaCommands(context, () => activeSessionID); + const trigger = (event: TuiEvent, reason: "idle" | "compacted" | "question") => { + const sessionID = getSessionID(event); + if (!sessionID) return; activeSessionID = sessionID; - }} - /> - )); + void getQuotaMessage(context, sessionID, reason) + .then((quota) => { + if (!quota) return; + context.ui.toast.show({ + variant: "info", + title: "OpenCode Quota", + message: quota.message, + duration: quota.duration, + }); + }) + .catch(reportFailure); + }; + const onStepEnded = context.data.on("session.step.ended", (event) => trigger(event, "idle")); + const onCompacted = context.data.on("session.compacted", (event) => + trigger(event, "compacted"), + ); + const onQuestion = context.data.on("session.tool.success", (event) => { + if (event.data?.tool === "question") trigger(event, "question"); + }); + disposeEvents = () => { + onStepEnded(); + onCompacted(); + onQuestion(); + }; + return null; + }, + }); + const disposeSidebar = context.ui.slot({ + append: "sidebar.content", + render: (props) => ( + { + activeSessionID = sessionID; + }} + /> + ), + }); return () => { disposeEvents?.(); disposeApp(); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index d72050c6..2c60936c 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -10,8 +10,8 @@ describe("V2 quota TUI commands", () => { data: { on: vi.fn(() => vi.fn()) }, keymap: { layer: vi.fn((build) => (layer = build())) }, ui: { - slot: vi.fn((name, render) => { - if (name === "app") render({}); + slot: vi.fn((claim) => { + if (claim.append === "app") claim.render(); return vi.fn(); }), toast: { show: vi.fn() }, @@ -36,5 +36,7 @@ describe("V2 quota TUI commands", () => { "tokens_between", ]); expect(layer?.commands.every((command) => command.palette)).toBe(true); + expect(context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "app" })); + expect(context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "sidebar.content" })); }); }); From f02f6a899195802623df46f61b6c5375527ac451 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Fri, 21 Aug 2026 20:01:29 +0200 Subject: [PATCH 09/97] fix(tui): Restore V2 packaging and event triggers Compile the V2 TUI entry to the filename imported by the package and verify the clean distribution can be loaded. Use the current compaction and tool lifecycle events so automatic quota notifications run on OpenCode V2. --- scripts/prepare-tui-dist.mjs | 38 +++++++++++++++++++++----------- src/tui-v2.tsx | 23 +++++++++++++++---- tests/tui-dist-packaging.test.ts | 17 ++++++++++++++ tests/tui-v2-commands.test.ts | 9 +++++++- 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/scripts/prepare-tui-dist.mjs b/scripts/prepare-tui-dist.mjs index 9c63eb29..b7c648d9 100644 --- a/scripts/prepare-tui-dist.mjs +++ b/scripts/prepare-tui-dist.mjs @@ -12,23 +12,35 @@ const distSourcePath = path.join(rootDir, "dist", "tui.tsx"); const distJsPath = path.join(rootDir, "dist", "tui.js"); const distJsxPath = path.join(rootDir, "dist", "tui.jsx"); const distJsxMapPath = path.join(rootDir, "dist", "tui.jsx.map"); +const tuiV2SourcePath = path.join(rootDir, "src", "tui-v2.tsx"); +const tuiV2DistJsPath = path.join(rootDir, "dist", "tui-v2.js"); +const tuiV2DistJsxPath = path.join(rootDir, "dist", "tui-v2.jsx"); +const tuiV2DistJsxMapPath = path.join(rootDir, "dist", "tui-v2.jsx.map"); await fs.copyFile(sourcePath, distSourcePath); -const source = await fs.readFile(sourcePath, "utf8"); -const transformed = await babel.transformAsync(source, { - filename: sourcePath, - configFile: false, - babelrc: false, - presets: [ - [solidPreset, { moduleName: "@opentui/solid", generate: "universal" }], - [typescriptPreset], - ], -}); +for (const [inputPath, outputPath] of [ + [sourcePath, distJsPath], + [tuiV2SourcePath, tuiV2DistJsPath], +]) { + const source = await fs.readFile(inputPath, "utf8"); + const transformed = await babel.transformAsync(source, { + filename: inputPath, + configFile: false, + babelrc: false, + presets: [ + [solidPreset, { moduleName: "@opentui/solid", generate: "universal" }], + [typescriptPreset], + ], + }); -if (!transformed?.code) { - throw new Error("Babel transform returned empty output"); + if (!transformed?.code) { + throw new Error(`Babel transform returned empty output for ${inputPath}`); + } + + await fs.writeFile(outputPath, `${transformed.code}\n`); } -await fs.writeFile(distJsPath, `${transformed.code}\n`); await fs.rm(distJsxPath, { force: true }); await fs.rm(distJsxMapPath, { force: true }); +await fs.rm(tuiV2DistJsxPath, { force: true }); +await fs.rm(tuiV2DistJsxMapPath, { force: true }); diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index ff74a86b..e1bb861d 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -273,6 +273,7 @@ const plugin = { setup(context: TuiContext) { let disposeEvents: (() => void) | undefined; let activeSessionID: string | undefined; + const questionToolCalls = new Set(); const disposeApp = context.ui.slot({ append: "app", render: () => { @@ -295,16 +296,30 @@ const plugin = { .catch(reportFailure); }; const onStepEnded = context.data.on("session.step.ended", (event) => trigger(event, "idle")); - const onCompacted = context.data.on("session.compacted", (event) => + const onCompacted = context.data.on("session.compaction.ended", (event) => trigger(event, "compacted"), ); - const onQuestion = context.data.on("session.tool.success", (event) => { - if (event.data?.tool === "question") trigger(event, "question"); + const onQuestionStarted = context.data.on("session.tool.input.started", (event) => { + const id = event.data?.id; + if (event.data?.name === "question" && typeof id === "string") { + questionToolCalls.add(id); + } + }); + const onQuestionSucceeded = context.data.on("session.tool.success", (event) => { + const id = event.data?.id; + if (typeof id === "string" && questionToolCalls.delete(id)) trigger(event, "question"); + }); + const onQuestionFailed = context.data.on("session.tool.failed", (event) => { + const id = event.data?.id; + if (typeof id === "string") questionToolCalls.delete(id); }); disposeEvents = () => { onStepEnded(); onCompacted(); - onQuestion(); + onQuestionStarted(); + onQuestionSucceeded(); + onQuestionFailed(); + questionToolCalls.clear(); }; return null; }, diff --git a/tests/tui-dist-packaging.test.ts b/tests/tui-dist-packaging.test.ts index 55203ad3..1e331819 100644 --- a/tests/tui-dist-packaging.test.ts +++ b/tests/tui-dist-packaging.test.ts @@ -20,6 +20,8 @@ vi.mock("@opentui/solid", () => ({ setProp: vi.fn(), })); +vi.mock("@opentui/solid/preload", () => ({})); + async function exists(url: URL): Promise { try { await access(fileURLToPath(url)); @@ -32,12 +34,18 @@ async function exists(url: URL): Promise { describe("tui dist packaging", () => { it("ships the precompiled TUI entry and removes stale jsx artifacts", async () => { const distTui = new URL("../dist/tui.js", import.meta.url); + const distTuiV2 = new URL("../dist/tui-v2.js", import.meta.url); const distJsx = new URL("../dist/tui.jsx", import.meta.url); const distJsxMap = new URL("../dist/tui.jsx.map", import.meta.url); + const distTuiV2Jsx = new URL("../dist/tui-v2.jsx", import.meta.url); + const distTuiV2JsxMap = new URL("../dist/tui-v2.jsx.map", import.meta.url); expect(await exists(distTui)).toBe(true); + expect(await exists(distTuiV2)).toBe(true); expect(await exists(distJsx)).toBe(false); expect(await exists(distJsxMap)).toBe(false); + expect(await exists(distTuiV2Jsx)).toBe(false); + expect(await exists(distTuiV2JsxMap)).toBe(false); const source = await readFile(distTui, "utf8"); expect(source).toContain("createComponent"); @@ -60,4 +68,13 @@ describe("tui dist packaging", () => { }); expect(typeof mod.default.tui).toBe("function"); }); + + it("can load the packaged root module", async () => { + const mod = await import("../dist/index.js"); + + expect(mod.default).toMatchObject({ + id: "@slkiser/opencode-quota", + }); + expect(typeof mod.default.setup).toBe("function"); + }); }); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 2c60936c..4894e7d9 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import plugin from "../src/tui-v2.js"; +import plugin from "../src/tui-v2.tsx"; describe("V2 quota TUI commands", () => { it("registers every quota command as a local slash command", () => { @@ -38,5 +38,12 @@ describe("V2 quota TUI commands", () => { expect(layer?.commands.every((command) => command.palette)).toBe(true); expect(context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "app" })); expect(context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "sidebar.content" })); + expect(context.data.on.mock.calls.map(([event]) => event)).toEqual([ + "session.step.ended", + "session.compaction.ended", + "session.tool.input.started", + "session.tool.success", + "session.tool.failed", + ]); }); }); From c57bef883f5089c573df02a5203e9dea585d85da Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Fri, 21 Aug 2026 21:32:42 +0200 Subject: [PATCH 10/97] fix(auth): report OpenCode credential database sources --- contributing/provider-template/README.md | 2 +- contributing/provider-template/config.test.ts | 2 +- contributing/provider-template/config.ts | 4 +- docs/readme/configuration.md | 8 +-- docs/readme/providers.md | 10 +-- docs/readme/troubleshooting.md | 6 +- src/lib/alibaba-auth.ts | 26 +++---- src/lib/anthropic-auth.ts | 2 +- src/lib/anthropic.ts | 2 +- src/lib/api-key-resolver.ts | 28 ++++---- src/lib/chutes-config.ts | 10 +-- src/lib/deepseek-auth.ts | 10 +-- src/lib/display-sanitize.ts | 2 +- src/lib/entries.ts | 4 +- src/lib/google.ts | 2 +- src/lib/kilo-config.ts | 10 +-- src/lib/kimi-auth.ts | 12 ++-- src/lib/minimax-auth.ts | 28 ++++---- src/lib/nanogpt-config.ts | 10 +-- src/lib/ollama-cloud-config.ts | 10 +-- src/lib/openai.ts | 2 +- src/lib/opencode-auth.ts | 71 ++++++++++++++----- src/lib/opencode-go-auth.ts | 12 ++-- src/lib/quota-providers-remote.ts | 10 +-- src/lib/quota-state-codec.ts | 10 +-- src/lib/quota-status.ts | 18 ++--- src/lib/synthetic-config.ts | 10 +-- src/lib/types.ts | 8 +-- src/lib/xai.ts | 2 +- src/lib/zai-auth.ts | 12 ++-- src/lib/zhipu-auth.ts | 12 ++-- src/providers/alibaba-coding-plan.ts | 3 +- src/providers/deepseek.ts | 2 +- src/providers/glm-coding-plan-provider.ts | 2 +- src/providers/kilo.ts | 2 +- src/providers/nanogpt.ts | 2 +- src/providers/ollama-cloud.ts | 2 +- src/providers/opencode-go.ts | 2 +- src/providers/quota-providers.ts | 20 +++--- src/providers/result-helpers.ts | 8 +-- tests/helpers/provider-api-key-contract.ts | 4 +- tests/helpers/trusted-config-test-harness.ts | 2 +- tests/lib.alibaba-auth.test.ts | 26 +++---- tests/lib.api-key-provider-queries.test.ts | 2 +- tests/lib.api-key-resolver.test.ts | 16 ++--- tests/lib.glm-coding-plan.test.ts | 2 +- tests/lib.invalid-aware-provider-auth.test.ts | 14 ++-- tests/lib.minimax-auth.test.ts | 28 ++++---- tests/lib.ollama-cloud-config.test.ts | 10 +-- tests/lib.opencode-auth.test.ts | 41 +++++++++-- tests/lib.opencode-go-auth.test.ts | 18 ++--- tests/lib.openrouter.test.ts | 12 ++-- tests/lib.provider-api-key-configs.test.ts | 12 ++-- tests/lib.quota-export.test.ts | 8 +-- tests/lib.quota-providers-auth.test.ts | 21 ++---- tests/lib.quota-state-codec.test.ts | 8 ++- tests/lib.quota-status.test.ts | 52 +++++++------- tests/plugin.qwen-hook.test.ts | 4 +- tests/provider-surfaces.opencode-go.test.ts | 4 +- tests/providers.alibaba-coding-plan.test.ts | 2 +- tests/providers.deepseek.test.ts | 2 +- tests/providers.glm-coding-plan.test.ts | 4 +- tests/providers.kilo.test.ts | 2 +- tests/providers.kimi-code.test.ts | 2 +- tests/providers.minimax-coding-plan.test.ts | 4 +- tests/providers.nanogpt.test.ts | 2 +- tests/providers.ollama-cloud.test.ts | 2 +- tests/providers.opencode-go.test.ts | 6 +- tests/providers.quota-providers.test.ts | 8 +-- .../quota-providers-docs-consistency.test.ts | 2 +- tests/quota-render-data.test.ts | 6 +- tests/quota-state.test.ts | 6 +- tests/v4-phase5-cross-surface.test.ts | 6 +- 73 files changed, 398 insertions(+), 338 deletions(-) diff --git a/contributing/provider-template/README.md b/contributing/provider-template/README.md index f1e544fe..4ab04521 100644 --- a/contributing/provider-template/README.md +++ b/contributing/provider-template/README.md @@ -12,7 +12,7 @@ This template is intentionally outside `src/` and is not published to npm. `pack Use this template for API-key/token providers that support all three trusted sources: -1. existing OpenCode API-key/token auth (`auth.json`) +1. existing OpenCode API-key/token auth (`opencode.db`) 2. trusted user/global OpenCode config 3. environment variables diff --git a/contributing/provider-template/config.test.ts b/contributing/provider-template/config.test.ts index 8d9baf40..ae519d0c 100644 --- a/contributing/provider-template/config.test.ts +++ b/contributing/provider-template/config.test.ts @@ -150,7 +150,7 @@ describe("example provider config", () => { const { resolveExampleProviderApiKey } = await import("../src/lib/example-provider-config.js"); await expect(resolveExampleProviderApiKey()).resolves.toEqual({ key: "auth-key", - source: "auth.json", + source: "opencode.db", }); }); }); diff --git a/contributing/provider-template/config.ts b/contributing/provider-template/config.ts index 962a73b1..d58da3a7 100644 --- a/contributing/provider-template/config.ts +++ b/contributing/provider-template/config.ts @@ -22,7 +22,7 @@ export type ExampleProviderKeySource = | "env:EXAMPLE_PROVIDER_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export interface ExampleProviderApiKeyResult { key: string; @@ -39,7 +39,7 @@ export async function resolveExampleProviderApiKey(): Promise.options.apiKey`, then API-key entries in OpenCode `auth.json`. +- Credentials resolve from `apiKeyEnv`, trusted global `provider..options.apiKey`, then API-key entries in OpenCode `opencode.db`. - Definitions run automatically with `enabledProviders: "auto"`. A manual list must include `quota-providers` and every built-in provider you still want. - To tune maintained estimates, use the reserved `qwen-code` or `alibaba-coding-plan` ID and its maintained window shape. Do not add a duplicate normal provider block. - Project secrets, scripts, custom headers, executable mappings, regular expressions, and JSONPath are not accepted. @@ -407,11 +407,11 @@ Existing `experimental.quotaToast` settings remain supported. Quota settings do | `cursorIncludedApiUsd` | unset | Override Cursor monthly included API budget in USD. | | `cursorBillingCycleStartDay` | unset | Local billing-cycle anchor day `1..28`; when unset, Cursor usage resets on the local calendar month. | -Kilo Gateway has no `quota-toast.json` credential setting. Use `KILO_API_KEY`, trusted user/global `provider.kilo.options.apiKey`, or a strict `kilo` API-key entry in OpenCode `auth.json`; project-local OpenCode config is not read for this secret. See [Kilo Gateway setup](providers.md#kilo-gateway). +Kilo Gateway has no `quota-toast.json` credential setting. Use `KILO_API_KEY`, trusted user/global `provider.kilo.options.apiKey`, or a strict `kilo` API-key entry in OpenCode `opencode.db`; project-local OpenCode config is not read for this secret. See [Kilo Gateway setup](providers.md#kilo-gateway). -Ollama Cloud has no `quota-toast.json` credential setting. Use `OLLAMA_API_KEY`, trusted user/global `provider.ollama-cloud.options.apiKey`, or a strict `ollama-cloud` API-key entry in OpenCode `auth.json`; project-local OpenCode config is not read for this secret. See [Ollama Cloud setup](providers.md#ollama-cloud). +Ollama Cloud has no `quota-toast.json` credential setting. Use `OLLAMA_API_KEY`, trusted user/global `provider.ollama-cloud.options.apiKey`, or a strict `ollama-cloud` API-key entry in OpenCode `opencode.db`; project-local OpenCode config is not read for this secret. See [Ollama Cloud setup](providers.md#ollama-cloud). -OpenCode Go has no `quota-toast.json` workspace ID, cookie, endpoint, credential, or token setting. It automatically uses `OPENCODE_API_KEY`, trusted user/global `provider.opencode.options.apiKey`, or a strict `opencode-go` API-key entry in OpenCode `auth.json`, with legacy `opencode` supported only as a fallback. `opencodeGoWindows` filters the Five-hour, Weekly, and Monthly rows returned by the official usage API, including the expanded sidebar. Use `tuiSidebarPanel.opencodeGoPreferredWindow` to prefer one available row only while the sidebar is collapsed. See [OpenCode Go setup](providers.md#opencode-go). +OpenCode Go has no `quota-toast.json` workspace ID, cookie, endpoint, credential, or token setting. It automatically uses `OPENCODE_API_KEY`, trusted user/global `provider.opencode.options.apiKey`, or a strict `opencode-go` API-key entry in OpenCode `opencode.db`, with legacy `opencode` supported only as a fallback. `opencodeGoWindows` filters the Five-hour, Weekly, and Monthly rows returned by the official usage API, including the expanded sidebar. Use `tuiSidebarPanel.opencodeGoPreferredWindow` to prefer one available row only while the sidebar is collapsed. See [OpenCode Go setup](providers.md#opencode-go). Xiaomi MiMo has no `quota-toast.json` credential or endpoint setting. Use `MIMO_USAGE_COOKIE` or trusted user/global `opencode-quota/mimo.json`; see [Xiaomi MiMo setup](providers.md#xiaomi-mimo). diff --git a/docs/readme/providers.md b/docs/readme/providers.md index 9b9e65ec..146df71e 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -260,7 +260,7 @@ Credentials are checked in this order: 1. The environment variable named by `apiKeyEnv`. 2. Trusted global `provider..options.apiKey`. -3. An API-key entry in OpenCode `auth.json`. +3. An API-key entry in OpenCode `opencode.db`. Project secrets are never read. Custom definitions cannot add scripts, methods, headers, templates, executable mappings, regular expressions, JSONPath, or automatic endpoint discovery. @@ -387,7 +387,7 @@ claude auth status If Claude lives at a custom path, set `anthropicBinaryPath` in `opencode-quota/quota-toast.json`. -When Claude Code does not expose quota windows itself, quota is read from Anthropic's OAuth usage endpoint using the first usable access token: OpenCode's own `anthropic` OAuth credential from `auth.json`, then Claude Code's credentials. `/quota_status` reports which store answered as `oauth_credential_source`. +When Claude Code does not expose quota windows itself, quota is read from Anthropic's OAuth usage endpoint using the first usable access token: OpenCode's own `anthropic` OAuth credential from `opencode.db`, then Claude Code's credentials. `/quota_status` reports which store answered as `oauth_credential_source`. @@ -517,7 +517,7 @@ Credentials resolve in this order: 1. `KILO_API_KEY` 2. Trusted user/global OpenCode config: `provider.kilo.options.apiKey` -3. A strict `kilo` API-key entry in OpenCode `auth.json`: `{ "type": "api", "key": "..." }` +3. A strict `kilo` API-key entry in OpenCode `opencode.db`: `{ "type": "api", "key": "..." }` Project-local `opencode.json` and `opencode.jsonc` files are not read for this secret. The canonical OpenCode provider ID is `kilo`; if you use manual provider selection, include `kilo` in `enabledProviders`. @@ -572,7 +572,7 @@ Credentials resolve in this order: 1. `OLLAMA_API_KEY` 2. Trusted user/global OpenCode config: `provider.ollama-cloud.options.apiKey` -3. A strict `ollama-cloud` API-key entry in OpenCode `auth.json`: `{ "type": "api", "key": "..." }` +3. A strict `ollama-cloud` API-key entry in OpenCode `opencode.db`: `{ "type": "api", "key": "..." }` Project-local `opencode.json` and `opencode.jsonc` files are not read for this secret. The old `OLLAMA_USAGE_COOKIE`, `ollama-cloud.json`, and `ollama-usage/config.yaml` cookie setup is no longer supported. @@ -584,7 +584,7 @@ OpenCode Go reads subscription quota from the official `https://opencode.ai/zen/ 1. `OPENCODE_API_KEY` 2. Trusted user/global OpenCode config: `provider.opencode.options.apiKey` -3. An `opencode-go` API-key entry in OpenCode `auth.json`: `{ "type": "api", "key": "..." }`. This is the key the OpenCode CLI writes via `opencode auth login -p opencode-go`. A legacy `opencode` entry is still accepted as a fallback. +3. An `opencode-go` API-key entry in OpenCode `opencode.db`: `{ "type": "api", "key": "..." }`. This is the key the OpenCode CLI writes via `opencode auth login -p opencode-go`. A legacy `opencode` entry is still accepted as a fallback. Project-local `opencode.json` and `opencode.jsonc` files are not read for this secret. Use `opencodeGoWindows` to choose which validated API results appear across surfaces and in the expanded sidebar: **Five-hour**, **Weekly**, and/or **Monthly**. To keep those rows expanded but prefer one while the sidebar is collapsed, set `tuiSidebarPanel.opencodeGoPreferredWindow` to `rolling`, `weekly`, or `monthly`; an unset or unavailable preference keeps the lowest-remaining selection. These settings do not change authentication or the API request. diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 27a72774..79cebbdc 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -99,7 +99,7 @@ Run `/quota_status` and check the OpenAI auth source and token status. | Symptom | Fix | | --------------------- | ------------------------------------------------------------------------------------------ | -| OpenAI quota missing | Confirm OpenCode native OpenAI OAuth is present in `auth.json`. | +| OpenAI quota missing | Confirm OpenCode native OpenAI OAuth is present in `opencode.db`. | | Token expired | Re-run OpenCode's OpenAI auth flow. | | Provider not detected | Confirm your OpenCode config uses the `openai` provider or a compatible OpenAI auth entry. | @@ -238,8 +238,8 @@ Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_* | Symptom | Fix | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Provider not detected | Set `OPENCODE_API_KEY`, trusted global `provider.opencode.options.apiKey`, or a strict `opencode-go` API-key entry in OpenCode `auth.json`; legacy `opencode` is supported only as a fallback. Then check `auth_state`, `auth_source`, and `auth_checked_paths`. | -| `auth_state` is `invalid` | Fix the primary `opencode-go` record in `auth.json` so it is `{ "type": "api", "key": "..." }`. A malformed primary record blocks the legacy `opencode` fallback and is reported in `auth_error`. | +| Provider not detected | Set `OPENCODE_API_KEY`, trusted global `provider.opencode.options.apiKey`, or a strict `opencode-go` API-key entry in OpenCode `opencode.db`; legacy `opencode` is supported only as a fallback. Then check `auth_state`, `auth_source`, and `auth_checked_paths`. | +| `auth_state` is `invalid` | Fix the primary `opencode-go` record in `opencode.db` so it is `{ "type": "api", "key": "..." }`. A malformed primary record blocks the legacy `opencode` fallback and is reported in `auth_error`. | | API returns 401 or 403 | The usage API rejected the key. Update the winning source shown by `auth_source`, wait briefly for credential caching to expire, and rerun `/quota_status`. | | Invalid API response | Check `live_fetch_error`. OpenCode Quota requires valid 5h, Weekly, and Monthly results, so one missing or malformed API window rejects the full response instead of showing partial quota. | | API request times out or fails | Check `live_fetch_error`, confirm `https://opencode.ai/zen/go/v1/usage` is reachable, and retry. Increase `requestTimeoutMs` only when the error is a timeout. | diff --git a/src/lib/alibaba-auth.ts b/src/lib/alibaba-auth.ts index d4afc8f5..f8f0889e 100644 --- a/src/lib/alibaba-auth.ts +++ b/src/lib/alibaba-auth.ts @@ -5,7 +5,7 @@ import { resolveApiKeyFromEnvAndConfig, } from "./api-key-resolver.js"; import { sanitizeDisplayText } from "./display-sanitize.js"; -import { getAuthPaths, readAuthFileCached } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFileCached } from "./opencode-auth.js"; import type { AlibabaAuthData, AlibabaCodingPlanTier, AuthData } from "./types.js"; export const DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS = 5_000; @@ -19,7 +19,7 @@ export type AlibabaCodingPlanKeySource = | "env:ALIBABA_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export type ResolvedAlibabaCodingPlanAuth = | { state: "none" } @@ -31,20 +31,20 @@ export type AlibabaCodingPlanAuthDiagnostics = state: "none"; source: null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; } | { state: "configured"; source: AlibabaCodingPlanKeySource; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; tier: AlibabaCodingPlanTier; } | { state: "invalid"; - source: "auth.json"; + source: "opencode.db"; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; error: string; rawTier?: string; }; @@ -202,7 +202,7 @@ async function resolveAlibabaCodingPlanAuthWithSource(params?: { return { auth, - source: auth.state === "none" ? null : "auth.json", + source: auth.state === "none" ? null : "opencode.db", }; } @@ -222,23 +222,23 @@ export async function getAlibabaCodingPlanAuthDiagnostics(params?: { envVarNames: [...ALLOWED_ALIBABA_ENV_VARS], getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, }); - const authPaths = getAuthPaths(); + const credentialDatabasePaths = getCredentialDatabasePaths(); if (auth.state === "none") { return { state: "none", source: null, checkedPaths, - authPaths, + credentialDatabasePaths, }; } if (auth.state === "invalid") { return { state: "invalid", - source: "auth.json", + source: "opencode.db", checkedPaths, - authPaths, + credentialDatabasePaths, error: auth.error, rawTier: auth.rawTier, }; @@ -246,9 +246,9 @@ export async function getAlibabaCodingPlanAuthDiagnostics(params?: { return { state: "configured", - source: source ?? "auth.json", + source: source ?? "opencode.db", checkedPaths, - authPaths, + credentialDatabasePaths, tier: auth.tier, }; } diff --git a/src/lib/anthropic-auth.ts b/src/lib/anthropic-auth.ts index 1394c7e2..3ea6042b 100644 --- a/src/lib/anthropic-auth.ts +++ b/src/lib/anthropic-auth.ts @@ -1,7 +1,7 @@ /** * OpenCode-managed Anthropic OAuth credentials. * - * Reads the `anthropic` OAuth entry from OpenCode's own auth.json. This is the + * Reads the `anthropic` OAuth entry from OpenCode's own opencode.db. This is the * credential OpenCode refreshes for Anthropic subscription models, so it stays * usable even when a separately installed Claude Code has stale credentials. */ diff --git a/src/lib/anthropic.ts b/src/lib/anthropic.ts index 8eea5b6b..ced40de7 100644 --- a/src/lib/anthropic.ts +++ b/src/lib/anthropic.ts @@ -4,7 +4,7 @@ * Uses the local Claude CLI/runtime to detect install/auth state first. When * Claude auth is confirmed but local quota windows are missing, it falls back * to Anthropic's OAuth usage endpoint using the first usable OAuth access - * token: OpenCode's own auth.json, then Claude OAuth credentials (macOS + * token: OpenCode's own opencode.db, then Claude OAuth credentials (macOS * Keychain first, then the local credentials file). */ diff --git a/src/lib/api-key-resolver.ts b/src/lib/api-key-resolver.ts index 4bcf3db3..ef6f1397 100644 --- a/src/lib/api-key-resolver.ts +++ b/src/lib/api-key-resolver.ts @@ -1,5 +1,5 @@ /** - * Generic API key resolution from env vars, config files, and auth.json. + * Generic API key resolution from env vars, config files, and opencode.db. * * Used by provider-specific config modules (synthetic-config, chutes-config) * to resolve API keys with consistent priority and behavior. @@ -168,10 +168,10 @@ export interface ResolveEnvAndConfigApiKeyConfig { /** Configuration for resolving an API key from multiple sources */ export interface ResolveApiKeyConfig extends ResolveEnvAndConfigApiKeyConfig { - /** Extract API key from auth.json data. Returns null if not found. */ + /** Extract API key from opencode.db data. Returns null if not found. */ extractFromAuth: (auth: unknown) => string | null; - /** Source label for auth.json */ + /** Source label for opencode.db */ authSource: Source; } @@ -188,7 +188,7 @@ export interface ResolveProviderApiKeyBaseConfig { export interface StrictApiKeyAuthConfig { policy?: "strict-api-key"; readAuth: () => Promise; - getAuthPaths?: () => string[]; + getCredentialDatabasePaths?: () => string[]; authKeys?: readonly string[]; authSource: Source; } @@ -196,7 +196,7 @@ export interface StrictApiKeyAuthConfig { export interface InvalidAwareApiKeyAuthConfig { policy: "invalid-aware-api-key"; readAuth: (maxAgeMs: number) => Promise; - getAuthPaths: () => string[]; + getCredentialDatabasePaths: () => string[]; authKeys: readonly string[]; authSource: AuthSource; displayName: string; @@ -210,7 +210,7 @@ export interface ResolveProviderApiKeyConfig auth?: StrictApiKeyAuthConfig; } -/** Configuration for providers that surface malformed winning auth.json entries. */ +/** Configuration for providers that surface malformed winning opencode.db entries. */ export interface ResolveInvalidAwareProviderApiKeyConfig< Source extends string, AuthSource extends Source, @@ -228,19 +228,19 @@ export type InvalidAwareAuthDiagnostics { configured: boolean; source: Source | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; }>; } @@ -361,7 +361,7 @@ function createInvalidAwareProviderApiKeyResolver envVar.name), getConfigCandidates: config.getConfigCandidates, }), - authPaths: config.auth.getAuthPaths(), + credentialDatabasePaths: config.auth.getCredentialDatabasePaths(), }; if (auth.state === "none") return { state: "none", source: null, ...paths }; if (auth.state === "invalid") { @@ -409,7 +409,7 @@ export function createProviderApiKeyResolver( return resolvedFromEnvOrConfig; } - // 3. Fallback to auth.json + // 3. Fallback to opencode.db const auth = await readAuth(); const key = config.extractFromAuth(auth); if (key) { diff --git a/src/lib/chutes-config.ts b/src/lib/chutes-config.ts index 84f3a9d1..a39b16eb 100644 --- a/src/lib/chutes-config.ts +++ b/src/lib/chutes-config.ts @@ -2,7 +2,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFile } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; export interface ChutesApiKeyResult { key: string; @@ -16,7 +16,7 @@ export type ChutesKeySource = | "env:CHUTES_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; @@ -29,8 +29,8 @@ const chutesApiKeyResolver = createProviderApiKeyResolver({ getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { readAuth: readAuthFile, - getAuthPaths, - authSource: "auth.json", + getCredentialDatabasePaths, + authSource: "opencode.db", }, }); @@ -46,7 +46,7 @@ export async function getChutesKeyDiagnostics(): Promise<{ configured: boolean; source: ChutesKeySource | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; }> { return chutesApiKeyResolver.diagnostics(); } diff --git a/src/lib/deepseek-auth.ts b/src/lib/deepseek-auth.ts index 6377234b..d2324193 100644 --- a/src/lib/deepseek-auth.ts +++ b/src/lib/deepseek-auth.ts @@ -2,7 +2,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFile } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; export interface DeepSeekApiKeyResult { key: string; @@ -16,7 +16,7 @@ export type DeepSeekKeySource = | "env:DEEPSEEK_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; @@ -29,8 +29,8 @@ const deepseekApiKeyResolver = createProviderApiKeyResolver({ getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { readAuth: readAuthFile, - getAuthPaths, - authSource: "auth.json", + getCredentialDatabasePaths, + authSource: "opencode.db", }, }); @@ -46,7 +46,7 @@ export async function getDeepSeekKeyDiagnostics(): Promise<{ configured: boolean; source: DeepSeekKeySource | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; }> { return deepseekApiKeyResolver.diagnostics(); } diff --git a/src/lib/display-sanitize.ts b/src/lib/display-sanitize.ts index 12c3de9f..d82e941d 100644 --- a/src/lib/display-sanitize.ts +++ b/src/lib/display-sanitize.ts @@ -129,7 +129,7 @@ export function sanitizeQuotaProviderResult(result: QuotaProviderResult): QuotaP ...diagnostic, modelIds: diagnostic.modelIds ? [...diagnostic.modelIds] : null, checkedPaths: [...diagnostic.checkedPaths], - authPaths: [...diagnostic.authPaths], + credentialDatabasePaths: [...diagnostic.credentialDatabasePaths], })), } : {}), diff --git a/src/lib/entries.ts b/src/lib/entries.ts index 6eff4c5f..d04f9f20 100644 --- a/src/lib/entries.ts +++ b/src/lib/entries.ts @@ -287,7 +287,7 @@ export interface QuotaProviderDiagnostic { | "explicit_env" | "global_opencode_json" | "global_opencode_jsonc" - | "auth_json" + | "opencode_db" | null; outcome: | "missing_credential" @@ -304,7 +304,7 @@ export interface QuotaProviderDiagnostic { httpStatus?: number; entryCount: number; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; statePath?: string; stateHealth?: "missing" | "healthy" | "malformed" | "version_mismatch"; stateVersion?: number | null; diff --git a/src/lib/google.ts b/src/lib/google.ts index 2cdb74aa..3ff1ed75 100644 --- a/src/lib/google.ts +++ b/src/lib/google.ts @@ -16,7 +16,7 @@ import { import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; // NOTE: Google Antigravity auth differs intentionally from Qwen: -// - Qwen reads OpenCode auth.json key "qwen-code" first, then falls back to +// - Qwen reads OpenCode opencode.db key "qwen-code" first, then falls back to // legacy key "opencode-qwencode-auth", and uses local quota state. // - Google refresh flow requires upstream OAuth client credentials from // opencode-antigravity-auth to match that plugin's runtime behavior. diff --git a/src/lib/kilo-config.ts b/src/lib/kilo-config.ts index ec080b1b..58da7ed9 100644 --- a/src/lib/kilo-config.ts +++ b/src/lib/kilo-config.ts @@ -2,14 +2,14 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFile } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; export interface KiloApiKeyResult { key: string; source: KiloKeySource; } -export type KiloKeySource = "env:KILO_API_KEY" | "opencode.json" | "opencode.jsonc" | "auth.json"; +export type KiloKeySource = "env:KILO_API_KEY" | "opencode.json" | "opencode.jsonc" | "opencode.db"; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; @@ -22,8 +22,8 @@ const kiloApiKeyResolver = createProviderApiKeyResolver({ getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { readAuth: readAuthFile, - getAuthPaths, - authSource: "auth.json", + getCredentialDatabasePaths, + authSource: "opencode.db", }, }); @@ -39,7 +39,7 @@ export async function getKiloKeyDiagnostics(): Promise<{ configured: boolean; source: KiloKeySource | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; }> { return kiloApiKeyResolver.diagnostics(); } diff --git a/src/lib/kimi-auth.ts b/src/lib/kimi-auth.ts index c566e51a..e1ab8c5e 100644 --- a/src/lib/kimi-auth.ts +++ b/src/lib/kimi-auth.ts @@ -3,7 +3,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFileCached } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFileCached } from "./opencode-auth.js"; import type { AuthData } from "./types.js"; export const DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS = 5_000; @@ -16,14 +16,14 @@ export type KimiKeySource = | "env:KIMI_CODE_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export type ResolvedKimiAuth = InvalidAwareAuthResult; -export type KimiAuthDiagnostics = InvalidAwareAuthDiagnostics; +export type KimiAuthDiagnostics = InvalidAwareAuthDiagnostics; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; -const kimiAuthResolver = createProviderApiKeyResolver({ +const kimiAuthResolver = createProviderApiKeyResolver({ envVars: [ { name: "KIMI_API_KEY", source: "env:KIMI_API_KEY" }, { name: "KIMI_CODE_API_KEY", source: "env:KIMI_CODE_API_KEY" }, @@ -36,11 +36,11 @@ const kimiAuthResolver = createProviderApiKeyResolver readAuthFileCached({ maxAgeMs }), - getAuthPaths, + getCredentialDatabasePaths, }, }); diff --git a/src/lib/minimax-auth.ts b/src/lib/minimax-auth.ts index d61a70a0..21360efc 100644 --- a/src/lib/minimax-auth.ts +++ b/src/lib/minimax-auth.ts @@ -2,7 +2,7 @@ * MiniMax auth resolver * * Resolves MiniMax credentials from trusted env vars, trusted user/global - * OpenCode config, and auth.json fallback into the standardized shape used + * OpenCode config, and opencode.db fallback into the standardized shape used * by the MiniMax Coding Plan providers. */ @@ -15,7 +15,7 @@ import { import { sanitizeDisplayText } from "./display-sanitize.js"; import { resolveEnvTemplate } from "./env-template.js"; import type { MiniMaxQuotaEndpointId } from "./minimax-endpoints.js"; -import { getAuthPaths, readAuthFileCached } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFileCached } from "./opencode-auth.js"; import type { AuthData, MiniMaxAuthData } from "./types.js"; export const DEFAULT_MINIMAX_AUTH_CACHE_MAX_AGE_MS = 5_000; @@ -26,9 +26,9 @@ export type MiniMaxKeySource = | "env:MINIMAX_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; -type MiniMaxInvalidSource = "opencode.json" | "opencode.jsonc" | "auth.json"; +type MiniMaxInvalidSource = "opencode.json" | "opencode.jsonc" | "opencode.db"; export type ResolvedMiniMaxAuth = | { state: "none" } @@ -40,20 +40,20 @@ export type MiniMaxAuthDiagnostics = state: "none"; source: null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; } | { state: "configured"; source: MiniMaxKeySource; endpoint: MiniMaxQuotaEndpointId; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; } | { state: "invalid"; source: MiniMaxInvalidSource; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; error: string; }; @@ -250,7 +250,7 @@ async function resolveMiniMaxAuthWithSource( return { auth, - source: auth.state === "none" ? null : "auth.json", + source: auth.state === "none" ? null : "opencode.db", }; } @@ -277,33 +277,33 @@ async function getMiniMaxAuthDiagnosticsForSpec( envVarNames: spec.envVars.map((envVar) => envVar.name), getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, }); - const authPaths = getAuthPaths(); + const credentialDatabasePaths = getCredentialDatabasePaths(); if (auth.state === "none") { return { state: "none", source: null, checkedPaths, - authPaths, + credentialDatabasePaths, }; } if (auth.state === "invalid") { return { state: "invalid", - source: (source ?? "auth.json") as MiniMaxInvalidSource, + source: (source ?? "opencode.db") as MiniMaxInvalidSource, checkedPaths, - authPaths, + credentialDatabasePaths, error: auth.error, }; } return { state: "configured", - source: (source ?? "auth.json") as MiniMaxKeySource, + source: (source ?? "opencode.db") as MiniMaxKeySource, endpoint: auth.endpoint, checkedPaths, - authPaths, + credentialDatabasePaths, }; } diff --git a/src/lib/nanogpt-config.ts b/src/lib/nanogpt-config.ts index 7f48bd27..32968bc0 100644 --- a/src/lib/nanogpt-config.ts +++ b/src/lib/nanogpt-config.ts @@ -2,7 +2,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFile } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; export interface NanoGptApiKeyResult { key: string; @@ -17,7 +17,7 @@ export type NanoGptKeySource = | "env:NANO_GPT_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; @@ -33,8 +33,8 @@ const nanoGptApiKeyResolver = createProviderApiKeyResolver({ getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { readAuth: readAuthFile, - getAuthPaths, - authSource: "auth.json", + getCredentialDatabasePaths, + authSource: "opencode.db", }, }); @@ -50,7 +50,7 @@ export async function getNanoGptKeyDiagnostics(): Promise<{ configured: boolean; source: NanoGptKeySource | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; }> { return nanoGptApiKeyResolver.diagnostics(); } diff --git a/src/lib/ollama-cloud-config.ts b/src/lib/ollama-cloud-config.ts index 10694a92..e5e0e8bc 100644 --- a/src/lib/ollama-cloud-config.ts +++ b/src/lib/ollama-cloud-config.ts @@ -2,7 +2,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFile } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; export interface OllamaCloudApiKeyResult { key: string; @@ -13,7 +13,7 @@ export type OllamaCloudKeySource = | "env:OLLAMA_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; @@ -26,8 +26,8 @@ const ollamaCloudApiKeyResolver = createProviderApiKeyResolver { return ollamaCloudApiKeyResolver.diagnostics(); } diff --git a/src/lib/openai.ts b/src/lib/openai.ts index 0d5cee70..1bbc4e37 100644 --- a/src/lib/openai.ts +++ b/src/lib/openai.ts @@ -1,7 +1,7 @@ /** * OpenAI (ChatGPT) quota fetcher * - * Uses OpenCode's auth.json native OpenCode OAuth entries and queries: + * Uses OpenCode's opencode.db native OpenCode OAuth entries and queries: * https://chatgpt.com/backend-api/wham/usage */ diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 4119a751..29d23750 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -24,7 +24,10 @@ type CredentialDatabase = { prepare(sql: string): { all(...params: string[]): unknown[] }; }; -type CredentialDatabaseConstructor = new (path: string, options?: Record) => CredentialDatabase; +type CredentialDatabaseConstructor = new ( + path: string, + options?: Record, +) => CredentialDatabase; type AuthCacheEntry = { timestamp: number; @@ -35,30 +38,47 @@ type AuthCacheEntry = { let authCache: AuthCacheEntry | null = null; /** - * Get candidate auth.json paths in priority order. - * Some OpenCode installations use Linux-style paths even on macOS, - * so we check multiple locations. + * Get candidate legacy auth.json paths for Cursor OAuth compatibility. + * Some OpenCode installations use Linux-style paths even on macOS, so we + * check multiple locations. */ export function getAuthPaths(): string[] { - // OpenCode stores auth at `${Global.Path.data}/auth.json`. - // We generate candidates based on OpenCode runtime dir semantics (xdg-basedir) - // plus platform fallbacks for alternate/legacy installs. + // Generate legacy candidates from OpenCode runtime dir semantics (xdg-basedir) + // plus platform fallbacks for alternate installs. const { dataDirs } = getOpencodeRuntimeDirCandidates(); return dataDirs.map((d) => join(d, "auth.json")); } -/** Returns OpenCode's primary auth.json path (for display/logging) */ +/** Returns the primary legacy auth.json path used for Cursor OAuth compatibility. */ export function getAuthPath(): string { return join(getOpencodeRuntimeDirs().dataDir, "auth.json"); } -export async function readAuthFile(): Promise { +/** + * Get candidate OpenCode credential database paths in priority order. + * + * `OPENCODE_DB` overrides the normal runtime data-directory candidates. + */ +export function getCredentialDatabasePaths(): string[] { const { dataDirs } = getOpencodeRuntimeDirCandidates(); - return readCredentialDatabases(dataDirs); + return getCredentialDatabasePathsForDataDirs(dataDirs); +} + +/** Returns OpenCode's primary credential database path for display/logging. */ +export function getCredentialDatabasePath(): string { + const paths = getCredentialDatabasePaths(); + if (paths[0]) return paths[0]; + return process.env.OPENCODE_DB?.trim() === ":memory:" + ? ":memory:" + : join(getOpencodeRuntimeDirs().dataDir, "opencode.db"); } -function readCredentialDatabases(dataDirs: string[]): AuthData | null { - for (const path of getCredentialDbPaths(dataDirs)) { +export async function readAuthFile(): Promise { + return readCredentialDatabases(getCredentialDatabasePaths()); +} + +function readCredentialDatabases(paths: string[]): AuthData | null { + for (const path of paths) { const auth = readCredentialDatabase(path); if (auth) return auth; } @@ -66,11 +86,15 @@ function readCredentialDatabases(dataDirs: string[]): AuthData | null { return null; } -function getCredentialDbPaths(dataDirs: string[]): string[] { +function getCredentialDatabasePathsForDataDirs(dataDirs: string[]): string[] { const override = process.env.OPENCODE_DB?.trim(); if (override) { if (override === ":memory:") return []; - return [isAbsolute(override) ? override : resolve(dataDirs[0] ?? getOpencodeRuntimeDirs().dataDir, override)]; + return [ + isAbsolute(override) + ? override + : resolve(dataDirs[0] ?? getOpencodeRuntimeDirs().dataDir, override), + ]; } return dataDirs.map((dataDir) => join(dataDir, "opencode.db")); } @@ -82,12 +106,19 @@ function readCredentialDatabase(path: string): AuthData | null { try { database = openCredentialDatabase(path); const rows = database - .prepare("SELECT integration_id, value FROM credential WHERE integration_id IS NOT NULL ORDER BY time_updated DESC, id DESC") + .prepare( + "SELECT integration_id, value FROM credential WHERE integration_id IS NOT NULL ORDER BY time_updated DESC, id DESC", + ) .all() as Array<{ integration_id?: unknown; value?: unknown }>; const auth: Record = {}; for (const row of rows) { - if (typeof row.integration_id !== "string" || typeof row.value !== "string" || row.integration_id in auth) continue; + if ( + typeof row.integration_id !== "string" || + typeof row.value !== "string" || + row.integration_id in auth + ) + continue; const value = parseCredentialValue(row.value); if (value) auth[row.integration_id] = value; } @@ -102,11 +133,15 @@ function readCredentialDatabase(path: string): AuthData | null { function openCredentialDatabase(path: string): CredentialDatabase { if ("Bun" in globalThis) { - const { Database } = runtimeRequire("bun:sqlite") as { Database: CredentialDatabaseConstructor }; + const { Database } = runtimeRequire("bun:sqlite") as { + Database: CredentialDatabaseConstructor; + }; return new Database(path, { readonly: true }); } - const { DatabaseSync } = runtimeRequire("node:sqlite") as { DatabaseSync: CredentialDatabaseConstructor }; + const { DatabaseSync } = runtimeRequire("node:sqlite") as { + DatabaseSync: CredentialDatabaseConstructor; + }; return new DatabaseSync(path, { readOnly: true, timeout: 5_000 }); } diff --git a/src/lib/opencode-go-auth.ts b/src/lib/opencode-go-auth.ts index 4ab369bc..908be92f 100644 --- a/src/lib/opencode-go-auth.ts +++ b/src/lib/opencode-go-auth.ts @@ -3,7 +3,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFileCached } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFileCached } from "./opencode-auth.js"; export const DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS = 5_000; // `opencode-go` is the provider id the OpenCode CLI writes via @@ -17,17 +17,17 @@ export type OpenCodeGoKeySource = | "env:OPENCODE_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export type ResolvedOpenCodeGoAuth = InvalidAwareAuthResult; export type OpenCodeGoAuthDiagnostics = InvalidAwareAuthDiagnostics< OpenCodeGoKeySource, - "auth.json" + "opencode.db" >; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; -const openCodeGoAuthResolver = createProviderApiKeyResolver({ +const openCodeGoAuthResolver = createProviderApiKeyResolver({ envVars: [{ name: "OPENCODE_API_KEY", source: "env:OPENCODE_API_KEY" }], providerKeys: OPENCODE_GO_PROVIDER_KEYS, allowedEnvVars: ALLOWED_OPENCODE_GO_ENV_VARS, @@ -37,12 +37,12 @@ const openCodeGoAuthResolver = createProviderApiKeyResolver readAuthFileCached({ maxAgeMs }), - getAuthPaths, + getCredentialDatabasePaths, }, }); diff --git a/src/lib/quota-providers-remote.ts b/src/lib/quota-providers-remote.ts index 9e3743c1..cf862975 100644 --- a/src/lib/quota-providers-remote.ts +++ b/src/lib/quota-providers-remote.ts @@ -5,7 +5,7 @@ import { } from "./api-key-resolver.js"; import { sanitizeSingleLineDisplayText } from "./display-sanitize.js"; import type { AccountingResultType, QuotaToastEntry } from "./entries.js"; -import { getAuthPaths, readAuthFile } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; import type { JsonV1Mapping, JsonV1NumberSource, @@ -40,13 +40,13 @@ const RESULT_TYPES = new Set([ ]); const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/; -export type QuotaProviderAuthSource = "env" | "opencode.json" | "opencode.jsonc" | "auth.json"; +export type QuotaProviderAuthSource = "env" | "opencode.json" | "opencode.jsonc" | "opencode.db"; export interface QuotaProviderAuthResolution { key?: string; source: QuotaProviderAuthSource | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; } export type RemoteQuotaProviderResult = @@ -105,7 +105,7 @@ export async function resolveQuotaProviderApiKey( auth: { readAuth: readAuthFile, authKeys: [source.providerId], - authSource: "auth.json", + authSource: "opencode.db", }, }); @@ -117,7 +117,7 @@ export async function resolveQuotaProviderApiKey( envVarNames: source.apiKeyEnv ? [source.apiKeyEnv] : [], getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, }), - authPaths: getAuthPaths(), + credentialDatabasePaths: getCredentialDatabasePaths(), }; } diff --git a/src/lib/quota-state-codec.ts b/src/lib/quota-state-codec.ts index 8d0832c2..44121a5a 100644 --- a/src/lib/quota-state-codec.ts +++ b/src/lib/quota-state-codec.ts @@ -37,7 +37,7 @@ export function cloneQuotaProviderResult(result: QuotaProviderResult): QuotaProv ...diagnostic, modelIds: diagnostic.modelIds ? [...diagnostic.modelIds] : null, checkedPaths: [...diagnostic.checkedPaths], - authPaths: [...diagnostic.authPaths], + credentialDatabasePaths: [...diagnostic.credentialDatabasePaths], })), } : {}), @@ -321,7 +321,7 @@ function isQuotaProviderDiagnostic(value: unknown): boolean { "httpStatus", "entryCount", "checkedPaths", - "authPaths", + "credentialDatabasePaths", "statePath", "stateHealth", "stateVersion", @@ -344,7 +344,7 @@ function isQuotaProviderDiagnostic(value: unknown): boolean { "explicit_env", "global_opencode_json", "global_opencode_jsonc", - "auth_json", + "opencode_db", ])) && isOneOf(value.outcome, [ "missing_credential", @@ -369,8 +369,8 @@ function isQuotaProviderDiagnostic(value: unknown): boolean { value.entryCount >= 0 && isDenseArray(value.checkedPaths) && value.checkedPaths.every((path) => typeof path === "string") && - isDenseArray(value.authPaths) && - value.authPaths.every((path) => typeof path === "string") && + isDenseArray(value.credentialDatabasePaths) && + value.credentialDatabasePaths.every((path) => typeof path === "string") && (value.statePath === undefined || typeof value.statePath === "string") && (value.stateHealth === undefined || isOneOf(value.stateHealth, ["missing", "healthy", "malformed", "version_mismatch"])) && diff --git a/src/lib/quota-status.ts b/src/lib/quota-status.ts index aafc7646..f1cdd5be 100644 --- a/src/lib/quota-status.ts +++ b/src/lib/quota-status.ts @@ -30,7 +30,7 @@ import { readPricingRefreshState, hasProvider as snapshotHasProvider, } from "./modelsdev-pricing.js"; -import { getAuthPath, getAuthPaths } from "./opencode-auth.js"; +import { getCredentialDatabasePath, getCredentialDatabasePaths } from "./opencode-auth.js"; import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { getOpenCodeDbPath, @@ -85,7 +85,7 @@ const OPENCODE_GO_STATUS_DETAIL_KEYS = new Set([ "auth_state", "auth_source", "auth_checked_paths", - "auth_paths", + "credential_database_paths", "auth_error", "selected_windows", "rolling_usage", @@ -331,8 +331,8 @@ function getQuotaProviderCredentialCategory( case "global_opencode_json": case "global_opencode_jsonc": return "trusted_global_config"; - case "auth_json": - return "auth_json"; + case "opencode_db": + return "opencode_db"; default: return "none"; } @@ -419,7 +419,7 @@ function createQuotaProvidersSection(params: { } const checkedPaths = diagnostic - ? [...new Set([...diagnostic.checkedPaths, ...diagnostic.authPaths])] + ? [...new Set([...diagnostic.checkedPaths, ...diagnostic.credentialDatabasePaths])] : []; rows.push({ key: `provider_${definition.id}`, @@ -813,7 +813,7 @@ export async function buildQuotaStatusReport(params: { key: "opencode_dirs", value: `data=${runtime.dataDir} config=${runtime.configDir} cache=${runtime.cacheDir} state=${runtime.stateDir}`, }); - const authCandidates = getAuthPaths(); + const authCandidates = getCredentialDatabasePaths(); const authPresent: string[] = []; await Promise.all( authCandidates.map(async (p) => { @@ -826,8 +826,8 @@ export async function buildQuotaStatusReport(params: { }), ); pathsRows.push({ - key: "auth.json", - value: `preferred=${getAuthPath()} present=${joinOrNone(authPresent)} candidates=${joinOrNone(authCandidates)}`, + key: "opencode.db", + value: `preferred=${getCredentialDatabasePath()} present=${joinOrNone(authPresent)} candidates=${joinOrNone(authCandidates)}`, }); appendProviderStatusDetailRows( @@ -844,7 +844,7 @@ export async function buildQuotaStatusReport(params: { "alibaba auth configured", "alibaba_api_key_source", "alibaba_api_key_checked_paths", - "alibaba_api_key_auth_paths", + "alibaba_api_key_credential_database_paths", "alibaba_coding_plan", "alibaba_auth_error", ]), diff --git a/src/lib/synthetic-config.ts b/src/lib/synthetic-config.ts index da947c00..29051410 100644 --- a/src/lib/synthetic-config.ts +++ b/src/lib/synthetic-config.ts @@ -2,7 +2,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFile } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; export interface SyntheticApiKeyResult { key: string; @@ -16,7 +16,7 @@ export type SyntheticKeySource = | "env:SYNTHETIC_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; @@ -29,8 +29,8 @@ const syntheticApiKeyResolver = createProviderApiKeyResolver getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { readAuth: readAuthFile, - getAuthPaths, - authSource: "auth.json", + getCredentialDatabasePaths, + authSource: "opencode.db", }, }); @@ -46,7 +46,7 @@ export async function getSyntheticKeyDiagnostics(): Promise<{ configured: boolean; source: SyntheticKeySource | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; }> { return syntheticApiKeyResolver.diagnostics(); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 54871739..a689de85 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -289,7 +289,7 @@ export const DEFAULT_CONFIG: QuotaToastConfig = { }; // ============================================================================= -// Auth Data Types (from ~/.local/share/opencode/auth.json) +// Auth Data Types (from ~/.local/share/opencode/opencode.db) // ============================================================================= /** GitHub Copilot authentication data */ @@ -447,7 +447,7 @@ export interface CopilotQuotaConfig { tier: CopilotTier; } -/** Full auth.json structure (partial - only what we need) */ +/** Full opencode.db structure (partial - only what we need) */ export interface AuthData { anthropic?: AnthropicOAuthAuthData; "github-copilot"?: CopilotAuthData; @@ -551,7 +551,7 @@ export interface GoogleQuotaResponse { // Kimi Types // ============================================================================= -/** Kimi auth entry in auth.json */ +/** Kimi auth entry in opencode.db */ export interface KimiAuthData { type: "api"; key: string; @@ -579,7 +579,7 @@ export type KimiResult = KimiQuotaResult | QuotaError | null; // Z.ai Types // ============================================================================= -/** Z.ai auth entry in auth.json */ +/** Z.ai auth entry in opencode.db */ export interface ZaiAuthData { type: "api"; key: string; diff --git a/src/lib/xai.ts b/src/lib/xai.ts index 690ab4c3..e1f7c54c 100644 --- a/src/lib/xai.ts +++ b/src/lib/xai.ts @@ -8,7 +8,7 @@ * After quota succeeds, subscription metadata may refine the display label via: * GET https://grok.com/rest/subscriptions * - * OpenCode remains the sole owner of OAuth refresh and auth.json persistence. + * OpenCode remains the sole owner of OAuth refresh and opencode.db persistence. */ import { sanitizeSingleLineDisplaySnippet } from "./display-sanitize.js"; diff --git a/src/lib/zai-auth.ts b/src/lib/zai-auth.ts index e467d101..1d9f1184 100644 --- a/src/lib/zai-auth.ts +++ b/src/lib/zai-auth.ts @@ -3,7 +3,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFileCached } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFileCached } from "./opencode-auth.js"; import type { AuthData } from "./types.js"; export const DEFAULT_ZAI_AUTH_CACHE_MAX_AGE_MS = 5_000; @@ -16,14 +16,14 @@ export type ZaiKeySource = | "env:ZAI_CODING_PLAN_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export type ResolvedZaiAuth = InvalidAwareAuthResult; -export type ZaiAuthDiagnostics = InvalidAwareAuthDiagnostics; +export type ZaiAuthDiagnostics = InvalidAwareAuthDiagnostics; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; -const zaiAuthResolver = createProviderApiKeyResolver({ +const zaiAuthResolver = createProviderApiKeyResolver({ envVars: [ { name: "ZAI_API_KEY", source: "env:ZAI_API_KEY" }, { name: "ZAI_CODING_PLAN_API_KEY", source: "env:ZAI_CODING_PLAN_API_KEY" }, @@ -36,11 +36,11 @@ const zaiAuthResolver = createProviderApiKeyResolver( auth: { policy: "invalid-aware-api-key", authKeys: ZAI_AUTH_KEYS, - authSource: "auth.json", + authSource: "opencode.db", displayName: "Z.ai", defaultMaxAgeMs: DEFAULT_ZAI_AUTH_CACHE_MAX_AGE_MS, readAuth: (maxAgeMs) => readAuthFileCached({ maxAgeMs }), - getAuthPaths, + getCredentialDatabasePaths, }, }); diff --git a/src/lib/zhipu-auth.ts b/src/lib/zhipu-auth.ts index 5ebda661..435860db 100644 --- a/src/lib/zhipu-auth.ts +++ b/src/lib/zhipu-auth.ts @@ -3,7 +3,7 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; -import { getAuthPaths, readAuthFileCached } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFileCached } from "./opencode-auth.js"; import type { AuthData } from "./types.js"; export const DEFAULT_ZHIPU_AUTH_CACHE_MAX_AGE_MS = 5_000; @@ -21,14 +21,14 @@ export type ZhipuKeySource = | "env:ZHIPU_CODING_PLAN_API_KEY" | "opencode.json" | "opencode.jsonc" - | "auth.json"; + | "opencode.db"; export type ResolvedZhipuAuth = InvalidAwareAuthResult; -export type ZhipuAuthDiagnostics = InvalidAwareAuthDiagnostics; +export type ZhipuAuthDiagnostics = InvalidAwareAuthDiagnostics; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; -const zhipuAuthResolver = createProviderApiKeyResolver({ +const zhipuAuthResolver = createProviderApiKeyResolver({ envVars: [ { name: "ZHIPU_API_KEY", source: "env:ZHIPU_API_KEY" }, { name: "ZHIPU_CODING_PLAN_API_KEY", source: "env:ZHIPU_CODING_PLAN_API_KEY" }, @@ -41,11 +41,11 @@ const zhipuAuthResolver = createProviderApiKeyResolver readAuthFileCached({ maxAgeMs }), - getAuthPaths, + getCredentialDatabasePaths, }, }); diff --git a/src/providers/alibaba-coding-plan.ts b/src/providers/alibaba-coding-plan.ts index 55dc54d3..f91f15a0 100644 --- a/src/providers/alibaba-coding-plan.ts +++ b/src/providers/alibaba-coding-plan.ts @@ -66,7 +66,8 @@ export const alibabaCodingPlanProvider: QuotaProvider = { "alibaba auth configured": diagnostics.state === "none" ? "false" : "true", alibaba_api_key_source: diagnostics.source ?? "(none)", alibaba_api_key_checked_paths: diagnostics.checkedPaths.join(" | ") || "(none)", - alibaba_api_key_auth_paths: diagnostics.authPaths.join(" | ") || "(none)", + alibaba_api_key_credential_database_paths: + diagnostics.credentialDatabasePaths.join(" | ") || "(none)", alibaba_coding_plan: diagnostics.state === "configured" ? diagnostics.tier diff --git a/src/providers/deepseek.ts b/src/providers/deepseek.ts index 2a57d5fc..df27cc67 100644 --- a/src/providers/deepseek.ts +++ b/src/providers/deepseek.ts @@ -136,7 +136,7 @@ export const deepseekProvider: QuotaProvider = { configured: false, source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })); const result = await queryDeepSeekBalance({ requestTimeoutMs: ctx.config?.requestTimeoutMs }); const providerResult = mapNullableProviderResult(result, { diff --git a/src/providers/glm-coding-plan-provider.ts b/src/providers/glm-coding-plan-provider.ts index 73fb4387..b802765a 100644 --- a/src/providers/glm-coding-plan-provider.ts +++ b/src/providers/glm-coding-plan-provider.ts @@ -35,7 +35,7 @@ export function createGlmCodingPlanProvider(params: { state: string; source: string | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; error?: string; }>; queryQuota: (params: { diff --git a/src/providers/kilo.ts b/src/providers/kilo.ts index 7e4fb978..e3c91be0 100644 --- a/src/providers/kilo.ts +++ b/src/providers/kilo.ts @@ -158,7 +158,7 @@ export const kiloProvider: QuotaProvider = { configured: false, source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })); const stateResult = await queryKiloQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs, diff --git a/src/providers/nanogpt.ts b/src/providers/nanogpt.ts index 2757862d..6fbd091e 100644 --- a/src/providers/nanogpt.ts +++ b/src/providers/nanogpt.ts @@ -187,7 +187,7 @@ export const nanoGptProvider: QuotaProvider = { configured: false, source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })); const result = await queryNanoGptQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs }); const providerResult = mapNullableProviderResult(result, { diff --git a/src/providers/ollama-cloud.ts b/src/providers/ollama-cloud.ts index 29e4623b..ea5c1a54 100644 --- a/src/providers/ollama-cloud.ts +++ b/src/providers/ollama-cloud.ts @@ -120,7 +120,7 @@ export const ollamaCloudProvider: QuotaProvider = { configured: false, source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })); const result = await queryOllamaCloudQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs, diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index d4bc398b..ada58fc8 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -35,7 +35,7 @@ function authStatusDetails(diagnostics: OpenCodeGoAuthDiagnostics): QuotaProvide auth_state: diagnostics.state, auth_source: diagnostics.source ?? "(none)", auth_checked_paths: diagnostics.checkedPaths.join(" | ") || "(none)", - auth_paths: diagnostics.authPaths.join(" | ") || "(none)", + credential_database_paths: diagnostics.credentialDatabasePaths.join(" | ") || "(none)", auth_error: diagnostics.state === "invalid" ? diagnostics.error : undefined, }); } diff --git a/src/providers/quota-providers.ts b/src/providers/quota-providers.ts index e6f7f8cb..68b68d5f 100644 --- a/src/providers/quota-providers.ts +++ b/src/providers/quota-providers.ts @@ -76,7 +76,7 @@ function buildDiagnosticIdentity( } function mapCredentialSource( - source: "env" | "opencode.json" | "opencode.jsonc" | "auth.json" | null, + source: "env" | "opencode.json" | "opencode.jsonc" | "opencode.db" | null, ): QuotaProviderDiagnostic["credentialSource"] { switch (source) { case "env": @@ -85,8 +85,8 @@ function mapCredentialSource( return "global_opencode_json"; case "opencode.jsonc": return "global_opencode_jsonc"; - case "auth.json": - return "auth_json"; + case "opencode.db": + return "opencode_db"; default: return null; } @@ -122,7 +122,7 @@ async function executeRemote( outcome: "missing_credential", entryCount: 0, checkedPaths: [...auth.checkedPaths], - authPaths: [...auth.authPaths], + credentialDatabasePaths: [...auth.credentialDatabasePaths], }, }; } @@ -139,7 +139,7 @@ async function executeRemote( ...classifyRuntimeError(result.error), entryCount: 0, checkedPaths: [...auth.checkedPaths], - authPaths: [...auth.authPaths], + credentialDatabasePaths: [...auth.credentialDatabasePaths], }, }; } @@ -160,7 +160,7 @@ async function executeRemote( outcome: "success", entryCount: result.entries.length, checkedPaths: [...auth.checkedPaths], - authPaths: [...auth.authPaths], + credentialDatabasePaths: [...auth.credentialDatabasePaths], }, }; } @@ -203,7 +203,7 @@ async function executeRemoteWithCache( outcome: "network_error", entryCount: 0, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }, }; } @@ -229,7 +229,7 @@ async function executeDefinition( outcome: "success", entryCount: result.entries.length, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], statePath: state.path, stateHealth: state.health, stateVersion: state.version, @@ -248,7 +248,7 @@ async function executeDefinition( outcome: "local_state_error", entryCount: 0, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], statePath: state.path, stateHealth: state.health, stateVersion: state.version, @@ -330,7 +330,7 @@ export const quotaProvidersProvider: QuotaProvider = { outcome: "network_error", entryCount: 0, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }, } satisfies InstanceResult; } diff --git a/src/providers/result-helpers.ts b/src/providers/result-helpers.ts index bb59771c..f89743c4 100644 --- a/src/providers/result-helpers.ts +++ b/src/providers/result-helpers.ts @@ -59,13 +59,13 @@ export function simpleApiKeyStatusDetails(diagnostics: { configured: boolean; source: string | null; checkedPaths: readonly string[]; - authPaths: readonly string[]; + credentialDatabasePaths: readonly string[]; }): QuotaProviderStatusDetail[] { return statusDetailsFromRecord({ api_key_configured: diagnostics.configured ? "true" : "false", api_key_source: diagnostics.source ?? "(none)", api_key_checked_paths: diagnostics.checkedPaths.join(" | ") || "(none)", - api_key_auth_paths: diagnostics.authPaths.join(" | ") || "(none)", + api_key_credential_database_paths: diagnostics.credentialDatabasePaths.join(" | ") || "(none)", }); } @@ -73,7 +73,7 @@ export function apiKeyStatusDetails(diagnostics: { state: string; source: string | null; checkedPaths: readonly string[]; - authPaths: readonly string[]; + credentialDatabasePaths: readonly string[]; error?: string; }): QuotaProviderStatusDetail[] { return statusDetailsFromRecord({ @@ -81,7 +81,7 @@ export function apiKeyStatusDetails(diagnostics: { api_key_configured: diagnostics.state === "configured" ? "true" : "false", api_key_source: diagnostics.source ?? "(none)", api_key_checked_paths: diagnostics.checkedPaths.join(" | ") || "(none)", - api_key_auth_paths: diagnostics.authPaths.join(" | ") || "(none)", + api_key_credential_database_paths: diagnostics.credentialDatabasePaths.join(" | ") || "(none)", auth_error: diagnostics.error ? sanitizeDisplayText(diagnostics.error) : undefined, }); } diff --git a/tests/helpers/provider-api-key-contract.ts b/tests/helpers/provider-api-key-contract.ts index 6accaff4..8f70483c 100644 --- a/tests/helpers/provider-api-key-contract.ts +++ b/tests/helpers/provider-api-key-contract.ts @@ -14,7 +14,7 @@ export interface SimpleApiKeyContractModule { configured: boolean; source: string | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; }>; getConfigCandidates: () => Array<{ path: string; isJsonc: boolean }>; } @@ -26,7 +26,7 @@ export interface InvalidAwareApiKeyContractModule { state: "none" | "configured" | "invalid"; source: string | null; checkedPaths: string[]; - authPaths: string[]; + credentialDatabasePaths: string[]; error?: string; }>; getConfigCandidates: () => Array<{ path: string; isJsonc: boolean }>; diff --git a/tests/helpers/trusted-config-test-harness.ts b/tests/helpers/trusted-config-test-harness.ts index 396b4abc..e2555f77 100644 --- a/tests/helpers/trusted-config-test-harness.ts +++ b/tests/helpers/trusted-config-test-harness.ts @@ -36,7 +36,7 @@ export const TRUSTED_CONFIG_ENV_KEYS = [ ] as const; export function getTrustedAuthPath(): string { - return join(homedir(), ".local", "share", "opencode", "auth.json"); + return join(homedir(), ".local", "share", "opencode", "opencode.db"); } export function getTrustedOpencodeConfigPaths() { diff --git a/tests/lib.alibaba-auth.test.ts b/tests/lib.alibaba-auth.test.ts index 8fa50dcf..905f158c 100644 --- a/tests/lib.alibaba-auth.test.ts +++ b/tests/lib.alibaba-auth.test.ts @@ -12,7 +12,7 @@ import { } from "./helpers/trusted-config-test-harness.js"; const mocks = vi.hoisted(() => ({ - getAuthPaths: vi.fn(() => ["/tmp/auth.json", "/tmp/auth-fallback.json"]), + getCredentialDatabasePaths: vi.fn(() => ["/tmp/opencode.db", "/tmp/auth-fallback.json"]), readAuthFileCached: vi.fn(), })); @@ -27,7 +27,7 @@ vi.mock("fs/promises", () => ({ })); vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPaths: mocks.getAuthPaths, + getCredentialDatabasePaths: mocks.getCredentialDatabasePaths, readAuthFileCached: mocks.readAuthFileCached, })); @@ -54,7 +54,9 @@ describe("alibaba auth resolution", () => { vi.clearAllMocks(); resetProcessEnv(originalEnv, ["ALIBABA_CODING_PLAN_API_KEY", "ALIBABA_API_KEY"]); - mocks.getAuthPaths.mockReset().mockReturnValue(["/tmp/auth.json", "/tmp/auth-fallback.json"]); + mocks.getCredentialDatabasePaths + .mockReset() + .mockReturnValue(["/tmp/opencode.db", "/tmp/auth-fallback.json"]); mocks.readAuthFileCached.mockReset(); fsConfigMocks = await loadFsConfigMocks(); @@ -188,7 +190,7 @@ describe("alibaba auth resolution", () => { }); describe("resolveAlibabaCodingPlanAuthCached", () => { - it("prefers ALIBABA_CODING_PLAN_API_KEY over auth.json and uses the fallback tier", async () => { + it("prefers ALIBABA_CODING_PLAN_API_KEY over opencode.db and uses the fallback tier", async () => { process.env.ALIBABA_CODING_PLAN_API_KEY = "env-key"; mocks.readAuthFileCached.mockResolvedValueOnce({ alibaba: { type: "api", key: "auth-key", tier: "max" }, @@ -259,7 +261,7 @@ describe("alibaba auth resolution", () => { await expect(resolveAlibabaCodingPlanAuthCached()).resolves.toEqual({ state: "none" }); }); - it("falls back to auth.json when env/config are not configured", async () => { + it("falls back to opencode.db when env/config are not configured", async () => { mocks.readAuthFileCached.mockResolvedValueOnce({ alibaba: { type: "api", key: "dashscope-key", tier: "pro" }, }); @@ -274,7 +276,7 @@ describe("alibaba auth resolution", () => { }); }); - it("returns invalid for access-only cached auth.json", async () => { + it("returns invalid for access-only cached opencode.db", async () => { mocks.readAuthFileCached.mockResolvedValueOnce({ alibaba: { type: "api", access: "dashscope-key", tier: "pro" }, }); @@ -285,7 +287,7 @@ describe("alibaba auth resolution", () => { }); }); - it("surfaces invalid auth.json tiers only when fallback auth wins", async () => { + it("surfaces invalid opencode.db tiers only when fallback auth wins", async () => { mocks.readAuthFileCached.mockResolvedValueOnce({ alibaba: { type: "api", key: "dashscope-key", tier: "max" }, }); @@ -313,7 +315,7 @@ describe("alibaba auth resolution", () => { state: "configured", source: "env:ALIBABA_API_KEY", checkedPaths: ["env:ALIBABA_API_KEY"], - authPaths: ["/tmp/auth.json", "/tmp/auth-fallback.json"], + credentialDatabasePaths: ["/tmp/opencode.db", "/tmp/auth-fallback.json"], tier: "lite", }); }); @@ -326,20 +328,20 @@ describe("alibaba auth resolution", () => { state: "none", source: null, checkedPaths: [trustedPaths.json], - authPaths: ["/tmp/auth.json", "/tmp/auth-fallback.json"], + credentialDatabasePaths: ["/tmp/opencode.db", "/tmp/auth-fallback.json"], }); }); - it("reports invalid auth.json diagnostics when fallback auth is malformed", async () => { + it("reports invalid opencode.db diagnostics when fallback auth is malformed", async () => { mocks.readAuthFileCached.mockResolvedValueOnce({ alibaba: { type: "api", key: "dashscope-key", tier: "max" }, }); await expect(getAlibabaCodingPlanAuthDiagnostics()).resolves.toEqual({ state: "invalid", - source: "auth.json", + source: "opencode.db", checkedPaths: [], - authPaths: ["/tmp/auth.json", "/tmp/auth-fallback.json"], + credentialDatabasePaths: ["/tmp/opencode.db", "/tmp/auth-fallback.json"], error: "Unsupported Alibaba Coding Plan tier: max", rawTier: "max", }); diff --git a/tests/lib.api-key-provider-queries.test.ts b/tests/lib.api-key-provider-queries.test.ts index 02342896..cadbbf16 100644 --- a/tests/lib.api-key-provider-queries.test.ts +++ b/tests/lib.api-key-provider-queries.test.ts @@ -11,7 +11,7 @@ import { querySyntheticQuota } from "../src/lib/synthetic.js"; vi.mock("../src/lib/opencode-auth.js", () => ({ readAuthFile: vi.fn(async () => ({})), - getAuthPaths: vi.fn(() => []), + getCredentialDatabasePaths: vi.fn(() => []), })); vi.mock("../src/lib/nanogpt-config.js", () => ({ diff --git a/tests/lib.api-key-resolver.test.ts b/tests/lib.api-key-resolver.test.ts index 1da71ae6..0dbeacca 100644 --- a/tests/lib.api-key-resolver.test.ts +++ b/tests/lib.api-key-resolver.test.ts @@ -51,7 +51,7 @@ describe("api-key-resolver", () => { vi.restoreAllMocks(); }); - it("keeps environment first, trusted JSONC before JSON, and auth.json last", async () => { + it("keeps environment first, trusted JSONC before JSON, and opencode.db last", async () => { const { existsSync } = await import("fs"); const { readFile } = await import("fs/promises"); const trustedJsoncPath = join(homedir(), ".config", "opencode", "opencode.jsonc"); @@ -68,7 +68,7 @@ describe("api-key-resolver", () => { : null, configJsonSource: "opencode.json" as const, configJsoncSource: "opencode.jsonc" as const, - authSource: "auth.json" as const, + authSource: "opencode.db" as const, }; process.env.TEST_PROVIDER_KEY = "env-key"; @@ -101,7 +101,7 @@ describe("api-key-resolver", () => { (readFile as any).mockReset(); await expect(resolveApiKey(config, readAuth)).resolves.toEqual({ key: "auth-key", - source: "auth.json", + source: "opencode.db", }); expect(readAuth).toHaveBeenCalledOnce(); }); @@ -161,20 +161,20 @@ describe("api-key-resolver", () => { const simpleReadAuth = vi.fn().mockResolvedValue({ provider: { type: "oauth", key: "ignored-key" }, }); - const simple = createProviderApiKeyResolver<"config" | "auth.json">({ + const simple = createProviderApiKeyResolver<"config" | "opencode.db">({ envVars: [], providerKeys: ["provider"], configJsonSource: "config", configJsoncSource: "config", getConfigCandidates: () => [], - auth: { readAuth: simpleReadAuth, authSource: "auth.json" }, + auth: { readAuth: simpleReadAuth, authSource: "opencode.db" }, }); await expect(simple.resolve()).resolves.toBeNull(); const invalidReadAuth = vi.fn().mockResolvedValue({ provider: { type: "oauth", key: "ignored-key" }, }); - const invalidAware = createProviderApiKeyResolver<"config" | "auth.json", "auth.json">({ + const invalidAware = createProviderApiKeyResolver<"config" | "opencode.db", "opencode.db">({ envVars: [], providerKeys: ["provider"], configJsonSource: "config", @@ -183,11 +183,11 @@ describe("api-key-resolver", () => { auth: { policy: "invalid-aware-api-key", authKeys: ["provider"], - authSource: "auth.json", + authSource: "opencode.db", displayName: "Provider", defaultMaxAgeMs: 5_000, readAuth: invalidReadAuth, - getAuthPaths: () => ["/tmp/auth.json"], + getCredentialDatabasePaths: () => ["/tmp/opencode.db"], }, }); diff --git a/tests/lib.glm-coding-plan.test.ts b/tests/lib.glm-coding-plan.test.ts index fbec30b6..488b1214 100644 --- a/tests/lib.glm-coding-plan.test.ts +++ b/tests/lib.glm-coding-plan.test.ts @@ -40,7 +40,7 @@ function configure(provider: ProviderCase): void { provider.resolveAuth.mockResolvedValueOnce({ state: "configured", apiKey: provider.key, - source: "auth.json", + source: "opencode.db", }); } diff --git a/tests/lib.invalid-aware-provider-auth.test.ts b/tests/lib.invalid-aware-provider-auth.test.ts index db35a254..d48342a9 100644 --- a/tests/lib.invalid-aware-provider-auth.test.ts +++ b/tests/lib.invalid-aware-provider-auth.test.ts @@ -20,7 +20,7 @@ import { } from "./helpers/trusted-config-test-harness.js"; const authMocks = vi.hoisted(() => ({ - getAuthPaths: vi.fn(() => ["/tmp/auth.json"]), + getCredentialDatabasePaths: vi.fn(() => ["/tmp/opencode.db"]), readAuthFileCached: vi.fn(), })); @@ -28,7 +28,7 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => createRuntimePathsMockModu vi.mock("fs", () => ({ existsSync: vi.fn() })); vi.mock("fs/promises", () => ({ readFile: vi.fn() })); vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPaths: authMocks.getAuthPaths, + getCredentialDatabasePaths: authMocks.getCredentialDatabasePaths, readAuthFileCached: authMocks.readAuthFileCached, })); @@ -100,7 +100,7 @@ describe("invalid-aware provider auth", () => { function resetFixture(): void { resetContractEnv(originalEnv, providers); resetFsConfigMocks(fsMocks); - authMocks.getAuthPaths.mockReset().mockReturnValue(["/tmp/auth.json"]); + authMocks.getCredentialDatabasePaths.mockReset().mockReturnValue(["/tmp/opencode.db"]); authMocks.readAuthFileCached.mockReset().mockResolvedValue(null); } @@ -299,7 +299,7 @@ describe("invalid-aware provider auth", () => { state: "configured", source: `env:${provider.envVars[1]}`, checkedPaths: [`env:${provider.envVars[1]}`], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); resetFixture(); @@ -309,9 +309,9 @@ describe("invalid-aware provider auth", () => { const invalid = await module.diagnostics(); expect(invalid, `${provider.name} invalid`).toEqual({ state: "invalid", - source: "auth.json", + source: "opencode.db", checkedPaths: [], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], error: `Unsupported ${provider.displayName} auth type: "oauth"`, }); expect(JSON.stringify(invalid), `${provider.name} secret-free`).not.toContain("diag-key"); @@ -321,7 +321,7 @@ describe("invalid-aware provider auth", () => { state: "none", source: null, checkedPaths: [], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); expect(module.getConfigCandidates(), provider.name).toEqual( expectedTrustedConfigCandidates(), diff --git a/tests/lib.minimax-auth.test.ts b/tests/lib.minimax-auth.test.ts index 17ccb8e4..a510b80f 100644 --- a/tests/lib.minimax-auth.test.ts +++ b/tests/lib.minimax-auth.test.ts @@ -12,7 +12,7 @@ import { } from "./helpers/trusted-config-test-harness.js"; const mocks = vi.hoisted(() => ({ - getAuthPaths: vi.fn(() => ["/tmp/auth.json"]), + getCredentialDatabasePaths: vi.fn(() => ["/tmp/opencode.db"]), readAuthFileCached: vi.fn(), })); @@ -27,7 +27,7 @@ vi.mock("fs/promises", () => ({ })); vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPaths: mocks.getAuthPaths, + getCredentialDatabasePaths: mocks.getCredentialDatabasePaths, readAuthFileCached: mocks.readAuthFileCached, })); @@ -68,7 +68,7 @@ describe("minimax auth resolution", () => { "MINIMAX_API_KEY", ]); - mocks.getAuthPaths.mockReset().mockReturnValue(["/tmp/auth.json"]); + mocks.getCredentialDatabasePaths.mockReset().mockReturnValue(["/tmp/opencode.db"]); mocks.readAuthFileCached.mockReset(); fsConfigMocks = await loadFsConfigMocks(); @@ -130,7 +130,7 @@ describe("minimax auth resolution", () => { }); describe("resolveMiniMaxAuthCached", () => { - it("prefers MINIMAX_CODING_PLAN_API_KEY over MINIMAX_API_KEY and auth.json", async () => { + it("prefers MINIMAX_CODING_PLAN_API_KEY over MINIMAX_API_KEY and opencode.db", async () => { process.env.MINIMAX_CODING_PLAN_API_KEY = "primary-env-key"; process.env.MINIMAX_API_KEY = "fallback-env-key"; mocks.readAuthFileCached.mockResolvedValueOnce( @@ -224,7 +224,7 @@ describe("minimax auth resolution", () => { await expect(resolveMiniMaxAuthCached()).resolves.toEqual({ state: "none" }); }); - it("falls back to auth.json with strict api key auth", async () => { + it("falls back to opencode.db with strict api key auth", async () => { mocks.readAuthFileCached.mockResolvedValueOnce( withMiniMaxAuth({ type: "api", key: "auth-key" }), ); @@ -239,7 +239,7 @@ describe("minimax auth resolution", () => { }); }); - it("returns invalid for access-only auth.json", async () => { + it("returns invalid for access-only opencode.db", async () => { mocks.readAuthFileCached.mockResolvedValueOnce( withMiniMaxAuth({ type: "api", access: "access-token" }), ); @@ -250,7 +250,7 @@ describe("minimax auth resolution", () => { }); }); - it("masks invalid auth.json when trusted config is configured", async () => { + it("masks invalid opencode.db when trusted config is configured", async () => { mockTrustedConfigFile( fsConfigMocks, trustedPaths.json, @@ -285,7 +285,7 @@ describe("minimax auth resolution", () => { }); describe("resolveMiniMaxChinaAuth", () => { - it("resolves MiniMax China auth.json keys", () => { + it("resolves MiniMax China opencode.db keys", () => { expect( resolveMiniMaxChinaAuth(withMiniMaxChinaAuth({ type: "api", key: "china-key" })), ).toEqual({ @@ -295,7 +295,7 @@ describe("minimax auth resolution", () => { }); }); - it("returns invalid for MiniMax China access-only auth.json", () => { + it("returns invalid for MiniMax China access-only opencode.db", () => { expect( resolveMiniMaxChinaAuth(withMiniMaxChinaAuth({ type: "api", access: "china-token" })), ).toEqual({ @@ -314,7 +314,7 @@ describe("minimax auth resolution", () => { source: "env:MINIMAX_API_KEY", endpoint: "international", checkedPaths: ["env:MINIMAX_API_KEY"], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); }); @@ -326,20 +326,20 @@ describe("minimax auth resolution", () => { source: "env:MINIMAX_CHINA_CODING_PLAN_API_KEY", endpoint: "china", checkedPaths: ["env:MINIMAX_CHINA_CODING_PLAN_API_KEY"], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); }); - it("reports invalid auth.json diagnostics when fallback auth is malformed", async () => { + it("reports invalid opencode.db diagnostics when fallback auth is malformed", async () => { mocks.readAuthFileCached.mockResolvedValueOnce( withMiniMaxAuth({ type: "oauth", key: "some-key" }), ); await expect(getMiniMaxAuthDiagnostics()).resolves.toEqual({ state: "invalid", - source: "auth.json", + source: "opencode.db", checkedPaths: [], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], error: 'Unsupported MiniMax auth type: "oauth"', }); }); diff --git a/tests/lib.ollama-cloud-config.test.ts b/tests/lib.ollama-cloud-config.test.ts index abd43ff6..3bb5060a 100644 --- a/tests/lib.ollama-cloud-config.test.ts +++ b/tests/lib.ollama-cloud-config.test.ts @@ -9,7 +9,7 @@ const mocks = vi.hoisted(() => ({ { path: "/trusted/opencode.jsonc", isJsonc: true }, { path: "/trusted/opencode.json", isJsonc: false }, ]), - getAuthPaths: vi.fn(() => ["/trusted/auth.json"]), + getCredentialDatabasePaths: vi.fn(() => ["/trusted/opencode.db"]), readAuthFile: vi.fn(), })); @@ -26,7 +26,7 @@ vi.mock("../src/lib/api-key-resolver.js", () => ({ })); vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPaths: mocks.getAuthPaths, + getCredentialDatabasePaths: mocks.getCredentialDatabasePaths, readAuthFile: mocks.readAuthFile, })); @@ -52,8 +52,8 @@ describe("ollama-cloud API key config", () => { getConfigCandidates: mocks.getGlobalOpencodeConfigCandidatePaths, auth: { readAuth: mocks.readAuthFile, - getAuthPaths: mocks.getAuthPaths, - authSource: "auth.json", + getCredentialDatabasePaths: mocks.getCredentialDatabasePaths, + authSource: "opencode.db", }, }); expect(JSON.stringify(mocks.config)).not.toContain("OLLAMA_USAGE_COOKIE"); @@ -70,7 +70,7 @@ describe("ollama-cloud API key config", () => { configured: true, source: "env:OLLAMA_API_KEY" as const, checkedPaths: ["env:OLLAMA_API_KEY"], - authPaths: ["/trusted/auth.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], }; mocks.resolve.mockResolvedValueOnce(resolved); mocks.has.mockResolvedValueOnce(true); diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 3e23c0fb..6893609a 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -1,17 +1,26 @@ -import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { clearReadAuthFileCacheForTests, readAuthFile } from "../src/lib/opencode-auth.js"; +import { + clearReadAuthFileCacheForTests, + getCredentialDatabasePath, + getCredentialDatabasePaths, + readAuthFile, +} from "../src/lib/opencode-auth.js"; const temporaryDirectories: string[] = []; afterEach(async () => { clearReadAuthFileCacheForTests(); vi.unstubAllEnvs(); - await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); }); async function createCredentialDatabase(): Promise<{ dataDir: string; databasePath: string }> { @@ -37,7 +46,13 @@ async function createCredentialDatabase(): Promise<{ dataDir: string; databasePa .run( "copilot", "github-copilot", - JSON.stringify({ type: "oauth", access: "copilot-access", refresh: "copilot-refresh", expires: 10, metadata: { enterpriseUrl: "example.ghe.com" } }), + JSON.stringify({ + type: "oauth", + access: "copilot-access", + refresh: "copilot-refresh", + expires: 10, + metadata: { enterpriseUrl: "example.ghe.com" }, + }), 1, ); database @@ -45,7 +60,12 @@ async function createCredentialDatabase(): Promise<{ dataDir: string; databasePa .run( "openai", "openai", - JSON.stringify({ type: "oauth", access: "openai-access", refresh: "openai-refresh", expires: 20 }), + JSON.stringify({ + type: "oauth", + access: "openai-access", + refresh: "openai-refresh", + expires: 20, + }), 2, ); database @@ -56,6 +76,17 @@ async function createCredentialDatabase(): Promise<{ dataDir: string; databasePa } describe("OpenCode auth reader", () => { + it("reports the credential database path and honors OPENCODE_DB", async () => { + const { dataDir } = await createCredentialDatabase(); + vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); + + expect(getCredentialDatabasePaths()).toContain(join(dataDir, "opencode.db")); + expect(getCredentialDatabasePath()).toBe(join(dataDir, "opencode.db")); + + vi.stubEnv("OPENCODE_DB", "custom.db"); + expect(getCredentialDatabasePaths()).toEqual([join(dataDir, "custom.db")]); + }); + it("falls back to OAuth credentials stored in OpenCode's database", async () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); diff --git a/tests/lib.opencode-go-auth.test.ts b/tests/lib.opencode-go-auth.test.ts index f07bc74d..ba620293 100644 --- a/tests/lib.opencode-go-auth.test.ts +++ b/tests/lib.opencode-go-auth.test.ts @@ -12,7 +12,7 @@ import { } from "./helpers/trusted-config-test-harness.js"; const authMocks = vi.hoisted(() => ({ - getAuthPaths: vi.fn(() => ["/tmp/auth.json"]), + getCredentialDatabasePaths: vi.fn(() => ["/tmp/opencode.db"]), readAuthFileCached: vi.fn(), queryOpenCodeGoQuota: vi.fn(), })); @@ -21,7 +21,7 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => createRuntimePathsMockModu vi.mock("fs", () => ({ existsSync: vi.fn() })); vi.mock("fs/promises", () => ({ readFile: vi.fn() })); vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPaths: authMocks.getAuthPaths, + getCredentialDatabasePaths: authMocks.getCredentialDatabasePaths, readAuthFileCached: authMocks.readAuthFileCached, })); vi.mock("../src/lib/opencode-go.js", () => ({ @@ -58,7 +58,7 @@ function resetFixture(): void { delete process.env[name]; } resetFsConfigMocks(fsMocks); - authMocks.getAuthPaths.mockReset().mockReturnValue(["/tmp/auth.json"]); + authMocks.getCredentialDatabasePaths.mockReset().mockReturnValue(["/tmp/opencode.db"]); authMocks.readAuthFileCached.mockReset().mockResolvedValue(null); } @@ -108,7 +108,7 @@ describe("OpenCode Go auth resolution", () => { } }); - it("accepts the opencode-go auth.json key written by `opencode auth login -p opencode-go`", () => { + it("accepts the opencode-go opencode.db key written by `opencode auth login -p opencode-go`", () => { expect(resolveOpenCodeGoAuth({ "opencode-go": { type: "api", key: "cli-key" } })).toEqual({ state: "configured", apiKey: "cli-key", @@ -120,7 +120,7 @@ describe("OpenCode Go auth resolution", () => { }); }); - it("prefers the opencode-go auth.json key over the legacy opencode fallback", () => { + it("prefers the opencode-go opencode.db key over the legacy opencode fallback", () => { const auth = { "opencode-go": { type: "api", key: "cli-key" }, opencode: { type: "api", key: "legacy-key" }, @@ -175,7 +175,7 @@ describe("OpenCode Go auth resolution", () => { await expect(resolveOpenCodeGoAuthCached()).resolves.toEqual({ state: "none" }); }); - it("continues past blank env and unusable config to canonical auth.json", async () => { + it("continues past blank env and unusable config to canonical opencode.db", async () => { process.env.OPENCODE_API_KEY = " "; mockTrustedConfigFile( fsMocks, @@ -209,9 +209,9 @@ describe("OpenCode Go auth resolution", () => { const diagnostics = await getOpenCodeGoAuthDiagnostics({ maxAgeMs: -1 }); expect(diagnostics).toEqual({ state: "invalid", - source: "auth.json", + source: "opencode.db", checkedPaths: expect.any(Array), - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], error: "OpenCode Go auth entry has unsupported type", }); expect(JSON.stringify(diagnostics)).not.toContain(secret); @@ -234,7 +234,7 @@ describe("OpenCode Go auth resolution", () => { state: "configured", source: "env:OPENCODE_API_KEY", checkedPaths: ["env:OPENCODE_API_KEY"], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); expect(getOpencodeConfigCandidatePaths()).toEqual([ { path: trustedPaths.jsonc, isJsonc: true }, diff --git a/tests/lib.openrouter.test.ts b/tests/lib.openrouter.test.ts index 6e35a452..04a2b402 100644 --- a/tests/lib.openrouter.test.ts +++ b/tests/lib.openrouter.test.ts @@ -23,9 +23,9 @@ describe("OpenRouter quota", () => { it("uses the standard OpenRouter credential sources and key endpoint", async () => { vi.mocked(resolveQuotaProviderApiKey).mockResolvedValueOnce({ key: "secret", - source: "auth.json", + source: "opencode.db", checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }); vi.mocked(fetchRemoteQuotaProvider).mockResolvedValueOnce({ success: true, @@ -58,7 +58,7 @@ describe("OpenRouter quota", () => { vi.mocked(resolveQuotaProviderApiKey).mockResolvedValue({ source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }); await expect(queryOpenRouterQuota()).resolves.toBeNull(); @@ -66,7 +66,7 @@ describe("OpenRouter quota", () => { await expect(resolveOpenRouterApiKey()).resolves.toEqual({ source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }); expect(fetchRemoteQuotaProvider).not.toHaveBeenCalled(); }); @@ -76,7 +76,7 @@ describe("OpenRouter quota", () => { key: "secret", source: "env", checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }); vi.mocked(fetchRemoteQuotaProvider).mockResolvedValue({ success: true, @@ -109,7 +109,7 @@ describe("OpenRouter quota", () => { key: "SUPER_SECRET", source: "env", checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }); vi.mocked(fetchRemoteQuotaProvider).mockResolvedValue({ success: false, diff --git a/tests/lib.provider-api-key-configs.test.ts b/tests/lib.provider-api-key-configs.test.ts index 20a4a907..0017f804 100644 --- a/tests/lib.provider-api-key-configs.test.ts +++ b/tests/lib.provider-api-key-configs.test.ts @@ -27,7 +27,7 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => createRuntimePathsMockModu vi.mock("fs", () => ({ existsSync: vi.fn() })); vi.mock("fs/promises", () => ({ readFile: vi.fn() })); vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPaths: () => ["/tmp/auth.json"], + getCredentialDatabasePaths: () => ["/tmp/opencode.db"], readAuthFile: authMocks.readAuthFile, })); @@ -161,7 +161,7 @@ describe("simple provider API key configs", () => { configured: true, source: `env:${provider.envVars[0]}`, checkedPaths: [`env:${provider.envVars[0]}`], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); } }); @@ -183,7 +183,7 @@ describe("simple provider API key configs", () => { } }); - it("keeps environment before config before strict auth.json", async () => { + it("keeps environment before config before strict opencode.db", async () => { for (const provider of providers) { const module = await provider.load(); @@ -224,7 +224,7 @@ describe("simple provider API key configs", () => { ); await expect(module.resolve(), provider.name).resolves.toEqual({ key: "auth-key", - source: "auth.json", + source: "opencode.db", }); authMocks.readAuthFile.mockResolvedValue( @@ -264,7 +264,7 @@ describe("simple provider API key configs", () => { configured: false, source: null, checkedPaths: [trustedPaths.json], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); expect(module.getConfigCandidates(), provider.name).toEqual( expectedTrustedConfigCandidates(), @@ -297,7 +297,7 @@ describe("simple provider API key configs", () => { ); await expect(module.resolve()).resolves.toEqual({ key: "alias-key", - source: "auth.json", + source: "opencode.db", }); }); }); diff --git a/tests/lib.quota-export.test.ts b/tests/lib.quota-export.test.ts index c64fdb53..cb810500 100644 --- a/tests/lib.quota-export.test.ts +++ b/tests/lib.quota-export.test.ts @@ -526,7 +526,7 @@ describe("buildQuotaExport", () => { outcome: "success", entryCount: 1, checkedPaths: ["env:GATEWAY_ONE_KEY"], - authPaths: ["/trusted/auth.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], }, { sourceId: "same-label-two", @@ -536,11 +536,11 @@ describe("buildQuotaExport", () => { apiKeyEnv: null, selected: true, attempted: true, - credentialSource: "auth_json", + credentialSource: "opencode_db", outcome: "invalid_json", entryCount: 0, checkedPaths: ["/trusted/opencode.json"], - authPaths: ["/trusted/auth.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], }, ], }, @@ -583,7 +583,7 @@ describe("buildQuotaExport", () => { expect(json).not.toContain("GATEWAY_ONE_KEY"); expect(json).not.toContain("private-adapter-literal"); expect(json).not.toContain("json-v1"); - expect(json).not.toContain("/trusted/auth.json"); + expect(json).not.toContain("/trusted/opencode.db"); }); it("returns ordered unavailable quota-provider definitions when the cache has no aggregate entry", async () => { diff --git a/tests/lib.quota-providers-auth.test.ts b/tests/lib.quota-providers-auth.test.ts index d2b303da..e52f84a9 100644 --- a/tests/lib.quota-providers-auth.test.ts +++ b/tests/lib.quota-providers-auth.test.ts @@ -42,7 +42,7 @@ describe("quota provider trusted auth binding", () => { const originalEnv = { ...process.env }; const trustedJson = join(homedir(), ".config", "opencode", "opencode.json"); const trustedJsonc = join(homedir(), ".config", "opencode", "opencode.jsonc"); - const authJson = join(homedir(), ".local", "share", "opencode", "auth.json"); + const authJson = join(homedir(), ".local", "share", "opencode", "opencode.db"); const workspaceJson = join(process.cwd(), "opencode.json"); beforeEach(async () => { @@ -59,7 +59,7 @@ describe("quota provider trusted auth binding", () => { process.env = { ...originalEnv }; }); - it("uses explicit environment auth before trusted config and strict auth.json", async () => { + it("uses explicit environment auth before trusted config and strict opencode.db", async () => { process.env.EXPLICIT_KEY = "env-secret"; const { existsSync } = await import("fs"); const { readFile } = await import("fs/promises"); @@ -130,7 +130,7 @@ describe("quota provider trusted auth binding", () => { expect(JSON.stringify(disallowed)).not.toContain("other-secret"); }); - it("falls back only to strict API-key-shaped auth.json for exact providerId", async () => { + it("ignores legacy opencode.db entries", async () => { const { readFile } = await import("fs/promises"); vi.mocked(readFile).mockImplementation(async (path) => { if (path === authJson) { @@ -143,19 +143,8 @@ describe("quota provider trusted auth binding", () => { }); const result = await resolveQuotaProviderApiKey(source()); - expect(result.key).toBe("auth-secret"); - expect(result.source).toBe("auth.json"); - - vi.mocked(readFile).mockImplementation(async (path) => { - if (path === authJson) { - return JSON.stringify({ - "provider-one": { type: "oauth", key: "oauth-secret" }, - }); - } - throw new Error("missing"); - }); - const oauth = await resolveQuotaProviderApiKey(source()); - expect(oauth.key).toBeUndefined(); + expect(result.key).toBeUndefined(); + expect(result.source).toBeNull(); }); it("never reads workspace config, SDK config, aliases, or derived env names", async () => { diff --git a/tests/lib.quota-state-codec.test.ts b/tests/lib.quota-state-codec.test.ts index 422e99fc..df566db3 100644 --- a/tests/lib.quota-state-codec.test.ts +++ b/tests/lib.quota-state-codec.test.ts @@ -104,7 +104,7 @@ function createValidResult(): QuotaProviderResult { httpStatus: 200, entryCount: 4, checkedPaths: ["env:SYNTHETIC_API_KEY"], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], statePath: "/tmp/state.json", stateHealth: "healthy", stateVersion: 2, @@ -215,7 +215,7 @@ describe("quota-state codec", () => { outcome: "local_state_error", entryCount: 0, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], stateHealth: "malformed", stateVersion: null, stateLastUpdatedAt: null, @@ -393,7 +393,9 @@ describe("quota-state codec", () => { expect((cloned.entries[0] as any).basis.limit.quantity).not.toBe( (input.entries[0] as any).basis.limit.quantity, ); - expect(cloned.diagnostics?.[0]?.authPaths).not.toBe(input.diagnostics?.[0]?.authPaths); + expect(cloned.diagnostics?.[0]?.credentialDatabasePaths).not.toBe( + input.diagnostics?.[0]?.credentialDatabasePaths, + ); }); it("encodes V2 and round-trips parsed envelopes by semantic value", () => { diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index 172e2653..b419be5f 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -46,8 +46,8 @@ vi.mock("fs/promises", () => ({ })); vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPath: () => "/tmp/auth.json", - getAuthPaths: () => ["/tmp/auth.json"], + getCredentialDatabasePath: () => "/tmp/opencode.db", + getCredentialDatabasePaths: () => ["/tmp/opencode.db"], readAuthFileCached: vi.fn(async () => ({})), })); @@ -321,7 +321,7 @@ describe("buildQuotaStatusReport", () => { httpStatus: 401, entryCount: 0, checkedPaths: ["env:INTERNAL_GATEWAY_KEY", "/trusted/opencode.jsonc"], - authPaths: ["/trusted/auth.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], }, ], }, @@ -338,7 +338,7 @@ describe("buildQuotaStatusReport", () => { expect(section).toContain("outcome=http_error"); expect(section).toContain("credential_category=trusted_global_config"); expect(section).toContain("env_name=INTERNAL_GATEWAY_KEY"); - expect(section).toContain("/trusted/opencode.jsonc | /trusted/auth.json"); + expect(section).toContain("/trusted/opencode.jsonc | /trusted/opencode.db"); expect(section).toContain("provider_second-source:"); expect(section).toContain("outcome=unavailable"); expect(section).not.toContain("private.example"); @@ -591,7 +591,7 @@ describe("buildQuotaStatusReport", () => { expect(report).toContain("- quota_plugin_configured: true"); expect(report).toContain("- quota_plugin_paths: /tmp/project/tui.jsonc"); expect(report).toContain( - "- auth.json: preferred=/tmp/auth.json present=(none) candidates=/tmp/auth.json", + "- opencode.db: preferred=/tmp/opencode.db present=(none) candidates=/tmp/opencode.db", ); expect(report).toContain( "- pricing: source=test active_source=bundled generated_at=2026-01-01T00:00:00.000Z units=usd_per_1m_tokens", @@ -921,7 +921,7 @@ describe("buildQuotaStatusReport", () => { makeProviderSuccessProbe("nanogpt", { api_key_configured: "true", api_key_source: "env:NANOGPT_API_KEY", - api_key_auth_paths: "/tmp/auth.json", + api_key_credential_database_paths: "/tmp/opencode.db", subscription_active: "false", subscription_state: "grace", enforce_daily_limit: "true", @@ -941,7 +941,7 @@ describe("buildQuotaStatusReport", () => { expect(report).toContain("nanogpt:"); expect(report).toContain("- api_key_configured: true"); expect(report).toContain("- api_key_source: env:NANOGPT_API_KEY"); - expect(report).toContain("- api_key_auth_paths: /tmp/auth.json"); + expect(report).toContain("- api_key_credential_database_paths: /tmp/opencode.db"); expect(report).toContain("- subscription_active: false"); expect(report).toContain("- subscription_state: grace"); expect(report).toContain("- enforce_daily_limit: true"); @@ -1042,7 +1042,7 @@ describe("buildQuotaStatusReport", () => { api_key_configured: "true", api_key_source: "env:DEEPSEEK_API_KEY", api_key_checked_paths: "env:DEEPSEEK_API_KEY", - api_key_auth_paths: "/tmp/auth.json", + api_key_credential_database_paths: "/tmp/opencode.db", }), ], }); @@ -1051,7 +1051,7 @@ describe("buildQuotaStatusReport", () => { expect(report).toContain("- api_key_configured: true"); expect(report).toContain("- api_key_source: env:DEEPSEEK_API_KEY"); expect(report).toContain("- api_key_checked_paths: env:DEEPSEEK_API_KEY"); - expect(report).toContain("- api_key_auth_paths: /tmp/auth.json"); + expect(report).toContain("- api_key_credential_database_paths: /tmp/opencode.db"); expect(report).toContain("- deepseek: pricing=no (account balance only (not token-priced))"); }); @@ -1091,7 +1091,7 @@ describe("buildQuotaStatusReport", () => { auth_state: "configured", auth_source: "env:OPENCODE_API_KEY", auth_checked_paths: "env:OPENCODE_API_KEY | provider.opencode.options.apiKey", - auth_paths: "/tmp/auth.json", + credential_database_paths: "/tmp/opencode.db", selected_windows: "rolling,weekly,monthly", rolling_usage: "status=ok percent_used=7 percent_remaining=93 reset_at=2026-03-12T17:45:00.000Z", @@ -1113,7 +1113,7 @@ describe("buildQuotaStatusReport", () => { expect(section).toContain( "- auth_checked_paths: env:OPENCODE_API_KEY | provider.opencode.options.apiKey", ); - expect(section).toContain("- auth_paths: /tmp/auth.json"); + expect(section).toContain("- credential_database_paths: /tmp/opencode.db"); expect(section).toContain("- selected_windows: rolling,weekly,monthly"); expect(section).toContain( "- rolling_usage: status=ok percent_used=7 percent_remaining=93 reset_at=2026-03-12T17:45:00.000Z", @@ -1139,10 +1139,10 @@ describe("buildQuotaStatusReport", () => { makeProviderProbe("opencode-go", { statusDetails: makeStatusDetails({ auth_state: "invalid", - auth_source: "auth.json", + auth_source: "opencode.db", auth_checked_paths: "env:OPENCODE_API_KEY | provider.opencode.options.apiKey", - auth_paths: "/tmp/auth.json", - auth_error: "auth.json entry opencode must contain a non-empty API key", + credential_database_paths: "/tmp/opencode.db", + auth_error: "opencode.db entry opencode must contain a non-empty API key", selected_windows: "rolling,weekly,monthly", config_state: "invalid", config_error: "legacy secret", @@ -1153,10 +1153,10 @@ describe("buildQuotaStatusReport", () => { const section = getReportSection(report, "opencode_go:"); expect(section).toContain("- auth_state: invalid"); - expect(section).toContain("- auth_source: auth.json"); - expect(section).toContain("- auth_paths: /tmp/auth.json"); + expect(section).toContain("- auth_source: opencode.db"); + expect(section).toContain("- credential_database_paths: /tmp/opencode.db"); expect(section).toContain( - "- auth_error: auth.json entry opencode must contain a non-empty API key", + "- auth_error: opencode.db entry opencode must contain a non-empty API key", ); expect(section).toContain("- selected_windows: rolling,weekly,monthly"); expect(section).not.toContain("config_"); @@ -1171,9 +1171,9 @@ describe("buildQuotaStatusReport", () => { "opencode-go", { auth_state: "configured", - auth_source: "auth.json", + auth_source: "opencode.db", auth_checked_paths: "env:OPENCODE_API_KEY", - auth_paths: "/tmp/auth.json", + credential_database_paths: "/tmp/opencode.db", selected_windows: "rolling,weekly,monthly", live_fetch_error: "OpenCode Go API error 503: unavailable", dashboard_url: "https://opencode.ai/workspace/private", @@ -1320,9 +1320,9 @@ describe("buildQuotaStatusReport", () => { makeProviderSuccessProbe("minimax-coding-plan", { auth_state: "configured", api_key_configured: "true", - api_key_source: "auth.json", + api_key_source: "opencode.db", api_key_checked_paths: "(none)", - api_key_auth_paths: "/tmp/auth.json", + api_key_credential_database_paths: "/tmp/opencode.db", five_hour_usage: "70/4500 percent_remaining=98 reset_at=2026-03-25T18:00:00.000Z", weekly_usage: "105/45000 percent_remaining=100 reset_at=2026-04-01T00:00:00.000Z", }), @@ -1333,9 +1333,9 @@ describe("buildQuotaStatusReport", () => { expect(report).toContain("minimax:"); expect(report).toContain("- auth_state: configured"); expect(report).toContain("- api_key_configured: true"); - expect(report).toContain("- api_key_source: auth.json"); + expect(report).toContain("- api_key_source: opencode.db"); expect(report).toContain("- api_key_checked_paths: (none)"); - expect(report).toContain("- api_key_auth_paths: /tmp/auth.json"); + expect(report).toContain("- api_key_credential_database_paths: /tmp/opencode.db"); expect(report).toContain( "- five_hour_usage: 70/4500 percent_remaining=98 reset_at=2026-03-25T18:00:00.000Z", ); @@ -1420,7 +1420,7 @@ describe("buildQuotaStatusReport", () => { "alibaba auth configured": "false", alibaba_api_key_source: "(none)", alibaba_api_key_checked_paths: "(none)", - alibaba_api_key_auth_paths: "/tmp/auth.json", + alibaba_api_key_credential_database_paths: "/tmp/opencode.db", alibaba_coding_plan: "(none)", }), makeProviderSuccessProbe("openai", { @@ -1474,14 +1474,14 @@ describe("buildQuotaStatusReport", () => { paths: - opencode_dirs: data=/tmp/data config=/tmp/config cache=/tmp/cache state=/tmp/state - - auth.json: preferred=/tmp/auth.json present=(none) candidates=/tmp/auth.json + - opencode.db: preferred=/tmp/opencode.db present=(none) candidates=/tmp/opencode.db - qwen oauth auth configured: false - qwen_oauth_source: (none) - qwen_local_plan: (none) - alibaba auth configured: false - alibaba_api_key_source: (none) - alibaba_api_key_checked_paths: (none) - - alibaba_api_key_auth_paths: /tmp/auth.json + - alibaba_api_key_credential_database_paths: /tmp/opencode.db - alibaba_coding_plan: (none) openai: diff --git a/tests/plugin.qwen-hook.test.ts b/tests/plugin.qwen-hook.test.ts index 8b427a67..795e202a 100644 --- a/tests/plugin.qwen-hook.test.ts +++ b/tests/plugin.qwen-hook.test.ts @@ -28,8 +28,8 @@ vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); vi.mock("../src/lib/opencode-auth.js", () => ({ readAuthFileCached: vi.fn(), readAuthFile: vi.fn(), - getAuthPath: vi.fn(() => "/tmp/auth.json"), - getAuthPaths: vi.fn(() => ["/tmp/auth.json"]), + getCredentialDatabasePath: vi.fn(() => "/tmp/opencode.db"), + getCredentialDatabasePaths: vi.fn(() => ["/tmp/opencode.db"]), clearReadAuthFileCacheForTests: vi.fn(), })); vi.mock("../src/lib/qwen-auth.js", () => diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 878d34b0..35fc6478 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -164,9 +164,9 @@ describe("OpenCode Go shared projections", () => { }); mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValue({ state: "configured", - source: "auth.json", + source: "opencode.db", checkedPaths: ["env:OPENCODE_API_KEY"], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); mocks.queryOpenCodeGoQuota.mockResolvedValue(successfulResult()); diff --git a/tests/providers.alibaba-coding-plan.test.ts b/tests/providers.alibaba-coding-plan.test.ts index 25387881..af3d1009 100644 --- a/tests/providers.alibaba-coding-plan.test.ts +++ b/tests/providers.alibaba-coding-plan.test.ts @@ -8,7 +8,7 @@ import { } from "./helpers/provider-assertions.js"; vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPaths: () => ["/tmp/auth.json"], + getCredentialDatabasePaths: () => ["/tmp/opencode.db"], readAuthFileCached: vi.fn(), })); diff --git a/tests/providers.deepseek.test.ts b/tests/providers.deepseek.test.ts index 9a3b34a8..bd09111d 100644 --- a/tests/providers.deepseek.test.ts +++ b/tests/providers.deepseek.test.ts @@ -14,7 +14,7 @@ vi.mock("../src/lib/deepseek.js", () => ({ configured: false, source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })), })); diff --git a/tests/providers.glm-coding-plan.test.ts b/tests/providers.glm-coding-plan.test.ts index 9c780dfe..fdd8d180 100644 --- a/tests/providers.glm-coding-plan.test.ts +++ b/tests/providers.glm-coding-plan.test.ts @@ -30,7 +30,7 @@ vi.mock("../src/lib/zai-auth.js", () => ({ state: "none", source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })), })); vi.mock("../src/lib/zhipu-auth.js", () => ({ @@ -40,7 +40,7 @@ vi.mock("../src/lib/zhipu-auth.js", () => ({ state: "none", source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })), })); diff --git a/tests/providers.kilo.test.ts b/tests/providers.kilo.test.ts index d8c9e796..ae1a8116 100644 --- a/tests/providers.kilo.test.ts +++ b/tests/providers.kilo.test.ts @@ -33,7 +33,7 @@ describe("Kilo Gateway provider", () => { configured: true, source: "env:KILO_API_KEY", checkedPaths: ["env:KILO_API_KEY"], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); }); diff --git a/tests/providers.kimi-code.test.ts b/tests/providers.kimi-code.test.ts index e5709c66..01ab39d8 100644 --- a/tests/providers.kimi-code.test.ts +++ b/tests/providers.kimi-code.test.ts @@ -17,7 +17,7 @@ vi.mock("../src/lib/kimi-auth.js", () => ({ state: "none", source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })), DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS: 5_000, })); diff --git a/tests/providers.minimax-coding-plan.test.ts b/tests/providers.minimax-coding-plan.test.ts index e54557e2..c0144f94 100644 --- a/tests/providers.minimax-coding-plan.test.ts +++ b/tests/providers.minimax-coding-plan.test.ts @@ -35,13 +35,13 @@ vi.mock("../src/lib/minimax-auth.js", () => ({ state: "none", source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })), getMiniMaxChinaAuthDiagnostics: vi.fn(async () => ({ state: "none", source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })), DEFAULT_MINIMAX_AUTH_CACHE_MAX_AGE_MS: 5_000, })); diff --git a/tests/providers.nanogpt.test.ts b/tests/providers.nanogpt.test.ts index 3ef9fab7..a14a404f 100644 --- a/tests/providers.nanogpt.test.ts +++ b/tests/providers.nanogpt.test.ts @@ -14,7 +14,7 @@ vi.mock("../src/lib/nanogpt.js", () => ({ configured: false, source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })), })); diff --git a/tests/providers.ollama-cloud.test.ts b/tests/providers.ollama-cloud.test.ts index 8abd419c..e59d5ca0 100644 --- a/tests/providers.ollama-cloud.test.ts +++ b/tests/providers.ollama-cloud.test.ts @@ -35,7 +35,7 @@ describe("ollama-cloud provider", () => { configured: true, source: "env:OLLAMA_API_KEY", checkedPaths: ["env:OLLAMA_API_KEY"], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], }); }); diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index ae90c77d..88a7c4c0 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -55,9 +55,9 @@ function diagnostics( ): Record { return { state, - source: state === "none" ? null : "auth.json", + source: state === "none" ? null : "opencode.db", checkedPaths: ["env:OPENCODE_API_KEY", "/tmp/opencode.json"], - authPaths: ["/tmp/auth.json"], + credentialDatabasePaths: ["/tmp/opencode.db"], ...(state === "invalid" ? { error: "OpenCode Go auth entry present but key is empty" } : {}), }; } @@ -195,7 +195,7 @@ describe("opencode-go provider", () => { "auth_state", "auth_source", "auth_checked_paths", - "auth_paths", + "credential_database_paths", "selected_windows", "rolling_usage", "weekly_usage", diff --git a/tests/providers.quota-providers.test.ts b/tests/providers.quota-providers.test.ts index 92f049ec..2ce2c364 100644 --- a/tests/providers.quota-providers.test.ts +++ b/tests/providers.quota-providers.test.ts @@ -82,9 +82,9 @@ describe("quota-providers aggregate provider", () => { beforeEach(() => { runtimeMocks.resolveQuotaProviderApiKey.mockReset().mockResolvedValue({ key: "secret", - source: "auth.json", + source: "opencode.db", checkedPaths: ["/trusted/opencode.json"], - authPaths: ["/trusted/auth.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], }); runtimeMocks.fetchRemoteQuotaProvider.mockReset().mockResolvedValue({ success: true, @@ -431,9 +431,9 @@ describe("quota-providers aggregate provider", () => { providerId: "runtime-provider", mode: "remote-api", format: "quota-v1", - credentialSource: "auth_json", + credentialSource: "opencode_db", checkedPaths: ["/trusted/opencode.json"], - authPaths: ["/trusted/auth.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], }); expect(JSON.stringify(result.diagnostics)).not.toContain("secret"); }); diff --git a/tests/quota-providers-docs-consistency.test.ts b/tests/quota-providers-docs-consistency.test.ts index 2cdb4aca..bfee0d1f 100644 --- a/tests/quota-providers-docs-consistency.test.ts +++ b/tests/quota-providers-docs-consistency.test.ts @@ -88,7 +88,7 @@ describe("quota provider Phase 7 documentation consistency", () => { for (const document of [configuration, providers]) { expect(document).toContain("apiKeyEnv"); expect(document).toContain("provider..options.apiKey"); - expect(document).toContain("auth.json"); + expect(document).toContain("opencode.db"); expect(document).toContain("quota-providers"); } diff --git a/tests/quota-render-data.test.ts b/tests/quota-render-data.test.ts index 84c913b4..2b3cc789 100644 --- a/tests/quota-render-data.test.ts +++ b/tests/quota-render-data.test.ts @@ -1313,7 +1313,7 @@ describe("collectQuotaRenderData shared quota state", () => { it("fetches an available but disabled provider once and preserves its status details", async () => { const firstProvider = testProvider("synthetic", { attempted: false, - statusDetails: [{ key: "api_key_source", value: "auth.json" }], + statusDetails: [{ key: "api_key_source", value: "opencode.db" }], rawDetails: [{ key: "usage_usd", value: "$2.50" }], }); const duplicateProvider = testProvider("synthetic", { @@ -1333,7 +1333,7 @@ describe("collectQuotaRenderData shared quota state", () => { attempted: false, entries: [], errors: [], - statusDetails: [{ key: "api_key_source", value: "auth.json" }], + statusDetails: [{ key: "api_key_source", value: "opencode.db" }], rawDetails: [{ key: "usage_usd", value: "$2.50" }], }, }, @@ -1343,7 +1343,7 @@ describe("collectQuotaRenderData shared quota state", () => { attempted: false, entries: [], errors: [], - statusDetails: [{ key: "api_key_source", value: "auth.json" }], + statusDetails: [{ key: "api_key_source", value: "opencode.db" }], rawDetails: [{ key: "usage_usd", value: "$2.50" }], }, }, diff --git a/tests/quota-state.test.ts b/tests/quota-state.test.ts index f3b5e748..1b26f127 100644 --- a/tests/quota-state.test.ts +++ b/tests/quota-state.test.ts @@ -1182,7 +1182,7 @@ describe("quota-state shared cache", () => { outcome: "success", entryCount: 1, checkedPaths: ["env:EXPLICIT_KEY"], - authPaths: ["/trusted/auth.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], }, ], }), @@ -1253,11 +1253,11 @@ describe("quota-state shared cache", () => { apiKeyEnv: null, selected: true, attempted: true, - credentialSource: "auth_json", + credentialSource: "opencode_db", outcome: "success", entryCount: 1, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }, ], } as const; diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index c4c534d8..3e72acd8 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -102,7 +102,7 @@ vi.mock("../src/lib/minimax-auth.js", () => ({ state: "none", source: null, checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], })), })); vi.mock("../src/lib/opencode-runtime-paths.js", () => @@ -282,10 +282,10 @@ describe("v4 Phase 5 cross-surface release evidence", () => { }); mocks.getMiniMaxAuthDiagnostics.mockResolvedValue({ state: "configured", - source: "auth.json", + source: "opencode.db", endpoint: "international", checkedPaths: [], - authPaths: [], + credentialDatabasePaths: [], }); const { quotaProvidersProvider } = await import("../src/providers/quota-providers.js"); From a76299f4a024601745ede4150f894e826f9e7833 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Fri, 21 Aug 2026 21:32:42 +0200 Subject: [PATCH 11/97] fix(tooling): lint nested staged files --- biome.json | 2 +- lefthook.yml | 2 +- scripts/verify-typescript-version.mjs | 13 +++---- src/tui-v2.tsx | 50 ++++++++++++++++----------- tests/package-manifest.test.ts | 14 ++++---- tests/release-gates.test.ts | 6 ++-- tests/tui-v2-commands.test.ts | 14 ++++++-- 7 files changed, 61 insertions(+), 40 deletions(-) diff --git a/biome.json b/biome.json index 17174aba..efeb97a7 100644 --- a/biome.json +++ b/biome.json @@ -83,7 +83,7 @@ } }, { - "includes": ["src/tui.tsx"], + "includes": ["src/tui.tsx", "src/tui-v2.tsx"], "linter": { "rules": { "a11y": { diff --git a/lefthook.yml b/lefthook.yml index 21047dec..4da47392 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,7 +1,7 @@ pre-commit: commands: biome: - glob: "*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc,css}" + glob: "**/*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc,css}" run: pnpm exec biome check --write --no-errors-on-unmatched {staged_files} stage_fixed: true diff --git a/scripts/verify-typescript-version.mjs b/scripts/verify-typescript-version.mjs index 13d3756e..e270922e 100644 --- a/scripts/verify-typescript-version.mjs +++ b/scripts/verify-typescript-version.mjs @@ -5,10 +5,11 @@ import { fileURLToPath } from "node:url"; import { parse } from "yaml"; const EXPECTED_TYPESCRIPT_VERSION = "7.0.2"; -const EXPECTED_PLUGIN_VERSION = "1.18.11"; -const EXPECTED_OPENTUI_VERSION = "0.4.5"; +const EXPECTED_PLUGIN_VERSION = "1.18.1"; +const EXPECTED_OPENTUI_SPECIFIER = "^0.5.3"; +const EXPECTED_OPENTUI_VERSION = "0.5.4"; const EXPECTED_OPENTUI_PACKAGES = ["@opentui/core", "@opentui/solid"]; -const BUN_FFI_STRUCTS_VERSION = "0.2.4"; +const BUN_FFI_STRUCTS_VERSION = "0.3.1"; const BUN_FFI_TYPESCRIPT_PEER = "^5"; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); @@ -66,9 +67,9 @@ if (configuredPlugin !== EXPECTED_PLUGIN_VERSION) { } for (const name of EXPECTED_OPENTUI_PACKAGES) { const configuredVersion = packageJson.dependencies?.[name]; - if (configuredVersion !== EXPECTED_OPENTUI_VERSION) { + if (configuredVersion !== EXPECTED_OPENTUI_SPECIFIER) { fail( - `${name} must be pinned exactly to ${EXPECTED_OPENTUI_VERSION} for @opencode-ai/plugin ${EXPECTED_PLUGIN_VERSION} compatibility; package.json has ${String(configuredVersion)}.`, + `${name} must be pinned to ${EXPECTED_OPENTUI_SPECIFIER}; package.json has ${String(configuredVersion)}.`, ); } } @@ -111,7 +112,7 @@ verifyImporterDependency("typescript", EXPECTED_TYPESCRIPT_VERSION); for (const name of EXPECTED_OPENTUI_PACKAGES) { const dependency = rootImporter.dependencies?.[name]; if ( - dependency?.specifier !== EXPECTED_OPENTUI_VERSION || + dependency?.specifier !== EXPECTED_OPENTUI_SPECIFIER || (dependency?.version !== EXPECTED_OPENTUI_VERSION && !dependency?.version?.startsWith(`${EXPECTED_OPENTUI_VERSION}(`)) ) { diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index e1bb861d..ae744529 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -1,7 +1,8 @@ /** @jsxImportSource @opentui/solid */ -import type { JSX } from "@opentui/solid"; + import { RGBA } from "@opentui/core"; -import { Show, createSignal, onCleanup } from "solid-js"; +import type { JSX } from "@opentui/solid"; +import { createSignal, onCleanup, Show } from "solid-js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { formatQuotaRows } from "./lib/format.js"; @@ -10,13 +11,13 @@ import { QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, } from "./lib/quota-dialog-commands.js"; +import { resolveQuotaFormatStyle } from "./lib/quota-format-style.js"; import { collectQuotaRenderData } from "./lib/quota-render-data.js"; import { createQuotaRuntimeRequestContext, - resolveQuotaRuntimeContext, type QuotaSessionModelContext, + resolveQuotaRuntimeContext, } from "./lib/quota-runtime-context.js"; -import { resolveQuotaFormatStyle } from "./lib/quota-format-style.js"; import { buildSidebarQuotaPanelLines } from "./lib/tui-sidebar-format.js"; const terminalForeground = RGBA.defaultForeground(); @@ -32,22 +33,25 @@ type TuiContext = { client: unknown; data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void }; keymap: { - layer: (build: () => { - mode: "global"; - commands: Array<{ - id: string; - title: string; - group: string; - palette: true; - slash: { name: string }; - run: (input?: unknown) => void; - }>; - }) => void; + layer: ( + build: () => { + mode: "global"; + commands: Array<{ + id: string; + title: string; + group: string; + palette: true; + slash: { name: string }; + run: (input?: unknown) => void; + }>; + }, + ) => void; }; ui: { - slot: (claim: - | { append: "app"; render: () => null } - | { append: "sidebar.content"; render: (props: { sessionID: string }) => JSX.Element } + slot: ( + claim: + | { append: "app"; render: () => null } + | { append: "sidebar.content"; render: (props: { sessionID: string }) => JSX.Element }, ) => () => void; toast: { show: (toast: Toast) => void }; dialog: { @@ -244,7 +248,11 @@ function SidebarQuotaView(props: { return ( - expandable() && setOpen((value) => !value)}> + expandable() && setOpen((value) => !value)} + > {open() ? "▼" : "▶"} @@ -295,7 +303,9 @@ const plugin = { }) .catch(reportFailure); }; - const onStepEnded = context.data.on("session.step.ended", (event) => trigger(event, "idle")); + const onStepEnded = context.data.on("session.step.ended", (event) => + trigger(event, "idle"), + ); const onCompacted = context.data.on("session.compaction.ended", (event) => trigger(event, "compacted"), ); diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index fcd1a924..d7aff136 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -153,9 +153,9 @@ describe("package manifest compatibility", () => { it("keeps the public plugin peer broad and reference-compatible development targets exact", () => { expect(pkg.peerDependencies?.["@opencode-ai/plugin"]).toBe("^1.4.3"); - expect(pkg.devDependencies?.["@opencode-ai/plugin"]).toBe("1.18.11"); - expect(pkg.dependencies?.["@opentui/core"]).toBe("0.4.5"); - expect(pkg.dependencies?.["@opentui/solid"]).toBe("0.4.5"); + expect(pkg.devDependencies?.["@opencode-ai/plugin"]).toBe("1.18.1"); + expect(pkg.dependencies?.["@opentui/core"]).toBe("^0.5.3"); + expect(pkg.dependencies?.["@opentui/solid"]).toBe("^0.5.3"); expect(readme).toContain("Node.js `>= 22` is required."); expect(readme).not.toContain("OpenCode `>= 1.4.3`"); expect(pkg.engines).not.toHaveProperty("opencode"); @@ -164,8 +164,10 @@ describe("package manifest compatibility", () => { it("keeps the TypeScript 7 toolchain explicit without suppressing the known peer mismatch", () => { expect(tsconfig.compilerOptions?.types).toEqual(["node"]); expect(typescriptValidator).toContain('const EXPECTED_TYPESCRIPT_VERSION = "7.0.2";'); - expect(typescriptValidator).toContain('const EXPECTED_PLUGIN_VERSION = "1.18.11";'); - expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_VERSION = "0.4.5";'); + expect(typescriptValidator).toContain('const EXPECTED_PLUGIN_VERSION = "1.18.1";'); + expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_SPECIFIER = "^0.5.3";'); + expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_VERSION = "0.5.4";'); + expect(typescriptValidator).toContain('const BUN_FFI_STRUCTS_VERSION = "0.3.1";'); expect(typescriptValidator).toContain('const BUN_FFI_TYPESCRIPT_PEER = "^5";'); expect(typescriptValidator).toContain("Known unmet peer:"); expect(typescriptValidator).not.toMatch(/TypeScript v4 freeze|\^5\.9/); @@ -215,7 +217,7 @@ describe("package manifest compatibility", () => { expect(lefthookConfig["pre-commit"]?.commands).toEqual({ biome: { - glob: "*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc,css}", + glob: "**/*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc,css}", run: "pnpm exec biome check --write --no-errors-on-unmatched {staged_files}", stage_fixed: true, }, diff --git a/tests/release-gates.test.ts b/tests/release-gates.test.ts index 2a71ea7d..5d5a588c 100644 --- a/tests/release-gates.test.ts +++ b/tests/release-gates.test.ts @@ -115,7 +115,7 @@ describe("v4 release gates", () => { const typescript = run(typescriptScript); expect(typescript.status).toBe(0); expect(typescript.stdout).toContain( - "TypeScript 7.0.2 and @opencode-ai/plugin 1.18.11 lock entries verified", + "TypeScript 7.0.2 and @opencode-ai/plugin 1.18.1 lock entries verified", ); const history = run(historyScript); @@ -147,7 +147,7 @@ describe("v4 release gates", () => { "references/local/opencode-1.18.2/package.json", "references/branches/private-plan.md", "prompt-exports/session.md", - "opencode-quota/auth.json", + "opencode-quota/opencode.db", "images/private-smoke.png", "opencode.json", "tui.json", @@ -210,7 +210,7 @@ describe("v4 release gates", () => { "references/local/v4.0.0-consolidated-plan.md", "references/upstream-plugins/README.md", "prompt-exports/review.md", - "opencode-quota/auth.json", + "opencode-quota/opencode.db", "tests/package-manifest.test.ts", "scripts/verify-release-version.mjs", "opencode-quota-logo-dark.svg", diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 4894e7d9..039c83c3 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -4,11 +4,17 @@ import plugin from "../src/tui-v2.tsx"; describe("V2 quota TUI commands", () => { it("registers every quota command as a local slash command", () => { - let layer: { commands: Array<{ slash: { name: string }; palette: boolean; run: () => void }> } | undefined; + let layer: + | { commands: Array<{ slash: { name: string }; palette: boolean; run: () => void }> } + | undefined; const context = { client: {}, data: { on: vi.fn(() => vi.fn()) }, - keymap: { layer: vi.fn((build) => (layer = build())) }, + keymap: { + layer: vi.fn((build) => { + layer = build(); + }), + }, ui: { slot: vi.fn((claim) => { if (claim.append === "app") claim.render(); @@ -37,7 +43,9 @@ describe("V2 quota TUI commands", () => { ]); expect(layer?.commands.every((command) => command.palette)).toBe(true); expect(context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "app" })); - expect(context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "sidebar.content" })); + expect(context.ui.slot).toHaveBeenCalledWith( + expect.objectContaining({ append: "sidebar.content" }), + ); expect(context.data.on.mock.calls.map(([event]) => event)).toEqual([ "session.step.ended", "session.compaction.ended", From 54d4ce322f969f186ea093103775a57d6c904ee0 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Wed, 26 Aug 2026 17:10:41 +0200 Subject: [PATCH 12/97] feat(quota): Show all OpenCode connections Read every native credential row and report quota independently for each connection. Preserve provider plans, aliases, active markers, partial errors, and per-connection accounting across supported providers. --- src/lib/accounting-format.ts | 2 +- src/lib/anthropic.ts | 10 ++ src/lib/copilot.ts | 4 +- src/lib/glm-coding-plan.ts | 6 +- src/lib/google-agy.ts | 4 +- src/lib/google-gemini-cli.ts | 4 +- src/lib/kimi.ts | 6 +- src/lib/openai.ts | 11 +- src/lib/opencode-auth.ts | 84 ++++++++++--- src/lib/toast-format-grouped.ts | 2 +- src/lib/xai.ts | 4 +- src/lib/zai.ts | 2 +- src/lib/zhipu.ts | 2 +- src/providers/anthropic.ts | 50 ++++++++ src/providers/copilot.ts | 69 ++++++++++- src/providers/glm-coding-plan-provider.ts | 64 ++++++++++ src/providers/google-agy.ts | 62 +++++++++- src/providers/google-gemini-cli.ts | 74 ++++++++++++ src/providers/kimi-code.ts | 63 ++++++++++ src/providers/minimax-coding-plan.ts | 70 ++++++++++- src/providers/openai.ts | 104 +++++++++++----- src/providers/opencode-go.ts | 49 +++++++- src/providers/xai.ts | 53 ++++++++ src/providers/zai.ts | 3 + src/providers/zhipu.ts | 3 + tests/format.test.ts | 8 +- tests/lib.accounting-format.test.ts | 2 +- tests/lib.opencode-auth.test.ts | 65 ++++++++++ tests/lib.opencode-go-auth.test.ts | 4 + tests/provider-surfaces.google-agy.test.ts | 4 +- ...ovider-surfaces.google-antigravity.test.ts | 2 +- tests/providers.anthropic.test.ts | 50 ++++++++ tests/providers.copilot.surfaces.test.ts | 24 ++++ tests/providers.glm-coding-plan.test.ts | 4 +- tests/providers.google-db-credentials.test.ts | 106 ++++++++++++++++ tests/providers.kimi-code.test.ts | 34 ++++++ tests/providers.minimax-coding-plan.test.ts | 29 +++++ tests/providers.openai.test.ts | 114 ++++++++++++++++++ tests/providers.opencode-go.test.ts | 29 +++++ tests/providers.xai.surfaces.test.ts | 26 ++++ tests/tui-sidebar-format.test.ts | 4 +- tests/tui-smoke.test.ts | 8 +- tests/v4-phase5-cross-surface.test.ts | 6 +- 43 files changed, 1234 insertions(+), 90 deletions(-) create mode 100644 tests/providers.google-db-credentials.test.ts diff --git a/src/lib/accounting-format.ts b/src/lib/accounting-format.ts index c2d6804e..68ae8665 100644 --- a/src/lib/accounting-format.ts +++ b/src/lib/accounting-format.ts @@ -36,7 +36,7 @@ const RESULT_TYPE_ORDER: Readonly> = { const WINDOW_LABELS: Readonly> = { rpm: "RPM", hour: "Hourly", - five_hour: "Five-hour", + five_hour: "5h", day: "Daily", week: "Weekly", month: "Monthly", diff --git a/src/lib/anthropic.ts b/src/lib/anthropic.ts index 4a44512f..7afcdb73 100644 --- a/src/lib/anthropic.ts +++ b/src/lib/anthropic.ts @@ -828,6 +828,16 @@ async function queryAnthropicQuotaFromOAuthAccessToken( } } +export async function queryAnthropicQuotaWithOAuth( + accessToken: string, + requestTimeoutMs?: number, +): Promise { + const result = await queryAnthropicQuotaFromOAuthAccessToken(accessToken, requestTimeoutMs); + return result.state === "success" + ? result.quota + : { success: false, error: result.detail ?? "Anthropic OAuth quota unavailable" }; +} + function extractAuthBoolean(data: unknown): boolean | undefined { const record = asRecord(data); if (!record) { diff --git a/src/lib/copilot.ts b/src/lib/copilot.ts index 48d4d53a..2e76afb7 100644 --- a/src/lib/copilot.ts +++ b/src/lib/copilot.ts @@ -1343,7 +1343,7 @@ async function fetchCopilotInternalUser(params: { * OpenCode-managed Copilot OAuth token can query the per-user internal quota endpoint. */ export async function queryCopilotQuota( - options: { requestTimeoutMs?: number } = {}, + options: { requestTimeoutMs?: number; authData?: AuthData } = {}, ): Promise { const pat = readQuotaConfigWithMeta(); @@ -1353,7 +1353,7 @@ export async function queryCopilotQuota( ); } if (pat.state === "absent" || !pat.config) { - const { auth } = selectCopilotAuth(await readAuthFile()); + const { auth } = selectCopilotAuth(options.authData ?? (await readAuthFile())); const token = getCopilotOAuthToken(auth); if (!auth || !token) return null; diff --git a/src/lib/glm-coding-plan.ts b/src/lib/glm-coding-plan.ts index 4b5285f9..ff302bb8 100644 --- a/src/lib/glm-coding-plan.ts +++ b/src/lib/glm-coding-plan.ts @@ -21,9 +21,11 @@ type GlmCodingPlanDescriptor = { export async function queryGlmCodingPlanQuota( descriptor: GlmCodingPlanDescriptor, - options: { requestTimeoutMs?: number } = {}, + options: { requestTimeoutMs?: number; apiKey?: string } = {}, ): Promise { - const auth = await descriptor.resolveAuth(); + const auth = options.apiKey + ? { state: "configured" as const, apiKey: options.apiKey } + : await descriptor.resolveAuth(); if (auth.state === "none") return null; if (auth.state === "invalid") return { success: false, error: auth.error }; diff --git a/src/lib/google-agy.ts b/src/lib/google-agy.ts index f0b45a5a..d751a59d 100644 --- a/src/lib/google-agy.ts +++ b/src/lib/google-agy.ts @@ -691,10 +691,10 @@ async function fetchAccountQuota(params: { export async function queryGoogleAgyQuota( client?: ConfigClient, - options: { requestTimeoutMs?: number } = {}, + options: { requestTimeoutMs?: number; authData?: AuthData } = {}, ): Promise { const [auth, configuredProjectId] = await Promise.all([ - readAuthFileCached({ maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS }), + options.authData ?? readAuthFileCached({ maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS }), resolveAgyConfiguredProjectId(client), ]); const accounts = resolveAgyAccounts(auth, configuredProjectId); diff --git a/src/lib/google-gemini-cli.ts b/src/lib/google-gemini-cli.ts index 4982b738..71a77880 100644 --- a/src/lib/google-gemini-cli.ts +++ b/src/lib/google-gemini-cli.ts @@ -597,10 +597,10 @@ async function fetchAccountQuota(params: { export async function queryGeminiCliQuota( client?: ConfigClient, - options: { requestTimeoutMs?: number } = {}, + options: { requestTimeoutMs?: number; authData?: AuthData } = {}, ): Promise { const [auth, configuredProjectId] = await Promise.all([ - readAuthFileCached({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS }), + options.authData ?? readAuthFileCached({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS }), resolveGeminiCliConfiguredProjectId(client), ]); const accounts = resolveGeminiCliAccounts(auth, configuredProjectId); diff --git a/src/lib/kimi.ts b/src/lib/kimi.ts index 931c121e..b2eb9d38 100644 --- a/src/lib/kimi.ts +++ b/src/lib/kimi.ts @@ -245,9 +245,11 @@ async function fetchKimiQuotaFromUrl( } export async function queryKimiQuota( - options: { requestTimeoutMs?: number } = {}, + options: { requestTimeoutMs?: number; apiKey?: string } = {}, ): Promise { - const auth = await resolveKimiAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }); + const auth = options.apiKey + ? { state: "configured" as const, apiKey: options.apiKey } + : await resolveKimiAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }); if (auth.state === "none") return null; if (auth.state === "invalid") { return { success: false, error: auth.error }; diff --git a/src/lib/openai.ts b/src/lib/openai.ts index 1bbc4e37..9130adb4 100644 --- a/src/lib/openai.ts +++ b/src/lib/openai.ts @@ -247,12 +247,13 @@ export async function hasOpenAIOAuthCached(params?: { maxAgeMs?: number }): Prom } export async function queryOpenAIQuota( - options: { requestTimeoutMs?: number } = {}, + options: { requestTimeoutMs?: number; auth?: ResolvedOpenAIOAuth } = {}, ): Promise { - const auth = await readAuthFileCached({ - maxAgeMs: DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS, - }); - const resolvedAuth = resolveOpenAIOAuth(auth); + const resolvedAuth = + options.auth ?? + resolveOpenAIOAuth( + await readAuthFileCached({ maxAgeMs: DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS }), + ); if (resolvedAuth.state !== "configured") return null; if (resolvedAuth.expiresAt && resolvedAuth.expiresAt < Date.now()) { diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 29d23750..5084d764 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -35,6 +35,42 @@ type AuthCacheEntry = { inFlight?: Promise; }; +export type CredentialRow = { + id: string; + integrationId: string; + label: string; + active: boolean; + value: Record; +}; + +export function formatCredentialDisplayNames( + providerName: string, + credentials: ReadonlyArray<{ row: CredentialRow; fallbackName: string }>, +): string[] { + const counts = new Map(); + return credentials.map(({ row, fallbackName }) => { + const alias = row.label.trim(); + const redundantAlias = + !alias || + alias.toLowerCase() === "default" || + alias.toLowerCase() === providerName.toLowerCase(); + const fallbackCategory = fallbackName + .trim() + .replace(new RegExp(`^${providerName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\s*`, "iu"), "") + .trim() + .replace(/^\((.*)\)$/u, "$1") + .trim(); + const aliasKey = redundantAlias ? "" : alias; + const duplicate = aliasKey ? (counts.get(aliasKey) ?? 0) + 1 : 1; + if (aliasKey) counts.set(aliasKey, duplicate); + const numberedAlias = duplicate === 1 ? aliasKey : `${aliasKey} ${duplicate}`; + const base = `[${providerName}${numberedAlias ? ` ${numberedAlias}` : ""}]`; + const category = fallbackCategory ? ` (${fallbackCategory})` : ""; + const active = row.active || (credentials.length === 1 && !credentials[0]!.row.active); + return `${base}${category}${active ? "*" : ""}`; + }); +} + let authCache: AuthCacheEntry | null = null; /** @@ -74,16 +110,25 @@ export function getCredentialDatabasePath(): string { } export async function readAuthFile(): Promise { - return readCredentialDatabases(getCredentialDatabasePaths()); + const rows = await readCredentialRows(); + const auth: Record = {}; + for (const row of rows) { + if (!(row.integrationId in auth)) auth[row.integrationId] = row.value; + } + return Object.keys(auth).length > 0 ? (auth as AuthData) : null; +} + +export async function readCredentialRows(): Promise { + return readCredentialRowsFromDatabases(getCredentialDatabasePaths()); } -function readCredentialDatabases(paths: string[]): AuthData | null { +function readCredentialRowsFromDatabases(paths: string[]): CredentialRow[] { for (const path of paths) { - const auth = readCredentialDatabase(path); - if (auth) return auth; + const rows = readCredentialDatabase(path); + if (rows.length > 0) return rows; } - return null; + return []; } function getCredentialDatabasePathsForDataDirs(dataDirs: string[]): string[] { @@ -99,33 +144,42 @@ function getCredentialDatabasePathsForDataDirs(dataDirs: string[]): string[] { return dataDirs.map((dataDir) => join(dataDir, "opencode.db")); } -function readCredentialDatabase(path: string): AuthData | null { - if (!existsSync(path)) return null; +function readCredentialDatabase(path: string): CredentialRow[] { + if (!existsSync(path)) return []; let database: CredentialDatabase | undefined; try { database = openCredentialDatabase(path); const rows = database .prepare( - "SELECT integration_id, value FROM credential WHERE integration_id IS NOT NULL ORDER BY time_updated DESC, id DESC", + "SELECT id, integration_id, label, active, value FROM credential WHERE integration_id IS NOT NULL ORDER BY active DESC, time_updated DESC, id DESC", ) - .all() as Array<{ integration_id?: unknown; value?: unknown }>; - const auth: Record = {}; + .all() as Array>; + const credentials: CredentialRow[] = []; for (const row of rows) { if ( + typeof row.id !== "string" || typeof row.integration_id !== "string" || - typeof row.value !== "string" || - row.integration_id in auth + typeof row.label !== "string" || + typeof row.value !== "string" ) continue; const value = parseCredentialValue(row.value); - if (value) auth[row.integration_id] = value; + if (value) { + credentials.push({ + id: row.id, + integrationId: row.integration_id, + label: row.label, + active: row.active === 1, + value, + }); + } } - return Object.keys(auth).length > 0 ? (auth as AuthData) : null; + return credentials; } catch { - return null; + return []; } finally { database?.close(); } diff --git a/src/lib/toast-format-grouped.ts b/src/lib/toast-format-grouped.ts index 0c7e0260..1d3f6cec 100644 --- a/src/lib/toast-format-grouped.ts +++ b/src/lib/toast-format-grouped.ts @@ -30,7 +30,7 @@ function normalizeLabelText(value?: string): string { const GROUPED_WINDOW_LABELS: Readonly> = { rpm: "RPM", - five_hour: "Five-hour", + five_hour: "5h", hour: "Hourly", week: "Weekly", day: "Daily", diff --git a/src/lib/xai.ts b/src/lib/xai.ts index e1f7c54c..262447da 100644 --- a/src/lib/xai.ts +++ b/src/lib/xai.ts @@ -226,12 +226,12 @@ function safeErrorText(message: string, accessToken: string): string { } export async function queryXaiQuota( - options: { requestTimeoutMs?: number } = {}, + options: { requestTimeoutMs?: number; auth?: ResolvedXaiOAuth } = {}, ): Promise { // OpenCode can replace this OAuth entry while servicing a model request. // Read the file directly so a post-request quota fetch cannot reuse the // token snapshot from before that refresh. - const resolvedAuth = resolveXaiOAuth(await readAuthFile()); + const resolvedAuth = options.auth ?? resolveXaiOAuth(await readAuthFile()); if (resolvedAuth.state !== "configured") return null; if (resolvedAuth.expiresAt !== undefined && resolvedAuth.expiresAt <= Date.now()) { diff --git a/src/lib/zai.ts b/src/lib/zai.ts index c33ee0a6..a31bebf3 100644 --- a/src/lib/zai.ts +++ b/src/lib/zai.ts @@ -10,6 +10,6 @@ const ZAI_QUOTA = { resolveAuth: resolveZaiAuthCached, } as const; -export function queryZaiQuota(options: { requestTimeoutMs?: number } = {}) { +export function queryZaiQuota(options: { requestTimeoutMs?: number; apiKey?: string } = {}) { return queryGlmCodingPlanQuota(ZAI_QUOTA, options); } diff --git a/src/lib/zhipu.ts b/src/lib/zhipu.ts index 09e8d791..1b69945f 100644 --- a/src/lib/zhipu.ts +++ b/src/lib/zhipu.ts @@ -9,6 +9,6 @@ const ZHIPU_QUOTA = { resolveAuth: resolveZhipuAuthCached, } as const; -export function queryZhipuQuota(options: { requestTimeoutMs?: number } = {}) { +export function queryZhipuQuota(options: { requestTimeoutMs?: number; apiKey?: string } = {}) { return queryGlmCodingPlanQuota(ZHIPU_QUOTA, options); } diff --git a/src/providers/anthropic.ts b/src/providers/anthropic.ts index 7bd12a60..ef07e667 100644 --- a/src/providers/anthropic.ts +++ b/src/providers/anthropic.ts @@ -8,7 +8,9 @@ import { getAnthropicDiagnostics, hasAnthropicCredentialsConfigured, queryAnthropicQuota, + queryAnthropicQuotaWithOAuth, } from "../lib/anthropic.js"; +import { resolveAnthropicOAuth } from "../lib/anthropic-auth.js"; import { sanitizeDisplayText } from "../lib/display-sanitize.js"; import type { QuotaProvider, @@ -16,7 +18,9 @@ import type { QuotaProviderResult, QuotaToastEntry, } from "../lib/entries.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; +import type { AuthData } from "../lib/types.js"; import { attemptedErrorResult, attemptedResult, @@ -58,6 +62,11 @@ export const anthropicProvider: QuotaProvider = { }; let statusDetails; let acquisitionMethod: QuotaToastEntry["accounting"]["acquisitionMethod"] = "local_cli"; + const databaseCredentials = (await readCredentialRows()).flatMap((row) => { + if (row.integrationId !== "anthropic") return []; + const auth = resolveAnthropicOAuth({ anthropic: row.value } as AuthData); + return auth.state === "configured" ? [{ row, auth }] : []; + }); try { const diagnostics = await getAnthropicDiagnostics(options); const quota = diagnostics.quotaSupported ? diagnostics.quota : undefined; @@ -87,6 +96,47 @@ export const anthropicProvider: QuotaProvider = { }); } + if (databaseCredentials.length > 0) { + const results = await Promise.all( + databaseCredentials.map(async ({ row, auth }) => ({ + row, + result: await queryAnthropicQuotaWithOAuth(auth.accessToken, options.requestTimeoutMs), + })), + ); + const names = formatCredentialDisplayNames( + "Claude", + results.map(({ row }) => ({ row, fallbackName: "Claude" })), + ); + const entries: QuotaToastEntry[] = []; + const errors: QuotaProviderResult["errors"] = []; + for (const [index, { row, result }] of results.entries()) { + const group = names[index] ?? "Claude"; + if (!result?.success) { + if (result) errors.push({ label: group, message: result.error }); + continue; + } + entries.push( + ...[["5h", result.five_hour] as const, ["Weekly", result.seven_day] as const].map( + ([label, window]) => ({ + accounting: { + resultType: "quota" as const, + acquisitionMethod: "remote_api" as const, + ownership: "maintained" as const, + authority: "provider_reported" as const, + sourceId: row.id, + }, + name: `${group} ${label}`, + group, + label: `${label}:`, + percentRemaining: window.percentRemaining, + resetTimeIso: window.resetTimeIso, + }), + ), + ); + } + return withStatusDetails(attemptedResult(entries, errors), statusDetails); + } + const result = await queryAnthropicQuota(options); if (!result) { return withStatusDetails(notAttemptedResult(), statusDetails); diff --git a/src/providers/copilot.ts b/src/providers/copilot.ts index 45501463..5f184ed6 100644 --- a/src/providers/copilot.ts +++ b/src/providers/copilot.ts @@ -17,10 +17,15 @@ import type { QuotaProviderResult, QuotaToastEntry, } from "../lib/entries.js"; -import { readAuthFileCached } from "../lib/opencode-auth.js"; +import { + formatCredentialDisplayNames, + readAuthFileCached, + readCredentialRows, +} from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import { modelIncludesAny, modelProviderIncludesAny } from "../lib/provider-model-matching.js"; import type { + AuthData, CopilotBudgetResult, CopilotEnterpriseUsageResult, CopilotOrganizationUsageResult, @@ -291,6 +296,68 @@ export const copilotProvider: QuotaProvider = { async fetch(ctx: QuotaProviderContext): Promise { const statusDetails = await getCopilotStatusDetails(); + const authData = await readAuthFileCached({ maxAgeMs: 5_000 }); + if (getCopilotQuotaAuthDiagnostics(authData).effectiveSource === "oauth") { + const credentials = (await readCredentialRows()).filter((row) => + ["github-copilot", "copilot", "copilot-chat", "github-copilot-chat"].includes( + row.integrationId, + ), + ); + if (credentials.length > 0) { + const results = await Promise.all( + credentials.map(async (row) => ({ + row, + result: await queryCopilotQuota({ + requestTimeoutMs: ctx.config?.requestTimeoutMs, + authData: { [row.integrationId]: row.value } as AuthData, + }), + })), + ); + const names = formatCredentialDisplayNames( + "Copilot", + results.map(({ row, result }) => ({ + row, + fallbackName: + result?.success && result.mode === "user_plan" && result.plan + ? `Copilot (${result.plan})` + : result?.success + ? getCopilotGroup(result.mode) + : "Copilot", + })), + ); + const entries: QuotaToastEntry[] = []; + const errors: QuotaProviderResult["errors"] = []; + for (const [index, { row, result }] of results.entries()) { + const group = names[index] ?? "Copilot"; + if (!result) continue; + if (!result.success) { + errors.push({ label: group, message: result.error }); + continue; + } + const resultEntries = + result.mode === "user_plan" + ? planEntries(result) + : result.mode === "user_quota" + ? personalEntries(result) + : managedEntries(result); + entries.push( + ...resultEntries.map((entry) => ({ + ...entry, + name: entry.group ? entry.name.replace(entry.group, group) : entry.name, + group, + accounting: { ...entry.accounting, sourceId: row.id }, + })), + ); + errors.push( + ...("warnings" in result ? (result.warnings ?? []) : []).map((message) => ({ + label: group, + message, + })), + ); + } + return withStatusDetails(attemptedResult(entries, errors), statusDetails); + } + } const result = await queryCopilotQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs }); if (!result) return withStatusDetails(notAttemptedResult(), statusDetails); if (!result.success) { diff --git a/src/providers/glm-coding-plan-provider.ts b/src/providers/glm-coding-plan-provider.ts index b802765a..0eb8b73f 100644 --- a/src/providers/glm-coding-plan-provider.ts +++ b/src/providers/glm-coding-plan-provider.ts @@ -1,6 +1,8 @@ import type { QuotaProvider, QuotaProviderContext } from "../lib/entries.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import type { CanonicalQuotaProviderId } from "../lib/provider-metadata.js"; +import type { AuthData } from "../lib/types.js"; import { apiKeyStatusDetails, attemptedResult, @@ -31,6 +33,13 @@ export function createGlmCodingPlanProvider(params: { errorLabel: string; authCacheMaxAgeMs: number; resolveAuth: (params: { maxAgeMs: number }) => Promise<{ state: string }>; + resolveCredentialAuth: ( + auth: AuthData, + ) => + | { state: "configured"; apiKey: string } + | { state: "none" } + | { state: "invalid"; error: string }; + credentialIntegrationIds: readonly string[]; getAuthDiagnostics: (params: { maxAgeMs: number }) => Promise<{ state: string; source: string | null; @@ -40,6 +49,7 @@ export function createGlmCodingPlanProvider(params: { }>; queryQuota: (params: { requestTimeoutMs?: number; + apiKey?: string; }) => Promise; matchesCurrentModel: (model: string) => boolean; }): QuotaProvider { @@ -63,6 +73,60 @@ export function createGlmCodingPlanProvider(params: { async fetch(ctx: QuotaProviderContext) { const diagnostics = await params.getAuthDiagnostics({ maxAgeMs: params.authCacheMaxAgeMs }); const authDetails = apiKeyStatusDetails(diagnostics); + if (diagnostics.source === "opencode.db") { + const credentials = (await readCredentialRows()).flatMap((row) => { + if (!params.credentialIntegrationIds.includes(row.integrationId)) return []; + const auth = params.resolveCredentialAuth({ [row.integrationId]: row.value } as AuthData); + return auth.state === "configured" ? [{ row, auth }] : []; + }); + if (credentials.length > 0) { + const results = await Promise.all( + credentials.map(async ({ row, auth }) => ({ + row, + result: await params.queryQuota({ + requestTimeoutMs: ctx.config?.requestTimeoutMs, + apiKey: auth.apiKey, + }), + })), + ); + const names = formatCredentialDisplayNames( + params.errorLabel, + results.map(({ row, result }) => ({ + row, + fallbackName: result?.success ? result.label : params.errorLabel, + })), + ); + const entries = []; + const errors = []; + for (const [index, { row, result }] of results.entries()) { + const group = names[index] ?? params.errorLabel; + const mapped = mapNullableProviderResult(result, { + errorLabel: group, + onSuccess: (quota) => + attemptedResult( + groupedPercentWindowEntries({ + group, + accounting: { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + sourceId: row.id, + }, + windows: [ + { window: quota.windows.fiveHour, suffix: "5h", label: "5h:" }, + { window: quota.windows.weekly, suffix: "Weekly", label: "Weekly:" }, + { window: quota.windows.mcp, suffix: "MCP", label: "MCP:" }, + ], + }), + ), + }); + entries.push(...mapped.entries); + errors.push(...mapped.errors); + } + return withStatusDetails(attemptedResult(entries, errors), authDetails); + } + } const result = await params.queryQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs }); const providerResult = mapNullableProviderResult(result, { errorLabel: params.errorLabel, diff --git a/src/providers/google-agy.ts b/src/providers/google-agy.ts index 7ad892e2..910a7c0c 100644 --- a/src/providers/google-agy.ts +++ b/src/providers/google-agy.ts @@ -11,8 +11,9 @@ import { queryGoogleAgyQuota, } from "../lib/google-agy.js"; import { inspectAgyCompanionPresence } from "../lib/google-agy-companion.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { parseProviderModelRef } from "../lib/provider-model-matching.js"; -import type { GoogleAgyQuotaBucket } from "../lib/types.js"; +import type { AuthData, GoogleAgyQuotaBucket } from "../lib/types.js"; import { createGoogleAccountLabelMap, formatGoogleAccountErrors, @@ -137,6 +138,65 @@ export const googleAgyProvider: QuotaProvider = { companion_error: companion.state !== "present" ? sanitizeDisplayText(companion.error) : undefined, }); + const credentialRows = (await readCredentialRows()).filter((row) => + ["google-agy", "opencode-agy-auth", "google-agy-auth"].includes(row.integrationId), + ); + if (credentialRows.length > 0) { + const results = await Promise.all( + credentialRows.map(async (row) => ({ + row, + result: await queryGoogleAgyQuota(ctx.client, { + requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured + ? ctx.config.requestTimeoutMs + : undefined, + authData: { [row.integrationId]: row.value } as AuthData, + }), + })), + ); + const names = formatCredentialDisplayNames( + "Google AGY", + results.map(({ row }) => ({ row, fallbackName: "Google AGY" })), + ); + const entries: QuotaToastEntry[] = []; + const errors: QuotaProviderResult["errors"] = []; + for (const [index, { row, result }] of results.entries()) { + const group = names[index] ?? "Google AGY"; + if (!result) continue; + if (!result.success) { + errors.push({ label: group, message: result.error }); + continue; + } + for (const bucket of [...result.buckets].sort(compareBuckets)) { + const right = formatRemainingAmount(bucket.remainingAmount); + entries.push({ + accounting: { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + sourceId: row.id, + }, + name: `${group} ${formatAgyFamilyLabel(bucket.family)} ${bucket.windowLabel}`, + group, + label: `${bucket.windowLabel}:`, + sortPriority: windowRank(bucket.window), + ...(right ? { right } : {}), + percentRemaining: bucket.percentRemaining, + resetTimeIso: bucket.resetTimeIso, + }); + } + errors.push( + ...(result.errors ?? []).map((error) => ({ + label: group, + message: error.error, + })), + ); + } + return withStatusDetails( + attemptedResult(entries, errors, { singleWindowShowRight: true }), + statusDetails, + ); + } const result = await queryGoogleAgyQuota(ctx.client, { requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured ? ctx.config.requestTimeoutMs diff --git a/src/providers/google-gemini-cli.ts b/src/providers/google-gemini-cli.ts index 1f4961e6..485a6ca1 100644 --- a/src/providers/google-gemini-cli.ts +++ b/src/providers/google-gemini-cli.ts @@ -11,7 +11,9 @@ import { queryGeminiCliQuota, } from "../lib/google-gemini-cli.js"; import { inspectGeminiCliCompanionPresence } from "../lib/google-gemini-cli-companion.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { parseProviderModelRef } from "../lib/provider-model-matching.js"; +import type { AuthData } from "../lib/types.js"; import { createGoogleAccountLabelMap, formatGoogleAccountErrors, @@ -73,6 +75,78 @@ export const googleGeminiCliProvider: QuotaProvider = { companion_error: companion.state !== "present" ? sanitizeDisplayText(companion.error) : undefined, }); + const credentialRows = (await readCredentialRows()).filter((row) => + ["google-gemini-cli", "gemini-cli", "opencode-gemini-auth", "gemini", "google"].includes( + row.integrationId, + ), + ); + if (credentialRows.length > 0) { + const results = await Promise.all( + credentialRows.map(async (row) => ({ + row, + result: await queryGeminiCliQuota(ctx.client, { + requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured + ? ctx.config.requestTimeoutMs + : undefined, + authData: { [row.integrationId]: row.value } as AuthData, + }), + })), + ); + const names = formatCredentialDisplayNames( + "Gemini CLI", + results.map(({ row }) => ({ row, fallbackName: "Gemini CLI" })), + ); + const entries: QuotaToastEntry[] = []; + const errors: QuotaProviderResult["errors"] = []; + for (const [index, { row, result }] of results.entries()) { + const group = names[index] ?? "Gemini CLI"; + if (!result) continue; + if (!result.success) { + errors.push({ label: group, message: result.error }); + continue; + } + for (const bucket of result.buckets) { + const parsedRemaining = bucket.remainingAmount + ? Number.parseInt(bucket.remainingAmount, 10) + : Number.NaN; + const remainingAmount = bucket.remainingAmount + ? `${Number.isFinite(parsedRemaining) ? parsedRemaining.toLocaleString("en-US") : bucket.remainingAmount} left` + : undefined; + const tokenType = bucket.tokenType?.trim().toUpperCase(); + const right = [ + remainingAmount, + tokenType && tokenType !== "REQUESTS" ? tokenType : undefined, + ] + .filter(Boolean) + .join(" "); + entries.push({ + accounting: { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + sourceId: row.id, + }, + name: `${group} ${bucket.displayName}`, + group, + label: `${bucket.displayName}:`, + ...(right ? { right } : {}), + percentRemaining: bucket.percentRemaining, + resetTimeIso: bucket.resetTimeIso, + }); + } + errors.push( + ...(result.errors ?? []).map((error) => ({ label: group, message: error.error })), + ); + } + return withStatusDetails( + attemptedResult(entries, errors, { + singleWindowDisplayName: "Gemini CLI", + singleWindowShowRight: true, + }), + statusDetails, + ); + } const result = await queryGeminiCliQuota(ctx.client, { requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured ? ctx.config.requestTimeoutMs diff --git a/src/providers/kimi-code.ts b/src/providers/kimi-code.ts index 19df2b47..6acb9e63 100644 --- a/src/providers/kimi-code.ts +++ b/src/providers/kimi-code.ts @@ -8,10 +8,13 @@ import { queryKimiQuota } from "../lib/kimi.js"; import { DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, getKimiAuthDiagnostics, + resolveKimiAuth, resolveKimiAuthCached, } from "../lib/kimi-auth.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import { normalizeQuotaProviderId } from "../lib/provider-metadata.js"; +import type { AuthData } from "../lib/types.js"; import { apiKeyStatusDetails, attemptedErrorResult, @@ -61,6 +64,66 @@ export const kimiCodeProvider: QuotaProvider = { return withStatusDetails(notAttemptedResult(), authDetails); } + if (diagnostics.source === "opencode.db") { + const credentialRows = (await readCredentialRows()).filter((row) => + ["kimi-for-coding", "kimi-code", "kimi"].includes(row.integrationId), + ); + const rowNames = formatCredentialDisplayNames( + "Kimi Code", + credentialRows.map((row) => ({ row, fallbackName: "Kimi Code" })), + ); + const displayNamesByRowId = new Map( + credentialRows.map((row, index) => [row.id, rowNames[index] ?? "Kimi Code"]), + ); + const invalidErrors: QuotaProviderResult["errors"] = []; + const credentials = credentialRows.flatMap((row) => { + const rowAuth = resolveKimiAuth({ [row.integrationId]: row.value } as AuthData); + if (rowAuth.state === "invalid") { + invalidErrors.push({ label: displayNamesByRowId.get(row.id) ?? "Kimi Code", message: rowAuth.error }); + } + return rowAuth.state === "configured" ? [{ row, auth: rowAuth }] : []; + }); + if (credentials.length > 0 || invalidErrors.length > 0) { + const results = await Promise.all( + credentials.map(async ({ row, auth }) => ({ + row, + result: await queryKimiQuota({ + requestTimeoutMs: ctx.config?.requestTimeoutMs, + apiKey: auth.apiKey, + }), + })), + ); + const entries: QuotaToastEntry[] = []; + const errors: QuotaProviderResult["errors"] = [...invalidErrors]; + for (const { row, result } of results) { + const group = displayNamesByRowId.get(row.id) ?? "Kimi Code"; + if (!result) continue; + if (!result.success) { + errors.push({ label: group, message: result.error }); + continue; + } + entries.push( + ...result.windows.map((window) => ({ + accounting: { + resultType: "quota" as const, + acquisitionMethod: "remote_api" as const, + ownership: "maintained" as const, + authority: "provider_reported" as const, + sourceId: row.id, + }, + name: `${group} ${window.label}`, + group, + label: `${window.label}:`, + right: formatUsageRight(window), + percentRemaining: window.percentRemaining, + resetTimeIso: window.resetTimeIso, + })), + ); + } + return withStatusDetails(attemptedResult(entries, errors), authDetails); + } + } + if (auth.state === "invalid") { return withStatusDetails(attemptedErrorResult("Kimi Code", auth.error), authDetails); } diff --git a/src/providers/minimax-coding-plan.ts b/src/providers/minimax-coding-plan.ts index 529d02ef..9f2d7757 100644 --- a/src/providers/minimax-coding-plan.ts +++ b/src/providers/minimax-coding-plan.ts @@ -18,16 +18,19 @@ import { getMiniMaxChinaAuthDiagnostics, type MiniMaxAuthDiagnostics, type ResolvedMiniMaxAuth, + resolveMiniMaxAuth, resolveMiniMaxAuthCached, + resolveMiniMaxChinaAuth, resolveMiniMaxChinaAuthCached, } from "../lib/minimax-auth.js"; import { getMiniMaxQuotaEndpoint, type MiniMaxQuotaEndpointId } from "../lib/minimax-endpoints.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { isAnyProviderIdAvailable, isCanonicalProviderAvailable, } from "../lib/provider-availability.js"; import { normalizeQuotaProviderId } from "../lib/provider-metadata.js"; -import type { MiniMaxResult, MiniMaxResultEntry } from "../lib/types.js"; +import type { AuthData, MiniMaxResult, MiniMaxResultEntry } from "../lib/types.js"; import { apiKeyStatusDetails, attemptedErrorResult, @@ -318,6 +321,8 @@ type MiniMaxProviderSpec = { label: string; endpoint: MiniMaxQuotaEndpointId; resolveAuthCached: (params?: { maxAgeMs?: number }) => Promise; + resolveCredentialAuth: (auth: AuthData) => ResolvedMiniMaxAuth; + credentialIntegrationIds: readonly string[]; getAuthDiagnostics: (params?: { maxAgeMs?: number }) => Promise; }; @@ -412,6 +417,65 @@ function createMiniMaxProvider(spec: MiniMaxProviderSpec): QuotaProvider { return withStatusDetails(notAttemptedResult(), statusDetails); } + if (diagnostics.source === "opencode.db") { + const credentialRows = (await readCredentialRows()).filter((row) => + spec.credentialIntegrationIds.includes(row.integrationId), + ); + const rowNames = formatCredentialDisplayNames( + spec.label, + credentialRows.map((row) => ({ row, fallbackName: spec.label })), + ); + const displayNamesByRowId = new Map( + credentialRows.map((row, index) => [row.id, rowNames[index] ?? spec.label]), + ); + const invalidErrors: QuotaProviderResult["errors"] = []; + const credentials = credentialRows.flatMap((row) => { + const rowAuth = spec.resolveCredentialAuth({ + [row.integrationId]: row.value, + } as AuthData); + if (rowAuth.state === "invalid") { + invalidErrors.push({ label: displayNamesByRowId.get(row.id) ?? spec.label, message: rowAuth.error }); + } + return rowAuth.state === "configured" ? [{ row, auth: rowAuth }] : []; + }); + if (credentials.length > 0 || invalidErrors.length > 0) { + const results = await Promise.all( + credentials.map(async ({ row, auth }) => ({ + row, + result: await queryMiniMaxQuota(auth.apiKey, { + endpoint: spec.endpoint, + label: spec.label, + requestTimeoutMs: ctx.config?.requestTimeoutMs, + }), + })), + ); + const entries: QuotaProviderResult["entries"] = []; + const errors: QuotaProviderResult["errors"] = [...invalidErrors]; + for (const { row, result } of results) { + const group = displayNamesByRowId.get(row.id) ?? spec.label; + if (!result.success) { + errors.push({ label: group, message: result.error }); + continue; + } + entries.push( + ...result.entries.map(({ window: _window, ...entry }) => ({ + ...entry, + name: entry.name.replace(spec.label, group), + group, + accounting: { + resultType: "quota" as const, + acquisitionMethod: "remote_api" as const, + ownership: "maintained" as const, + authority: "provider_reported" as const, + sourceId: row.id, + }, + })), + ); + } + return withStatusDetails(attemptedResult(entries, errors), statusDetails); + } + } + if (auth.state === "invalid") { return withStatusDetails(attemptedErrorResult(spec.label, auth.error), statusDetails); } @@ -464,6 +528,8 @@ export const minimaxCodingPlanProvider: QuotaProvider = createMiniMaxProvider({ label: MINIMAX_PROVIDER_LABEL, endpoint: "international", resolveAuthCached: resolveMiniMaxAuthCached, + resolveCredentialAuth: resolveMiniMaxAuth, + credentialIntegrationIds: ["minimax-coding-plan"], getAuthDiagnostics: getMiniMaxAuthDiagnostics, }); @@ -472,5 +538,7 @@ export const minimaxChinaCodingPlanProvider: QuotaProvider = createMiniMaxProvid label: MINIMAX_CHINA_PROVIDER_LABEL, endpoint: "china", resolveAuthCached: resolveMiniMaxChinaAuthCached, + resolveCredentialAuth: resolveMiniMaxChinaAuth, + credentialIntegrationIds: ["minimax-china-coding-plan", "minimax-cn-coding-plan"], getAuthDiagnostics: getMiniMaxChinaAuthDiagnostics, }); diff --git a/src/providers/openai.ts b/src/providers/openai.ts index a4a688b8..a9af054d 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -10,9 +10,10 @@ import { queryOpenAIQuota, resolveOpenAIOAuth, } from "../lib/openai.js"; -import { readAuthFileCached } from "../lib/opencode-auth.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import { modelProviderIncludesAny } from "../lib/provider-model-matching.js"; +import type { AuthData } from "../lib/types.js"; import { attemptedResult, groupedPercentWindowEntries, @@ -44,48 +45,87 @@ export const openaiProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { - const auth = resolveOpenAIOAuth(await readAuthFileCached({ maxAgeMs: 5_000 })); - const result = await queryOpenAIQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs }); - const providerResult = mapNullableProviderResult(result, { - errorLabel: "OpenAI", - onSuccess: (result) => - attemptedResult( - groupedPercentWindowEntries({ - group: result.label, - accounting: { - resultType: "rate_limit", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", - }, - windows: [ - { window: result.windows.hourly, suffix: "5h", label: "5h:" }, - { window: result.windows.weekly, suffix: "Weekly", label: "Weekly:" }, - { window: result.windows.monthly, suffix: "Monthly", label: "Monthly:" }, - { window: result.windows.codeReview, suffix: "Code Review", label: "Code Review:" }, - ], - }), - [], - { - singleWindowDisplayName: result.label, - }, - ), + const rows = (await readCredentialRows()).filter((row) => + ["openai", "codex", "chatgpt", "opencode"].includes(row.integrationId), + ); + const credentials = rows.flatMap((row) => { + const auth = resolveOpenAIOAuth({ [row.integrationId]: row.value } as AuthData); + return auth.state === "configured" ? [{ row, auth }] : []; }); - const configured = auth.state === "configured"; - const expiresAt = configured ? auth.expiresAt : undefined; + const entries: QuotaProviderResult["entries"] = []; + const errors: QuotaProviderResult["errors"] = []; + const mapResult = ( + result: Awaited>, + options: { group?: string; sourceId?: string } = {}, + ) => + mapNullableProviderResult(result, { + errorLabel: options.group ?? "OpenAI", + onSuccess: (result) => { + const group = options.group ?? result.label; + return attemptedResult( + groupedPercentWindowEntries({ + group, + accounting: { + resultType: "rate_limit", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + ...(options.sourceId ? { sourceId: options.sourceId } : {}), + }, + windows: [ + { window: result.windows.hourly, suffix: "5h", label: "5h:" }, + { window: result.windows.weekly, suffix: "Weekly", label: "Weekly:" }, + { window: result.windows.monthly, suffix: "Monthly", label: "Monthly:" }, + { window: result.windows.codeReview, suffix: "Code Review", label: "Code Review:" }, + ], + }), + [], + { singleWindowDisplayName: group }, + ); + }, + }); + const results = await Promise.all( + credentials.map(async ({ row, auth }) => ({ + row, + auth, + result: await queryOpenAIQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs, auth }), + })), + ); + const names = formatCredentialDisplayNames( + "OpenAI", + results.map(({ row, result }) => ({ + row, + fallbackName: result?.success ? result.label : "OpenAI", + })), + ); + + for (const [index, { row, result }] of results.entries()) { + const providerResult = mapResult(result, { group: names[index], sourceId: row.id }); + entries.push(...providerResult.entries); + errors.push(...providerResult.errors); + } + const providerResult = + entries.length > 0 || errors.length > 0 + ? attemptedResult(entries, errors) + : mapResult(await queryOpenAIQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs })); + const configuredAuth = credentials[0]?.auth; + const configured = configuredAuth !== undefined; + const expiresAt = configuredAuth?.expiresAt; return withStatusDetails( providerResult, statusDetailsFromRecord({ auth_configured: configured ? "true" : "false", - auth_source: configured ? auth.sourceKey : "(none)", + auth_source: configuredAuth?.sourceKey ?? "(none)", token_status: !configured ? "(none)" : expiresAt && expiresAt < Date.now() ? "expired" : "valid", token_expires_at: expiresAt ? new Date(expiresAt).toISOString() : "(none)", - account_email: configured && auth.email ? sanitizeDisplayText(auth.email) : "(none)", - account_id: configured && auth.accountId ? sanitizeDisplayText(auth.accountId) : "(none)", + account_email: configuredAuth?.email ? sanitizeDisplayText(configuredAuth.email) : "(none)", + account_id: configuredAuth?.accountId + ? sanitizeDisplayText(configuredAuth.accountId) + : "(none)", }), ); }, diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index 023162d7..7f422fd3 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -5,11 +5,13 @@ import type { QuotaProviderStatusDetail, QuotaToastEntry, } from "../lib/entries.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { queryOpenCodeGoQuota } from "../lib/opencode-go.js"; import { DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, getOpenCodeGoAuthDiagnostics, type OpenCodeGoAuthDiagnostics, + resolveOpenCodeGoAuth, resolveOpenCodeGoAuthCached, } from "../lib/opencode-go-auth.js"; import { normalizeQuotaProviderId } from "../lib/provider-metadata.js"; @@ -43,6 +45,8 @@ function authStatusDetails(diagnostics: OpenCodeGoAuthDiagnostics): QuotaProvide function buildOpenCodeGoEntries( result: Extract, selectedWindows: OpenCodeGoWindowKey[], + group = OPENCODE_GO_PROVIDER_LABEL, + sourceId?: string, ): QuotaToastEntry[] { const selected = new Set(selectedWindows); const entries: QuotaToastEntry[] = []; @@ -58,9 +62,10 @@ function buildOpenCodeGoEntries( acquisitionMethod: "remote_api", ownership: "maintained", authority: "provider_reported", + ...(sourceId ? { sourceId } : {}), }, - name: labels.name, - group: OPENCODE_GO_PROVIDER_LABEL, + name: `${group} ${labels.label.slice(0, -1)}`, + group, label: labels.label, percentRemaining: usage.percentRemaining, resetTimeIso: usage.resetTimeIso, @@ -102,6 +107,46 @@ export const opencodeGoProvider: QuotaProvider = { return withStatusDetails(notAttemptedResult(), statusDetails); } + if (diagnostics.source === "opencode.db") { + const credentialRows = (await readCredentialRows()).filter((row) => + ["opencode-go", "opencode"].includes(row.integrationId), + ); + const rowNames = formatCredentialDisplayNames( + OPENCODE_GO_PROVIDER_LABEL, + credentialRows.map((row) => ({ row, fallbackName: OPENCODE_GO_PROVIDER_LABEL })), + ); + const displayNamesByRowId = new Map( + credentialRows.map((row, index) => [row.id, rowNames[index] ?? OPENCODE_GO_PROVIDER_LABEL]), + ); + const invalidErrors: QuotaProviderResult["errors"] = []; + const credentials = credentialRows.flatMap((row) => { + const rowAuth = resolveOpenCodeGoAuth({ [row.integrationId]: row.value }); + if (rowAuth.state === "invalid") { + invalidErrors.push({ label: displayNamesByRowId.get(row.id) ?? OPENCODE_GO_PROVIDER_LABEL, message: rowAuth.error }); + } + return rowAuth.state === "configured" ? [{ row, auth: rowAuth }] : []; + }); + if (credentials.length > 0 || invalidErrors.length > 0) { + const results = await Promise.all( + credentials.map(async ({ row, auth }) => ({ + row, + result: await queryOpenCodeGoQuota(auth.apiKey, { + requestTimeoutMs: ctx.config.requestTimeoutMs, + }), + })), + ); + const entries: QuotaToastEntry[] = []; + const errors: QuotaProviderResult["errors"] = [...invalidErrors]; + for (const { row, result } of results) { + const group = displayNamesByRowId.get(row.id) ?? OPENCODE_GO_PROVIDER_LABEL; + if (result.success) + entries.push(...buildOpenCodeGoEntries(result, windows, group, row.id)); + else errors.push({ label: group, message: result.error, retryable: result.retryable }); + } + return withStatusDetails(attemptedResult(entries, errors), statusDetails); + } + } + if (auth.state === "invalid") { return withStatusDetails( attemptedErrorResult(OPENCODE_GO_PROVIDER_LABEL, auth.error), diff --git a/src/providers/xai.ts b/src/providers/xai.ts index e71f9203..fe697f6b 100644 --- a/src/providers/xai.ts +++ b/src/providers/xai.ts @@ -8,13 +8,16 @@ import type { QuotaProviderMatchContext, QuotaProviderResult, } from "../lib/entries.js"; +import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import { modelProviderMatchesRuntimeId } from "../lib/provider-model-matching.js"; +import type { AuthData } from "../lib/types.js"; import { DEFAULT_XAI_AUTH_CACHE_MAX_AGE_MS, hasXaiOAuthCached, periodKindLabel, queryXaiQuota, + resolveXaiOAuth, } from "../lib/xai.js"; import { attemptedResult, mapNullableProviderResult } from "./result-helpers.js"; @@ -40,6 +43,56 @@ export const xaiProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { + const credentials = (await readCredentialRows()).flatMap((row) => { + if (row.integrationId !== "xai") return []; + const auth = resolveXaiOAuth({ xai: row.value } as AuthData); + return auth.state === "configured" ? [{ row, auth }] : []; + }); + if (credentials.length > 0) { + const results = await Promise.all( + credentials.map(async ({ row, auth }) => ({ + row, + result: await queryXaiQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs, auth }), + })), + ); + const names = formatCredentialDisplayNames( + "xAI", + results.map(({ row, result }) => ({ + row, + fallbackName: result?.success ? result.label : "xAI", + })), + ); + const entries: QuotaProviderResult["entries"] = []; + const errors: QuotaProviderResult["errors"] = []; + for (const [index, { row, result }] of results.entries()) { + const group = names[index] ?? "xAI"; + const mapped = mapNullableProviderResult(result, { + errorLabel: group, + onSuccess: (result) => { + const period = periodKindLabel(result.window.kind); + return attemptedResult([ + { + accounting: { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + sourceId: row.id, + }, + name: `${group} ${period}`, + group, + label: `${period}:`, + percentRemaining: result.window.percentRemaining, + resetTimeIso: result.window.resetTimeIso, + }, + ]); + }, + }); + entries.push(...mapped.entries); + errors.push(...mapped.errors); + } + return attemptedResult(entries, errors); + } const result = await queryXaiQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs }); return mapNullableProviderResult(result, { diff --git a/src/providers/zai.ts b/src/providers/zai.ts index 25d2a9e7..0201a13f 100644 --- a/src/providers/zai.ts +++ b/src/providers/zai.ts @@ -3,6 +3,7 @@ import { queryZaiQuota } from "../lib/zai.js"; import { DEFAULT_ZAI_AUTH_CACHE_MAX_AGE_MS, getZaiAuthDiagnostics, + resolveZaiAuth, resolveZaiAuthCached, } from "../lib/zai-auth.js"; import { createGlmCodingPlanProvider } from "./glm-coding-plan-provider.js"; @@ -13,6 +14,8 @@ export const zaiProvider = createGlmCodingPlanProvider({ errorLabel: "Z.ai", authCacheMaxAgeMs: DEFAULT_ZAI_AUTH_CACHE_MAX_AGE_MS, resolveAuth: resolveZaiAuthCached, + resolveCredentialAuth: resolveZaiAuth, + credentialIntegrationIds: ["zai-coding-plan"], getAuthDiagnostics: getZaiAuthDiagnostics, queryQuota: queryZaiQuota, matchesCurrentModel(model) { diff --git a/src/providers/zhipu.ts b/src/providers/zhipu.ts index 96824785..495330d4 100644 --- a/src/providers/zhipu.ts +++ b/src/providers/zhipu.ts @@ -3,6 +3,7 @@ import { queryZhipuQuota } from "../lib/zhipu.js"; import { DEFAULT_ZHIPU_AUTH_CACHE_MAX_AGE_MS, getZhipuAuthDiagnostics, + resolveZhipuAuth, resolveZhipuAuthCached, } from "../lib/zhipu-auth.js"; import { createGlmCodingPlanProvider } from "./glm-coding-plan-provider.js"; @@ -13,6 +14,8 @@ export const zhipuProvider = createGlmCodingPlanProvider({ errorLabel: "Zhipu", authCacheMaxAgeMs: DEFAULT_ZHIPU_AUTH_CACHE_MAX_AGE_MS, resolveAuth: resolveZhipuAuthCached, + resolveCredentialAuth: resolveZhipuAuth, + credentialIntegrationIds: ["zhipu-coding-plan", "zhipuai-coding-plan"], getAuthDiagnostics: getZhipuAuthDiagnostics, queryQuota: queryZhipuQuota, matchesCurrentModel(model) { diff --git a/tests/format.test.ts b/tests/format.test.ts index b3daa6a6..17f1369b 100644 --- a/tests/format.test.ts +++ b/tests/format.test.ts @@ -758,9 +758,9 @@ describe("formatQuotaRows", () => { ], }); - expect(out.indexOf("Five-hour")).toBeGreaterThanOrEqual(0); + expect(out.indexOf("5h")).toBeGreaterThanOrEqual(0); expect(out.indexOf("Weekly")).toBeGreaterThanOrEqual(0); - expect(out.indexOf("Five-hour")).toBeLessThan(out.indexOf("Weekly")); + expect(out.indexOf("5h")).toBeLessThan(out.indexOf("Weekly")); }); it("renders all-window percent rows as used when percentDisplayMode is used", () => { @@ -804,7 +804,7 @@ describe("formatQuotaRows", () => { ], }); - expect(out).toContain("Five-hour"); + expect(out).toContain("5h"); expect(out).not.toContain("0/135"); expect(out).toContain("100% left"); }); @@ -847,7 +847,7 @@ describe("formatQuotaRows", () => { expect(out.indexOf("[Qwen] (free)")).toBeLessThan(out.indexOf("[OpenAI] (Pro)")); expect(out.indexOf("RPM")).toBeLessThan(out.indexOf("Daily")); - expect(out.indexOf("Five-hour")).toBeLessThan(out.indexOf("Weekly")); + expect(out.indexOf("5h")).toBeLessThan(out.indexOf("Weekly")); }); it("preserves explicit legacy Google-style labels and only falls back for unlabeled rows", () => { diff --git a/tests/lib.accounting-format.test.ts b/tests/lib.accounting-format.test.ts index 5ab0f478..cc9fc465 100644 --- a/tests/lib.accounting-format.test.ts +++ b/tests/lib.accounting-format.test.ts @@ -123,7 +123,7 @@ describe("accounting-format", () => { it("owns provider-neutral labels including typed windows and components", () => { expect(formatAccountingWindowLabel("rpm")).toBe("RPM"); - expect(formatAccountingWindowLabel("five_hour")).toBe("Five-hour"); + expect(formatAccountingWindowLabel("five_hour")).toBe("5h"); expect(formatAccountingWindowLabel("code_review")).toBe("Code Review"); expect( formatAccountingSemanticLabel( diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 6893609a..a785e7fd 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -8,7 +8,9 @@ import { clearReadAuthFileCacheForTests, getCredentialDatabasePath, getCredentialDatabasePaths, + formatCredentialDisplayNames, readAuthFile, + readCredentialRows, } from "../src/lib/opencode-auth.js"; const temporaryDirectories: string[] = []; @@ -76,6 +78,34 @@ async function createCredentialDatabase(): Promise<{ dataDir: string; databasePa } describe("OpenCode auth reader", () => { + it.each([ + ["OpenAI", "OpenAI (Pro)", "default", true, "[OpenAI] (Pro)*"], + ["OpenAI", "OpenAI (Pro)", "SEPD", true, "[OpenAI SEPD] (Pro)*"], + ["Copilot", "Copilot (business)", "sita", true, "[Copilot sita] (business)*"], + ["xAI", "xAI SuperGrok", "personal", true, "[xAI personal] (SuperGrok)*"], + ["OpenAI", "OpenAI (Pro)", " ", true, "[OpenAI] (Pro)*"], + ["OpenAI", "OpenAI (Pro)", "openai", true, "[OpenAI] (Pro)*"], + ])("formats %s credential aliases as %s", (providerName, fallbackName, label, active, expected) => { + expect( + formatCredentialDisplayNames(providerName, [ + { + row: { id: "credential", integrationId: providerName, label, active, value: {} }, + fallbackName, + }, + ]), + ).toEqual([expected]); + }); + + it("numbers duplicate custom aliases without numbering default credentials", () => { + expect( + formatCredentialDisplayNames("OpenAI", [ + { row: { id: "a", integrationId: "openai", label: "SEPD", active: true, value: {} }, fallbackName: "OpenAI (Pro)" }, + { row: { id: "b", integrationId: "openai", label: "SEPD", active: false, value: {} }, fallbackName: "OpenAI (Pro)" }, + { row: { id: "c", integrationId: "openai", label: "default", active: false, value: {} }, fallbackName: "OpenAI (Pro)" }, + ]), + ).toEqual(["[OpenAI SEPD] (Pro)*", "[OpenAI SEPD 2] (Pro)", "[OpenAI] (Pro)"]); + }); + it("reports the credential database path and honors OPENCODE_DB", async () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); @@ -98,6 +128,41 @@ describe("OpenCode auth reader", () => { }); }); + it("exposes every credential row with active rows first", async () => { + const { databasePath } = await createCredentialDatabase(); + const database = new DatabaseSync(databasePath); + database.prepare("UPDATE credential SET label = 'Work', active = 0 WHERE id = 'openai'").run(); + database + .prepare("INSERT INTO credential VALUES (?, ?, ?, ?, NULL, NULL, ?, ?, ?)") + .run( + "openai-active", + "openai", + "Personal", + JSON.stringify({ type: "oauth", access: "personal-access" }), + 1, + 4, + 4, + ); + database.close(); + vi.stubEnv("OPENCODE_DB", databasePath); + + await expect(readCredentialRows()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "openai-active", + integrationId: "openai", + label: "Personal", + active: true, + value: expect.objectContaining({ access: "personal-access" }), + }), + expect.objectContaining({ id: "openai", active: false }), + ]), + ); + expect( + (await readCredentialRows()).findIndex((row) => row.id === "openai-active"), + ).toBeLessThan((await readCredentialRows()).findIndex((row) => row.id === "openai")); + }); + it("ignores legacy auth.json entries", async () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); diff --git a/tests/lib.opencode-go-auth.test.ts b/tests/lib.opencode-go-auth.test.ts index 6385b260..fa09ec87 100644 --- a/tests/lib.opencode-go-auth.test.ts +++ b/tests/lib.opencode-go-auth.test.ts @@ -16,6 +16,8 @@ import { const authMocks = vi.hoisted(() => ({ getCredentialDatabasePaths: vi.fn(() => ["/tmp/opencode.db"]), readAuthFileCached: vi.fn(), + readCredentialRows: vi.fn(async () => []), + formatCredentialDisplayNames: vi.fn(), queryOpenCodeGoQuota: vi.fn(), readFile: vi.fn(), lstat: vi.fn(), @@ -36,6 +38,8 @@ vi.mock("node:fs/promises", async (importOriginal) => ({ vi.mock("../src/lib/opencode-auth.js", () => ({ getCredentialDatabasePaths: authMocks.getCredentialDatabasePaths, readAuthFileCached: authMocks.readAuthFileCached, + readCredentialRows: authMocks.readCredentialRows, + formatCredentialDisplayNames: authMocks.formatCredentialDisplayNames, })); vi.mock("../src/lib/opencode-go.js", () => ({ queryOpenCodeGoQuota: authMocks.queryOpenCodeGoQuota, diff --git a/tests/provider-surfaces.google-agy.test.ts b/tests/provider-surfaces.google-agy.test.ts index 999ee298..f8d12c6a 100644 --- a/tests/provider-surfaces.google-agy.test.ts +++ b/tests/provider-surfaces.google-agy.test.ts @@ -143,8 +143,8 @@ describe("Google AGY provider surfaces", () => { for (const output of [toast, sidebar]) { for (const header of headers) expect(output).toContain(header); expect(output).toContain("Weekly"); - expect(output).toContain("Five-hour"); - expect(output.indexOf("Weekly")).toBeLessThan(output.indexOf("Five-hour")); + expect(output).toContain("5h"); + expect(output.indexOf("Weekly")).toBeLessThan(output.indexOf("5h")); } for (const header of headers) expect(command).toContain(header); expect(command.indexOf("Week quota")).toBeLessThan(command.indexOf("5h quota")); diff --git a/tests/provider-surfaces.google-antigravity.test.ts b/tests/provider-surfaces.google-antigravity.test.ts index bf38f86e..61c32389 100644 --- a/tests/provider-surfaces.google-antigravity.test.ts +++ b/tests/provider-surfaces.google-antigravity.test.ts @@ -221,7 +221,7 @@ describe("Google Antigravity provider surfaces", () => { expect(output).toContain("Antigravity (bob…)"); } expect(command.match(/\n {2}Quota\s/gu)).toHaveLength(2); - expect(toast).not.toMatch(/\n(?:5h|7d|Weekly|Five-hour)\s/u); + expect(toast).not.toMatch(/\n(?:5h|7d|Weekly)\s/u); expect(surfaces.sidebar.status).toBe("ready"); expect(sidebar.match(/\nQuota\s/gu)).toHaveLength(2); expect(surfaces.compact.status).toBe("ready"); diff --git a/tests/providers.anthropic.test.ts b/tests/providers.anthropic.test.ts index 212afcf5..d395748c 100644 --- a/tests/providers.anthropic.test.ts +++ b/tests/providers.anthropic.test.ts @@ -12,9 +12,59 @@ vi.mock("../src/lib/anthropic.js", () => ({ getAnthropicDiagnostics: vi.fn(), hasAnthropicCredentialsConfigured: vi.fn(), queryAnthropicQuota: vi.fn(), + queryAnthropicQuotaWithOAuth: vi.fn(), +})); + +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: vi.fn().mockResolvedValue([]), })); describe("anthropic provider", () => { + it("fans out to a valid inactive database credential when the active credential is expired", async () => { + const { getAnthropicDiagnostics, queryAnthropicQuota, queryAnthropicQuotaWithOAuth } = + await import("../src/lib/anthropic.js"); + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "expired-active", + integrationId: "anthropic", + label: "default", + active: true, + value: { type: "oauth", access: "expired", expires: 0 }, + }, + { + id: "valid-inactive", + integrationId: "anthropic", + label: "Work", + active: false, + value: { type: "oauth", access: "valid" }, + }, + ]); + (getAnthropicDiagnostics as any).mockResolvedValueOnce({ + installed: true, + authStatus: "authenticated", + quotaSupported: true, + quotaSource: "claude-auth-status-json", + checkedCommands: [], + quota: { success: true, five_hour: { percentRemaining: 1 }, seven_day: { percentRemaining: 1 } }, + }); + (queryAnthropicQuotaWithOAuth as any).mockResolvedValueOnce({ + success: true, + five_hour: { percentRemaining: 80 }, + seven_day: { percentRemaining: 70 }, + }); + + const out = await anthropicProvider.fetch({} as any); + + expect(queryAnthropicQuotaWithOAuth).toHaveBeenCalledWith("valid", undefined); + expect(queryAnthropicQuota).not.toHaveBeenCalled(); + expect(out.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ + ["[Claude Work]*", "valid-inactive"], + ["[Claude Work]*", "valid-inactive"], + ]); + }); + it("reports the credential store that answered the usage probe", async () => { const { getAnthropicDiagnostics, queryAnthropicQuota } = await import( "../src/lib/anthropic.js" diff --git a/tests/providers.copilot.surfaces.test.ts b/tests/providers.copilot.surfaces.test.ts index 7407b8fa..35f97cb4 100644 --- a/tests/providers.copilot.surfaces.test.ts +++ b/tests/providers.copilot.surfaces.test.ts @@ -27,6 +27,30 @@ const data: QuotaRenderData = { }; describe("Copilot usage-only four-surface formatting", () => { + it.each([ + ["[Copilot] (business)*"], + ["[Copilot sita] (business)*"], + ])("preserves the credential group %s on command, toast, and sidebar", (group) => { + const credentialData: QuotaRenderData = { + ...data, + entries: data.entries.map((entry) => ({ + ...entry, + name: entry.name.replace("Copilot", group), + group, + })), + }; + const outputs = [ + formatQuotaCommand({ ...credentialData, generatedAtMs: 0 }), + formatQuotaRowsGrouped(credentialData), + buildSidebarQuotaPanelLines({ + data: credentialData, + config: { formatStyle: "allWindows", percentDisplayMode: "remaining" }, + }).join("\n"), + ]; + + for (const output of outputs) expect(output).toContain(group); + }); + it("does not invent a remaining percentage or reset on any surface", () => { const web = formatQuotaCommand({ ...data, generatedAtMs: 0 }); const toast = formatQuotaRowsGrouped({ diff --git a/tests/providers.glm-coding-plan.test.ts b/tests/providers.glm-coding-plan.test.ts index fdd8d180..9f253a35 100644 --- a/tests/providers.glm-coding-plan.test.ts +++ b/tests/providers.glm-coding-plan.test.ts @@ -26,6 +26,7 @@ vi.mock("../src/lib/zhipu.js", () => ({ queryZhipuQuota: mocks.queryZhipuQuota } vi.mock("../src/lib/zai-auth.js", () => ({ DEFAULT_ZAI_AUTH_CACHE_MAX_AGE_MS: 5_000, resolveZaiAuthCached: mocks.resolveZaiAuthCached, + resolveZaiAuth: vi.fn(), getZaiAuthDiagnostics: vi.fn(async () => ({ state: "none", source: null, @@ -36,6 +37,7 @@ vi.mock("../src/lib/zai-auth.js", () => ({ vi.mock("../src/lib/zhipu-auth.js", () => ({ DEFAULT_ZHIPU_AUTH_CACHE_MAX_AGE_MS: 5_000, resolveZhipuAuthCached: mocks.resolveZhipuAuthCached, + resolveZhipuAuth: vi.fn(), getZhipuAuthDiagnostics: vi.fn(async () => ({ state: "none", source: null, @@ -238,7 +240,7 @@ describe("Z.ai credit quota surfaces", () => { expect(output).toContain("89%"); } for (const output of [command, toast, sidebar]) { - expect(output).toMatch(/(?:5h|Five-hour)/u); + expect(output).toContain("5h"); expect(output).toMatch(/\bWeek(?:ly)?\b/u); } }); diff --git a/tests/providers.google-db-credentials.test.ts b/tests/providers.google-db-credentials.test.ts new file mode 100644 index 00000000..d66dfbef --- /dev/null +++ b/tests/providers.google-db-credentials.test.ts @@ -0,0 +1,106 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + rows: vi.fn(), + agyQuery: vi.fn(), + geminiQuery: vi.fn(), +})); + +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: mocks.rows, +})); +vi.mock("../src/lib/google-agy.js", () => ({ + hasAgyQuotaRuntimeAvailable: vi.fn(), + inspectAgyAuthPresence: vi.fn(async () => ({ + state: "configured", + sourceKey: "google-agy", + accountCount: 2, + validAccountCount: 2, + })), + queryGoogleAgyQuota: mocks.agyQuery, +})); +vi.mock("../src/lib/google-gemini-cli.js", () => ({ + hasGeminiCliQuotaRuntimeAvailable: vi.fn(), + inspectGeminiCliAuthPresence: vi.fn(async () => ({ + state: "configured", + sourceKey: "google-gemini-cli", + accountCount: 2, + validAccountCount: 2, + })), + queryGeminiCliQuota: mocks.geminiQuery, +})); +vi.mock("../src/lib/google-agy-companion.js", () => ({ + inspectAgyCompanionPresence: vi.fn(async () => ({ state: "present", resolvedPath: "/plugin" })), +})); +vi.mock("../src/lib/google-gemini-cli-companion.js", () => ({ + inspectGeminiCliCompanionPresence: vi.fn(async () => ({ + state: "present", + resolvedPath: "/plugin", + })), +})); + +import { googleAgyProvider } from "../src/providers/google-agy.js"; +import { googleGeminiCliProvider } from "../src/providers/google-gemini-cli.js"; + +describe.each([ + { + name: "Google AGY", + integrationId: "google-agy", + provider: googleAgyProvider, + query: mocks.agyQuery, + result: { + success: true, + buckets: [ + { + family: "Gemini Models", + window: "weekly", + windowLabel: "Weekly", + accountIndex: 0, + percentRemaining: 70, + }, + ], + }, + }, + { + name: "Gemini CLI", + integrationId: "google-gemini-cli", + provider: googleGeminiCliProvider, + query: mocks.geminiQuery, + result: { + success: true, + buckets: [{ displayName: "Gemini 2.5", percentRemaining: 70 }], + }, + }, +])("$name DB credentials", ({ integrationId, provider, query, result }) => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.rows.mockResolvedValue([ + { + id: "active-id", + integrationId, + label: "Work", + active: true, + value: { type: "oauth", refresh: "one|project" }, + }, + { + id: "other-id", + integrationId, + label: "Work", + active: false, + value: { type: "oauth", refresh: "two|project" }, + }, + ]); + query.mockResolvedValue(result); + }); + + it("queries every row without deduping duplicate aliases", async () => { + const output = await provider.fetch({ client: {}, config: {} } as never); + + expect(query).toHaveBeenCalledTimes(2); + expect(output.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ + [`[${provider.id === "google-agy" ? "Google AGY" : "Gemini CLI"} Work]*`, "active-id"], + [`[${provider.id === "google-agy" ? "Google AGY" : "Gemini CLI"} Work 2]`, "other-id"], + ]); + }); +}); diff --git a/tests/providers.kimi-code.test.ts b/tests/providers.kimi-code.test.ts index 01ab39d8..43b6ccfa 100644 --- a/tests/providers.kimi-code.test.ts +++ b/tests/providers.kimi-code.test.ts @@ -8,11 +8,13 @@ import { import { createProviderAvailabilityContext } from "./helpers/provider-test-harness.js"; const authMocks = vi.hoisted(() => ({ + resolveKimiAuth: vi.fn(), resolveKimiAuthCached: vi.fn(), })); vi.mock("../src/lib/kimi-auth.js", () => ({ resolveKimiAuthCached: authMocks.resolveKimiAuthCached, + resolveKimiAuth: authMocks.resolveKimiAuth, getKimiAuthDiagnostics: vi.fn(async () => ({ state: "none", source: null, @@ -30,6 +32,11 @@ vi.mock("../src/lib/provider-availability.js", () => ({ isCanonicalProviderAvailable: vi.fn(), })); +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: vi.fn().mockResolvedValue([]), +})); + import { kimiCodeProvider } from "../src/providers/kimi-code.js"; describe("kimi-code provider", () => { @@ -39,6 +46,7 @@ describe("kimi-code provider", () => { state: "configured", apiKey: "test-key", }); + authMocks.resolveKimiAuth.mockReturnValue({ state: "configured", apiKey: "row-key" }); }); it("returns attempted:false when no kimi auth is configured", async () => { @@ -107,6 +115,32 @@ describe("kimi-code provider", () => { }); }); + it("keeps a valid inactive duplicate-label database credential when the active row is invalid", async () => { + const { getKimiAuthDiagnostics } = await import("../src/lib/kimi-auth.js"); + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + const { queryKimiQuota } = await import("../src/lib/kimi.js"); + (getKimiAuthDiagnostics as any).mockResolvedValueOnce({ state: "invalid", source: "opencode.db", checkedPaths: [], credentialDatabasePaths: [] }); + (readCredentialRows as any).mockResolvedValueOnce([ + { id: "bad", integrationId: "kimi", label: "shared", active: true, value: { key: "" } }, + { id: "good", integrationId: "kimi", label: "shared", active: false, value: { key: "ok" } }, + ]); + authMocks.resolveKimiAuth.mockImplementation((auth: any) => + auth.kimi.key ? { state: "configured", apiKey: "row-key" } : { state: "invalid", error: "empty key" }, + ); + (queryKimiQuota as any).mockResolvedValueOnce({ + success: true, + label: "Kimi Code", + windows: [{ label: "5h", used: 1, limit: 10, percentRemaining: 90 }], + }); + + const out = await kimiCodeProvider.fetch({ config: {} } as any); + expect(out.errors).toContainEqual({ label: "[Kimi Code shared]*", message: "empty key" }); + expect(out.entries).toContainEqual( + expect.objectContaining({ group: "[Kimi Code shared 2]", accounting: expect.objectContaining({ sourceId: "good" }) }), + ); + expect(queryKimiQuota).toHaveBeenCalledOnce(); + }); + it("maps errors into toast errors", async () => { const { queryKimiQuota } = await import("../src/lib/kimi.js"); (queryKimiQuota as any).mockResolvedValueOnce({ diff --git a/tests/providers.minimax-coding-plan.test.ts b/tests/providers.minimax-coding-plan.test.ts index c0144f94..c2939770 100644 --- a/tests/providers.minimax-coding-plan.test.ts +++ b/tests/providers.minimax-coding-plan.test.ts @@ -29,6 +29,8 @@ const mocks = vi.hoisted(() => { }); vi.mock("../src/lib/minimax-auth.js", () => ({ + resolveMiniMaxAuth: vi.fn(), + resolveMiniMaxChinaAuth: vi.fn(), resolveMiniMaxAuthCached: mocks.resolveMiniMaxAuthCached, resolveMiniMaxChinaAuthCached: mocks.resolveMiniMaxChinaAuthCached, getMiniMaxAuthDiagnostics: vi.fn(async () => ({ @@ -55,6 +57,11 @@ vi.mock("../src/lib/provider-availability.js", () => ({ isCanonicalProviderAvailable: mocks.isCanonicalProviderAvailable, })); +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: vi.fn().mockResolvedValue([]), +})); + import { minimaxChinaCodingPlanProvider, minimaxCodingPlanProvider, @@ -159,6 +166,28 @@ describe("minimax-coding-plan provider", () => { expect(out.errors[0]?.message).toBe("Invalid API key"); }); + it("keeps a valid inactive duplicate-label database credential when the active row is invalid", async () => { + const { getMiniMaxAuthDiagnostics, resolveMiniMaxAuth } = await import("../src/lib/minimax-auth.js"); + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + (getMiniMaxAuthDiagnostics as any).mockResolvedValueOnce({ state: "invalid", source: "opencode.db", checkedPaths: [], credentialDatabasePaths: [] }); + mockMiniMaxAuthInvalid("empty key"); + (readCredentialRows as any).mockResolvedValueOnce([ + { id: "bad", integrationId: "minimax-coding-plan", label: "shared", active: true, value: { key: "" } }, + { id: "good", integrationId: "minimax-coding-plan", label: "shared", active: false, value: { key: "ok" } }, + ]); + (resolveMiniMaxAuth as any).mockImplementation((auth: any) => + auth["minimax-coding-plan"].key ? { state: "configured", apiKey: "row-key", endpoint: "international" } : { state: "invalid", error: "empty key" }, + ); + mockMiniMaxHttpSuccess([createCodingPlanModel()]); + + const out = await runProviderFetch(); + expect(out.errors).toContainEqual({ label: "[MiniMax Coding Plan shared]*", message: "empty key" }); + expect(out.entries).toContainEqual( + expect.objectContaining({ group: "[MiniMax Coding Plan shared 2]", accounting: expect.objectContaining({ sourceId: "good" }) }), + ); + expect(out.entries).not.toHaveLength(0); + }); + it("maps MiniMax-M* model to rolling 5h and weekly entries", async () => { mockMiniMaxAuthConfigured(); mockMiniMaxHttpSuccess([createCodingPlanModel({ model_name: "MiniMax-M2.7" })]); diff --git a/tests/providers.openai.test.ts b/tests/providers.openai.test.ts index 78e95141..e772abe2 100644 --- a/tests/providers.openai.test.ts +++ b/tests/providers.openai.test.ts @@ -1,4 +1,8 @@ import { describe, expect, it, vi } from "vitest"; +import { formatQuotaCommand } from "../src/lib/quota-command-format.js"; +import { projectQuotaProviderResults } from "../src/lib/quota-accounting-projection.js"; +import { formatQuotaRowsGrouped } from "../src/lib/toast-format-grouped.js"; +import { buildSidebarQuotaPanelLines } from "../src/lib/tui-sidebar-format.js"; import { openaiProvider } from "../src/providers/openai.js"; import { expectAttemptedWithErrorLabel, @@ -15,6 +19,11 @@ vi.mock("../src/lib/openai.js", () => ({ queryOpenAIQuota: vi.fn(), })); +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: vi.fn().mockResolvedValue([]), +})); + describe("openai provider", () => { it("passes configured requestTimeoutMs to the query", async () => { const { queryOpenAIQuota } = await import("../src/lib/openai.js"); @@ -95,6 +104,111 @@ describe("openai provider", () => { expectAttemptedWithErrorLabel(out, "OpenAI"); }); + it("queries and labels every database credential independently", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + const { queryOpenAIQuota, resolveOpenAIOAuth } = await import("../src/lib/openai.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "active-id", + integrationId: "openai", + label: "Work", + active: true, + value: { type: "oauth", access: "active" }, + }, + { + id: "other-id", + integrationId: "openai", + label: "Work", + active: false, + value: { type: "oauth", access: "other" }, + }, + ]); + (resolveOpenAIOAuth as any).mockImplementation((auth: any) => ({ + state: "configured", + sourceKey: "openai", + accessToken: auth.openai.access, + })); + (queryOpenAIQuota as any).mockResolvedValue({ + success: true, + label: "OpenAI (Business)", + windows: { + hourly: { percentRemaining: 42 }, + weekly: { percentRemaining: 80 }, + monthly: { percentRemaining: 67 }, + codeReview: { percentRemaining: 55 }, + }, + }); + + const out = await openaiProvider.fetch({} as any); + + expect(queryOpenAIQuota).toHaveBeenCalledTimes(2); + expect(out.entries).toHaveLength(8); + expect(out.entries.filter((entry) => entry.label === "5h:")).toHaveLength(2); + expect(projectQuotaProviderResults([out], "allWindows", "summary")).toHaveLength(8); + const singleWindow = projectQuotaProviderResults([out], "singleWindow", "summary"); + expect(singleWindow).toHaveLength(2); + expect(singleWindow.map((entry) => entry.accounting.sourceId)).toEqual([ + "active-id", + "other-id", + ]); + expect( + out.entries.map((entry) => [entry.group, entry.accounting.sourceId]), + ).toEqual([ + ["[OpenAI Work] (Business)*", "active-id"], + ["[OpenAI Work] (Business)*", "active-id"], + ["[OpenAI Work] (Business)*", "active-id"], + ["[OpenAI Work] (Business)*", "active-id"], + ["[OpenAI Work 2] (Business)", "other-id"], + ["[OpenAI Work 2] (Business)", "other-id"], + ["[OpenAI Work 2] (Business)", "other-id"], + ["[OpenAI Work 2] (Business)", "other-id"], + ]); + }); + + it.each([ + ["OpenAI", "[OpenAI] (Business)*"], + ["SEPD", "[OpenAI SEPD] (Business)*"], + ["default", "[OpenAI] (Business)*"], + ])("renders DB alias %s literally on sidebar, CLI, and toast", async (label, expected) => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + const { queryOpenAIQuota, resolveOpenAIOAuth } = await import("../src/lib/openai.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "credential-id", + integrationId: "openai", + label, + active: true, + value: { type: "oauth", access: "token" }, + }, + ]); + (resolveOpenAIOAuth as any).mockReturnValue({ + state: "configured", + sourceKey: "openai", + accessToken: "token", + }); + (queryOpenAIQuota as any).mockResolvedValueOnce({ + success: true, + label: "OpenAI (Business)", + windows: { hourly: { percentRemaining: 42 } }, + }); + + const result = await openaiProvider.fetch({ config: {} } as any); + const data = { entries: result.entries, errors: result.errors }; + const outputs = [ + buildSidebarQuotaPanelLines({ + data, + config: { formatStyle: "allWindows", percentDisplayMode: "remaining" }, + }).join("\n"), + formatQuotaCommand(data), + formatQuotaRowsGrouped(data), + ]; + + for (const output of outputs) { + expect(output).toContain(expected); + expect(output).not.toContain("[OpenAI (OpenAI)*]"); + } + }); + it("is available when provider ids include openai/chatgpt/codex", async () => { const { hasOpenAIOAuthCached } = await import("../src/lib/openai.js"); (hasOpenAIOAuthCached as any).mockResolvedValue(false); diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index 88a7c4c0..bddd4a73 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -10,6 +10,7 @@ import { createProviderAvailabilityContext } from "./helpers/provider-test-harne const mocks = vi.hoisted(() => ({ resolveOpenCodeGoAuthCached: vi.fn(), + resolveOpenCodeGoAuth: vi.fn(), getOpenCodeGoAuthDiagnostics: vi.fn(), queryOpenCodeGoQuota: vi.fn(), })); @@ -18,12 +19,18 @@ vi.mock("../src/lib/opencode-go-auth.js", () => ({ DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS: 5_000, resolveOpenCodeGoAuthCached: mocks.resolveOpenCodeGoAuthCached, getOpenCodeGoAuthDiagnostics: mocks.getOpenCodeGoAuthDiagnostics, + resolveOpenCodeGoAuth: mocks.resolveOpenCodeGoAuth, })); vi.mock("../src/lib/opencode-go.js", () => ({ queryOpenCodeGoQuota: mocks.queryOpenCodeGoQuota, })); +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: vi.fn().mockResolvedValue([]), +})); + import { opencodeGoProvider } from "../src/providers/opencode-go.js"; function successfulResult() { @@ -82,6 +89,7 @@ describe("opencode-go provider", () => { apiKey: "provider-test-token", }); mocks.queryOpenCodeGoQuota.mockResolvedValue(successfulResult()); + mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "row-token" }); }); it("returns not attempted for absent auth without calling the API", async () => { @@ -114,6 +122,27 @@ describe("opencode-go provider", () => { expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); }); + it("keeps a valid inactive duplicate-label database credential when the active row is invalid", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValueOnce(diagnostics("invalid")); + mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ state: "invalid", error: "empty key" }); + (readCredentialRows as any).mockResolvedValueOnce([ + { id: "bad", integrationId: "opencode-go", label: "shared", active: true, value: { key: "" } }, + { id: "good", integrationId: "opencode-go", label: "shared", active: false, value: { key: "ok" } }, + ]); + mocks.resolveOpenCodeGoAuth.mockImplementation((auth: any) => + auth["opencode-go"].key ? { state: "configured", apiKey: "row-token" } : { state: "invalid", error: "empty key" }, + ); + + const out = await runFetch(); + expect(out.errors).toContainEqual({ label: "[OpenCode Go shared]*", message: "empty key" }); + expect(out.entries).toContainEqual( + expect.objectContaining({ group: "[OpenCode Go shared 2]", accounting: expect.objectContaining({ sourceId: "good" }) }), + ); + expect(out.entries).toHaveLength(3); + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + }); + it("passes the resolved token and effective timeout to the API client", async () => { await runFetch(["rolling", "weekly", "monthly"], 12_345); diff --git a/tests/providers.xai.surfaces.test.ts b/tests/providers.xai.surfaces.test.ts index 1ad1fec4..cfeed28c 100644 --- a/tests/providers.xai.surfaces.test.ts +++ b/tests/providers.xai.surfaces.test.ts @@ -29,6 +29,32 @@ function renderDataForLabel(label: (typeof xaiLabels)[number]): QuotaRenderData } describe("xAI four-surface formatting", () => { + it.each([ + ["[xAI] (SuperGrok)*"], + ["[xAI personal] (SuperGrok)*"], + ])("preserves the credential group %s on command, toast, and sidebar", (group) => { + const data: QuotaRenderData = { + entries: [ + { + ...renderDataForLabel("xAI SuperGrok").entries[0]!, + name: `${group} Weekly`, + group, + }, + ], + errors: [], + }; + const outputs = [ + formatQuotaCommand({ ...data, generatedAtMs: 0 }), + formatQuotaRowsGrouped(data), + buildSidebarQuotaPanelLines({ + data, + config: { formatStyle: "allWindows", percentDisplayMode: "remaining" }, + }).join("\n"), + ]; + + for (const output of outputs) expect(output).toContain(group); + }); + it.each( xaiLabels, )("shows the %s weekly quota in command, toast, sidebar, and compact output", (label) => { diff --git a/tests/tui-sidebar-format.test.ts b/tests/tui-sidebar-format.test.ts index 5a67ce5c..4061823d 100644 --- a/tests/tui-sidebar-format.test.ts +++ b/tests/tui-sidebar-format.test.ts @@ -257,9 +257,9 @@ describe("buildSidebarQuotaPanelLines", () => { }, }); - expect(lines.findIndex((line) => line.includes("Five-hour"))).toBeGreaterThanOrEqual(0); + expect(lines.findIndex((line) => line.includes("5h"))).toBeGreaterThanOrEqual(0); expect(lines.findIndex((line) => line.includes("Weekly"))).toBeGreaterThanOrEqual(0); - expect(lines.findIndex((line) => line.includes("Five-hour"))).toBeLessThan( + expect(lines.findIndex((line) => line.includes("5h"))).toBeLessThan( lines.findIndex((line) => line.includes("Weekly")), ); }); diff --git a/tests/tui-smoke.test.ts b/tests/tui-smoke.test.ts index f0e4728d..935e9c6d 100644 --- a/tests/tui-smoke.test.ts +++ b/tests/tui-smoke.test.ts @@ -1099,10 +1099,10 @@ describe("tui plugin smoke", () => { loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ sidebar: { status: "ready", - lines: ["OpenCode Go Five-hour 98%"], + lines: ["OpenCode Go 5h 98%"], linesExpanded: [ "[OpenCode Go]", - "Five-hour window 98%", + "5h window 98%", "Weekly window 53%", "Monthly window 33%", ], @@ -1143,7 +1143,7 @@ describe("tui plugin smoke", () => { expect(collapsedHeader.props.children[1].props.children).toEqual([" (", 2, " providers)"]); expect( collapsed.props.children[1].props.children.map((line: any) => line.props.children), - ).toEqual(["OpenCode Go Five-hour 98%"]); + ).toEqual(["OpenCode Go 5h 98%"]); collapsedHeader.props.children[0].props.onMouseDown(); @@ -1157,7 +1157,7 @@ describe("tui plugin smoke", () => { expect(expandedHeader.props.children[0].props.children.props.children).toBe("▼ Quota"); expect( expanded.props.children[1].props.children.map((line: any) => line.props.children), - ).toEqual(["[OpenCode Go]", "Five-hour window 98%", "Weekly window 53%", "Monthly window 33%"]); + ).toEqual(["[OpenCode Go]", "5h window 98%", "Weekly window 53%", "Monthly window 33%"]); }); it("keeps non-expandable empty sidebar panels visible while collapsed", async () => { diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 3e72acd8..c3ebba3d 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -94,6 +94,8 @@ vi.mock("../src/lib/alibaba-auth.js", () => createAlibabaAuthModuleMock(mocks.resolveAlibabaCodingPlanAuthCached), ); vi.mock("../src/lib/minimax-auth.js", () => ({ + resolveMiniMaxAuth: vi.fn(), + resolveMiniMaxChinaAuth: vi.fn(), DEFAULT_MINIMAX_AUTH_CACHE_MAX_AGE_MS: 5_000, resolveMiniMaxAuthCached: mocks.resolveMiniMaxAuthCached, getMiniMaxAuthDiagnostics: mocks.getMiniMaxAuthDiagnostics, @@ -692,7 +694,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { }); const toastOutput = getToastMessage(client); expect(toastOutput).toContain("MiniMax Coding Plan"); - expect(toastOutput).toContain("Five-hour"); + expect(toastOutput).toContain("5h"); expect(toastOutput).toContain("Weekly"); expect(toastOutput).toContain("35%"); expect(toastOutput).toContain("80%"); @@ -717,7 +719,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { ...(surfaces.sidebar.linesExpanded ?? []), ].join("\n"); expect(sidebarOutput).toContain("MiniMax Coding Plan"); - expect(sidebarOutput).toContain("Five-hour"); + expect(sidebarOutput).toContain("5h"); expect(sidebarOutput).toContain("Weekly"); expect(sidebarOutput).toContain("35%"); expect(sidebarOutput).toContain("80%"); From d5c19554eb759159f81ccbab3856fcdac0ca6e66 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Thu, 27 Aug 2026 18:46:17 +0200 Subject: [PATCH 13/97] fix(copilot): Show reported subscription plan Use the plan returned by GitHub for user-scoped Copilot quota presentation instead of inferring a personal license from the billing mode. Preserve the user billing scope in diagnostics. --- src/providers/copilot.ts | 21 +++++----- tests/providers.copilot.test.ts | 72 +++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/src/providers/copilot.ts b/src/providers/copilot.ts index 5f184ed6..004cc1c7 100644 --- a/src/providers/copilot.ts +++ b/src/providers/copilot.ts @@ -58,10 +58,12 @@ function formatBillingPeriod(period: { year: number; month: number }): string { function getCopilotGroup( mode: "user_quota" | "user_plan" | "organization_usage" | "enterprise_usage", + plan?: string, ): string { - return mode === "organization_usage" || mode === "enterprise_usage" - ? "Copilot (business)" - : "Copilot (personal)"; + if (mode === "organization_usage" || mode === "enterprise_usage") { + return "Copilot (business)"; + } + return plan ? `Copilot (${plan})` : "Copilot (personal)"; } function formatNumber(value: number): string { @@ -141,7 +143,7 @@ function planEntries(result: CopilotPlanResult): QuotaToastEntry[] { kind: "value", accounting: remoteAccounting("quota", result.authority), name: "Copilot", - group: getCopilotGroup(result.mode), + group: getCopilotGroup(result.mode, result.plan), label: "Plan:", value: result.plan ? `${result.plan} | quota details unavailable` @@ -152,7 +154,7 @@ function planEntries(result: CopilotPlanResult): QuotaToastEntry[] { } function personalEntries(result: CopilotQuotaResult): QuotaToastEntry[] { - const group = getCopilotGroup(result.mode); + const group = getCopilotGroup(result.mode, result.plan); const name = result.unit === "ai_credits" ? "Copilot AI Credits" @@ -317,12 +319,9 @@ export const copilotProvider: QuotaProvider = { "Copilot", results.map(({ row, result }) => ({ row, - fallbackName: - result?.success && result.mode === "user_plan" && result.plan - ? `Copilot (${result.plan})` - : result?.success - ? getCopilotGroup(result.mode) - : "Copilot", + fallbackName: result?.success + ? getCopilotGroup(result.mode, "plan" in result ? result.plan : undefined) + : "Copilot", })), ); const entries: QuotaToastEntry[] = []; diff --git a/tests/providers.copilot.test.ts b/tests/providers.copilot.test.ts index d43b4bd8..22d2a54c 100644 --- a/tests/providers.copilot.test.ts +++ b/tests/providers.copilot.test.ts @@ -8,6 +8,11 @@ import { } from "./helpers/provider-assertions.js"; import { createProviderAvailabilityContext } from "./helpers/provider-test-harness.js"; +const authMocks = vi.hoisted(() => ({ + readAuthFileCached: vi.fn(async () => null), + readCredentialRows: vi.fn(async () => []), +})); + vi.mock("../src/lib/copilot.js", () => ({ hasCopilotQuotaRuntimeAvailable: vi.fn(async () => false), queryCopilotQuota: vi.fn(), @@ -35,6 +40,12 @@ vi.mock("../src/lib/copilot.js", () => ({ })), })); +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readAuthFileCached: authMocks.readAuthFileCached, + readCredentialRows: authMocks.readCredentialRows, +})); + describe("copilot provider", () => { it("returns attempted:false when Copilot accounting is unavailable", async () => { const { queryCopilotQuota } = await import("../src/lib/copilot.js"); @@ -95,7 +106,7 @@ describe("copilot provider", () => { expect(visibleEntries(out.entries, "copilot")).toEqual([ { name: "Copilot Premium Interactions", - group: "Copilot (personal)", + group: "Copilot (enterprise)", label: "Quota:", right: "600.5/1,000", percentRemaining: 40, @@ -108,6 +119,61 @@ describe("copilot provider", () => { }); }); + it("uses the OAuth GHE Copilot plan in the credential presentation without changing billing scope", async () => { + const { getCopilotQuotaAuthDiagnostics, queryCopilotQuota } = await import( + "../src/lib/copilot.js" + ); + const diagnostics = { + pat: { state: "absent", checkedPaths: [] }, + oauth: { + configured: true, + keyName: "github-copilot", + hasRefreshToken: false, + hasAccessToken: true, + hasEnterpriseUrl: true, + }, + deployment: "ghe.com", + apiHost: "api.acme.ghe.com", + enterpriseHostSource: "oauth", + effectiveSource: "oauth", + override: "none", + billingMode: "user_quota", + billingScope: "user", + quotaApi: "copilot_internal_user", + budgetApi: "not_available", + oauthAccountingState: "available_via_copilot_internal_user", + billingApiAccessLikely: true, + remainingTotalsState: "reported_by_copilot_internal_user", + }; + (getCopilotQuotaAuthDiagnostics as any) + .mockReturnValueOnce(diagnostics) + .mockReturnValueOnce(diagnostics); + authMocks.readCredentialRows.mockResolvedValueOnce([ + { + id: "copilot-alice", + integrationId: "github-copilot", + label: "alice", + active: true, + value: { type: "oauth", access: "oauth-token", enterpriseUrl: "acme.ghe.com" }, + }, + ]); + (queryCopilotQuota as any).mockResolvedValueOnce({ + success: true, + mode: "user_quota", + unit: "premium_interactions", + used: 600, + total: 1_000, + percentRemaining: 40, + authority: "provider_reported", + plan: "enterprise", + }); + + const out = await copilotProvider.fetch({} as any); + + expect(visibleEntries(out.entries, "copilot")[0]?.group).toBe("[Copilot alice] (enterprise)*"); + expect(out.statusDetails).toContainEqual({ key: "billing_scope", value: "user" }); + }); + it("renders pooled organization credits plus a real additional-usage budget", async () => { const { queryCopilotQuota } = await import("../src/lib/copilot.js"); (queryCopilotQuota as any).mockResolvedValueOnce({ @@ -227,7 +293,7 @@ describe("copilot provider", () => { expect(visibleEntries(out.entries, "copilot")).toEqual([ { name: "Copilot Premium Requests", - group: "Copilot (personal)", + group: "Copilot (pro+)", label: "Quota:", right: "150/1,500", percentRemaining: 90, @@ -252,7 +318,7 @@ describe("copilot provider", () => { { kind: "value", name: "Copilot", - group: "Copilot (personal)", + group: "Copilot (business)", label: "Plan:", value: "business | quota details unavailable", resetTimeIso: "2026-02-01T00:00:00.000Z", From 84834324bf8e9f701eeeff55c0cb7034e8a99682 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Sat, 5 Sep 2026 10:40:17 +0200 Subject: [PATCH 14/97] fix(tui): Restore local plugin discovery Add the conventional local TUI entrypoint and align OpenTUI with the beta-19151 runtime without changing server or TUI behavior. --- package.json | 4 +- pnpm-lock.yaml | 94 +++++++++++++-------------- scripts/verify-typescript-version.mjs | 4 +- tests/package-manifest.test.ts | 8 +-- tests/tui-dist-packaging.test.ts | 8 +++ tui.ts | 1 + 6 files changed, 64 insertions(+), 55 deletions(-) create mode 100644 tui.ts diff --git a/package.json b/package.json index ae301724..012c790f 100644 --- a/package.json +++ b/package.json @@ -98,8 +98,8 @@ "dependencies": { "@clack/prompts": "^1.2.0", "@opentelemetry/api": "^1.9.1", - "@opentui/core": "^0.5.3", - "@opentui/solid": "^0.5.3", + "@opentui/core": "^0.5.10", + "@opentui/solid": "^0.5.10", "comment-json": "^5.0.0", "jsonc-parser": "^3.3.1", "solid-js": "^1.9.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60c8eef5..2783e0a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,11 @@ importers: specifier: ^1.9.1 version: 1.9.1 '@opentui/core': - specifier: ^0.5.3 - version: 0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10) + specifier: ^0.5.10 + version: 0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10) '@opentui/solid': - specifier: ^0.5.3 - version: 0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) + specifier: ^0.5.10 + version: 0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) comment-json: specifier: ^5.0.0 version: 5.0.0 @@ -44,7 +44,7 @@ importers: version: 2.3.11 '@opencode-ai/plugin': specifier: 1.18.1 - version: 1.18.1(@opentui/core@0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)) + version: 1.18.1(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)) '@opentelemetry/sdk-metrics': specifier: 2.10.0 version: 2.10.0(@opentelemetry/api@1.9.1) @@ -519,55 +519,55 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} - '@opentui/core-darwin-arm64@0.5.4': - resolution: {integrity: sha512-oETbn6tg/0g+mOvGXy3iot+1Zv7CGr65U1lRaPJ7kpsKGnOxftCpDD1qA0I6eQwfPJa9k7h9D/9yGB3IbweGcg==} + '@opentui/core-darwin-arm64@0.5.10': + resolution: {integrity: sha512-Vyb+nTbhab8ZcRy5gg1loEEGwRcIbjAeVRIBfHBcbFDqmITBOg7x2gqJ+x/TnoOy4uwMhCmICUN2wiyREw3r1Q==} cpu: [arm64] os: [darwin] - '@opentui/core-darwin-x64@0.5.4': - resolution: {integrity: sha512-TIeqCNfAV8xvNAv6oVBYsoGBz/p8CxcYm668OQIeBPUO+irqbQ72vJJa/SwFZrUEAHFmsDELaB6y80oHU5Jm6g==} + '@opentui/core-darwin-x64@0.5.10': + resolution: {integrity: sha512-tTFLcM7Oj1gTyhm/bUdAt3C6grZdCxPk6+/g2azcZBUlI3/62LwbeRS6HbQKFFmm+1fUmX8cq6kWrtul885mVg==} cpu: [x64] os: [darwin] - '@opentui/core-linux-arm64-musl@0.5.4': - resolution: {integrity: sha512-8vFWd1dsPZj9fHQKlsGHue3ukp7MRjTSpmIrdneIruOcoK2etccHfd6bqIo4FcNr+HA0eI2FP4ZIL9xhE3r9/w==} + '@opentui/core-linux-arm64-musl@0.5.10': + resolution: {integrity: sha512-dGMphDKexSdeYqwl0wgoFBP88Ta/cdi1Zc1mk29/ENkSCGz+74zlCHgqTHRNGLmI8W5TfuUtCyktQH11/Z+TBQ==} cpu: [arm64] os: [linux] libc: [musl] - '@opentui/core-linux-arm64@0.5.4': - resolution: {integrity: sha512-UrOsX3D5BOO9TI30WvRwEK/lyPPhY75+LwSqeRRe8SV2ON+Ez5QqY4oryTyYC6+yNahTJufz34n0YgwnQaxTNA==} + '@opentui/core-linux-arm64@0.5.10': + resolution: {integrity: sha512-ncJXcgudhBf2GdJyF3xVQN/Ec+1F7GOL+pRrURmgBYSj2v1w6EyoDQFAACtPTK2c3R38W6fvZwL4JSLlm4EFXQ==} cpu: [arm64] os: [linux] - '@opentui/core-linux-x64-musl@0.5.4': - resolution: {integrity: sha512-YYREqUB3v5K0qWij3A5YWzJkkVXp/EqIiuHZKsAjrUAY/0+Bp8Hn0baLvvvkuklpG9whW+GfwIeUsmp4fxqmCA==} + '@opentui/core-linux-x64-musl@0.5.10': + resolution: {integrity: sha512-Oj4H9hApuvuTKPWxh4SoZAgGJorR7vbvnrZA/cAkSMAk2VGSoHRRcqeXQbcH8IcdjVZ0KFpv8Zkl/D5Ye+2mew==} cpu: [x64] os: [linux] libc: [musl] - '@opentui/core-linux-x64@0.5.4': - resolution: {integrity: sha512-1RXzSl6d347O7mUXviXFWlFFyILr7qsVt4jwD3k0UiKtENeEDNi+glM1bKHbXwCdQjfA835QgFA2mbILybK+aQ==} + '@opentui/core-linux-x64@0.5.10': + resolution: {integrity: sha512-5qtYaOgwVycZD1GaGshTRsi0rXPAmVExO03N1JQaHu+NYxK/vXSOc7Bu4QW0sPXx3Sp0SpzpP+FHjXABfoK66g==} cpu: [x64] os: [linux] - '@opentui/core-win32-arm64@0.5.4': - resolution: {integrity: sha512-F9sB6suPJmwkLF2MwKEC+OtwP6cn5QspIm4MCh2hmu3mVqSz9GDpYID1X3sAh9/V79EVocqiOSqI3KeCTRouKQ==} + '@opentui/core-win32-arm64@0.5.10': + resolution: {integrity: sha512-A9VhgvTxQoUdZ+8LmUumEng1sQNbj9QQQT3NYG9mSxI54qTANi7vOWNSphMiY6RMVsr22pgm6nUvSSvJXv7Jog==} cpu: [arm64] os: [win32] - '@opentui/core-win32-x64@0.5.4': - resolution: {integrity: sha512-2/6dPPPJ9xL/bWz9jh+lZeV2g/tbxZ9N4FBIqwhnqSfIYy5jOnscsPZpcN4X6PstfJAo2yf0Ebk/tLTOzOS6TQ==} + '@opentui/core-win32-x64@0.5.10': + resolution: {integrity: sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw==} cpu: [x64] os: [win32] - '@opentui/core@0.5.4': - resolution: {integrity: sha512-czcJKQ72QhTWvu1eWfKg4EPN1GLLND6cP9MOhDyqYzCdIZgxQSJVWYzz6c4/CQGOu/qQLRyce2y1efVu4lgQ0w==} + '@opentui/core@0.5.10': + resolution: {integrity: sha512-C3a2UbmefeAjIxAgm4BqjuSxKT4oqutfvYFwVvUgMxmGRHkNbBc/s7sukV0JgwcxFcV3uMFrXxo+E+BQtvuOiw==} peerDependencies: web-tree-sitter: 0.25.10 - '@opentui/solid@0.5.4': - resolution: {integrity: sha512-1injDvieTyYXD3RLC2EclaJK8XZ2+qyJ7wGeQPSzpcYEEcanb7G79KVWFlN7YUzqVAhTIictZDoQ6SRvIwL4Ww==} + '@opentui/solid@0.5.10': + resolution: {integrity: sha512-KrmMIsHiKBHOABTC0brOwqWm+sGq1ZX2sGCAx6WgtBbE3STMup9n8TAy/6gUYhwcjC9zugT53ytfSVwCwVWZUg==} peerDependencies: solid-js: 1.9.12 @@ -1966,15 +1966,15 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': optional: true - '@opencode-ai/plugin@1.18.1(@opentui/core@0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))': + '@opencode-ai/plugin@1.18.1(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))': dependencies: '@ai-sdk/provider': 3.0.8 '@opencode-ai/sdk': 1.18.1 effect: 4.0.0-beta.83 zod: 4.1.8 optionalDependencies: - '@opentui/core': 0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10) - '@opentui/solid': 0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) + '@opentui/core': 0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10) + '@opentui/solid': 0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) '@opencode-ai/sdk@1.18.1': dependencies: @@ -2001,31 +2001,31 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} - '@opentui/core-darwin-arm64@0.5.4': + '@opentui/core-darwin-arm64@0.5.10': optional: true - '@opentui/core-darwin-x64@0.5.4': + '@opentui/core-darwin-x64@0.5.10': optional: true - '@opentui/core-linux-arm64-musl@0.5.4': + '@opentui/core-linux-arm64-musl@0.5.10': optional: true - '@opentui/core-linux-arm64@0.5.4': + '@opentui/core-linux-arm64@0.5.10': optional: true - '@opentui/core-linux-x64-musl@0.5.4': + '@opentui/core-linux-x64-musl@0.5.10': optional: true - '@opentui/core-linux-x64@0.5.4': + '@opentui/core-linux-x64@0.5.10': optional: true - '@opentui/core-win32-arm64@0.5.4': + '@opentui/core-win32-arm64@0.5.10': optional: true - '@opentui/core-win32-x64@0.5.4': + '@opentui/core-win32-x64@0.5.10': optional: true - '@opentui/core@0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10)': + '@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10)': dependencies: bun-ffi-structs: 0.3.1(typescript@7.0.2) diff: 9.0.0 @@ -2034,22 +2034,22 @@ snapshots: strip-ansi: 7.1.2 web-tree-sitter: 0.25.10 optionalDependencies: - '@opentui/core-darwin-arm64': 0.5.4 - '@opentui/core-darwin-x64': 0.5.4 - '@opentui/core-linux-arm64': 0.5.4 - '@opentui/core-linux-arm64-musl': 0.5.4 - '@opentui/core-linux-x64': 0.5.4 - '@opentui/core-linux-x64-musl': 0.5.4 - '@opentui/core-win32-arm64': 0.5.4 - '@opentui/core-win32-x64': 0.5.4 + '@opentui/core-darwin-arm64': 0.5.10 + '@opentui/core-darwin-x64': 0.5.10 + '@opentui/core-linux-arm64': 0.5.10 + '@opentui/core-linux-arm64-musl': 0.5.10 + '@opentui/core-linux-x64': 0.5.10 + '@opentui/core-linux-x64-musl': 0.5.10 + '@opentui/core-win32-arm64': 0.5.10 + '@opentui/core-win32-x64': 0.5.10 transitivePeerDependencies: - typescript - '@opentui/solid@0.5.4(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)': + '@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)': dependencies: '@babel/core': 7.28.0 '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) - '@opentui/core': 0.5.4(typescript@7.0.2)(web-tree-sitter@0.25.10) + '@opentui/core': 0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10) babel-plugin-module-resolver: 5.0.2 babel-preset-solid: 1.9.12(@babel/core@7.28.0)(solid-js@1.9.12) entities: 7.0.1 diff --git a/scripts/verify-typescript-version.mjs b/scripts/verify-typescript-version.mjs index e270922e..49dd5aaf 100644 --- a/scripts/verify-typescript-version.mjs +++ b/scripts/verify-typescript-version.mjs @@ -6,8 +6,8 @@ import { parse } from "yaml"; const EXPECTED_TYPESCRIPT_VERSION = "7.0.2"; const EXPECTED_PLUGIN_VERSION = "1.18.1"; -const EXPECTED_OPENTUI_SPECIFIER = "^0.5.3"; -const EXPECTED_OPENTUI_VERSION = "0.5.4"; +const EXPECTED_OPENTUI_SPECIFIER = "^0.5.10"; +const EXPECTED_OPENTUI_VERSION = "0.5.10"; const EXPECTED_OPENTUI_PACKAGES = ["@opentui/core", "@opentui/solid"]; const BUN_FFI_STRUCTS_VERSION = "0.3.1"; const BUN_FFI_TYPESCRIPT_PEER = "^5"; diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index d7aff136..4bb176ba 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -154,8 +154,8 @@ describe("package manifest compatibility", () => { it("keeps the public plugin peer broad and reference-compatible development targets exact", () => { expect(pkg.peerDependencies?.["@opencode-ai/plugin"]).toBe("^1.4.3"); expect(pkg.devDependencies?.["@opencode-ai/plugin"]).toBe("1.18.1"); - expect(pkg.dependencies?.["@opentui/core"]).toBe("^0.5.3"); - expect(pkg.dependencies?.["@opentui/solid"]).toBe("^0.5.3"); + expect(pkg.dependencies?.["@opentui/core"]).toBe("^0.5.10"); + expect(pkg.dependencies?.["@opentui/solid"]).toBe("^0.5.10"); expect(readme).toContain("Node.js `>= 22` is required."); expect(readme).not.toContain("OpenCode `>= 1.4.3`"); expect(pkg.engines).not.toHaveProperty("opencode"); @@ -165,8 +165,8 @@ describe("package manifest compatibility", () => { expect(tsconfig.compilerOptions?.types).toEqual(["node"]); expect(typescriptValidator).toContain('const EXPECTED_TYPESCRIPT_VERSION = "7.0.2";'); expect(typescriptValidator).toContain('const EXPECTED_PLUGIN_VERSION = "1.18.1";'); - expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_SPECIFIER = "^0.5.3";'); - expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_VERSION = "0.5.4";'); + expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_SPECIFIER = "^0.5.10";'); + expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_VERSION = "0.5.10";'); expect(typescriptValidator).toContain('const BUN_FFI_STRUCTS_VERSION = "0.3.1";'); expect(typescriptValidator).toContain('const BUN_FFI_TYPESCRIPT_PEER = "^5";'); expect(typescriptValidator).toContain("Known unmet peer:"); diff --git a/tests/tui-dist-packaging.test.ts b/tests/tui-dist-packaging.test.ts index 1e331819..b41760ef 100644 --- a/tests/tui-dist-packaging.test.ts +++ b/tests/tui-dist-packaging.test.ts @@ -32,6 +32,14 @@ async function exists(url: URL): Promise { } describe("tui dist packaging", () => { + it("loads the conventional root TUI entrypoint as the existing local plugin", async () => { + const [local, source] = await Promise.all([import("../tui.js"), import("../src/index.js")]); + + expect(local.default).toBe(source.default); + expect(local.default).toMatchObject({ id: "@slkiser/opencode-quota" }); + expect(typeof local.default.setup).toBe("function"); + }); + it("ships the precompiled TUI entry and removes stale jsx artifacts", async () => { const distTui = new URL("../dist/tui.js", import.meta.url); const distTuiV2 = new URL("../dist/tui-v2.js", import.meta.url); diff --git a/tui.ts b/tui.ts new file mode 100644 index 00000000..0fdc642d --- /dev/null +++ b/tui.ts @@ -0,0 +1 @@ +export { default } from "./src/index.js"; From 02b576e283567c6ab46fe3164931274cc224c19d Mon Sep 17 00:00:00 2001 From: Ran Sagy Date: Tue, 8 Sep 2026 18:23:43 +0300 Subject: [PATCH 15/97] fix(quota): collapse credential alias rows before Go multi-connection fan-out The V2 multi-connection fan-out treated every credential row whose integration_id matched ["opencode-go", "opencode"] as a separate OpenCode Go connection. `opencode` is a legacy alias of the `opencode-go` integration (see resolveOpenCodeGoAuth), so anyone with both OpenCode Go and OpenCode Zen authenticated - typically with the same workspace API key - had their Go 5h/weekly quota reported twice, and a Zen credential with a different key would be queried against the Go quota API. Select connections per upstream account instead: - prefer native `opencode-go` rows and fall back to `opencode` rows only when no native row exists, mirroring the auth resolver precedence; - collapse rows holding identical credential values (key-order insensitive) so alias duplicates never fan out. Exposes selectConnectionCredentialRows from lib/opencode-auth and derives the fan-out integration list from the resolver constants. --- src/lib/opencode-auth.ts | 42 ++++++ src/lib/opencode-go-auth.ts | 2 + src/providers/opencode-go.ts | 23 +++- tests/lib.opencode-auth.test.ts | 97 ++++++++++++- tests/lib.opencode-go-auth.test.ts | 3 +- tests/provider-surfaces.opencode-go.test.ts | 1 + tests/providers.opencode-go.test.ts | 144 +++++++++++++++++++- 7 files changed, 298 insertions(+), 14 deletions(-) diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 5084d764..6734c2a0 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -122,6 +122,48 @@ export async function readCredentialRows(): Promise { return readCredentialRowsFromDatabases(getCredentialDatabasePaths()); } +function canonicalCredentialValueKey(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map((item) => canonicalCredentialValueKey(item)).join(",")}]`; + } + if (value !== null && typeof value === "object") { + const entries = Object.entries(value as Record).sort(([a], [b]) => + a < b ? -1 : a > b ? 1 : 0, + ); + return `{${entries + .map(([key, nested]) => `${JSON.stringify(key)}:${canonicalCredentialValueKey(nested)}`) + .join(",")}}`; + } + return value === undefined ? "undefined" : JSON.stringify(value); +} + +/** + * Collapse credential rows that represent the same upstream connection. + * + * Rows holding identical credential values are the same connection (e.g. the + * same workspace key stored under the `opencode-go` integration and its legacy + * `opencode` alias), and rows under `primaryIntegrationId` take precedence over + * alias rows so a real credential for another integration that shares the key + * is not reported as an additional connection. Input order is preserved + * otherwise. + */ +export function selectConnectionCredentialRows( + rows: readonly CredentialRow[], + primaryIntegrationId: string, +): CredentialRow[] { + const primaryRows = rows.filter((row) => row.integrationId === primaryIntegrationId); + const candidates = primaryRows.length > 0 ? primaryRows : [...rows]; + const seen = new Set(); + const selected: CredentialRow[] = []; + for (const row of candidates) { + const key = canonicalCredentialValueKey(row.value); + if (seen.has(key)) continue; + seen.add(key); + selected.push(row); + } + return selected; +} + function readCredentialRowsFromDatabases(paths: string[]): CredentialRow[] { for (const path of paths) { const rows = readCredentialDatabase(path); diff --git a/src/lib/opencode-go-auth.ts b/src/lib/opencode-go-auth.ts index 19458271..92d92b00 100644 --- a/src/lib/opencode-go-auth.ts +++ b/src/lib/opencode-go-auth.ts @@ -11,6 +11,8 @@ export const DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS = 5_000; // `opencode` stays as a fallback alias for existing manual setups. const OPENCODE_GO_AUTH_KEYS = ["opencode-go", "opencode"] as const; const OPENCODE_GO_PROVIDER_KEYS = ["opencode-go", "opencode"] as const; +/** Integration IDs whose credential rows can hold OpenCode Go auth, in resolver precedence order. */ +export const OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS: readonly string[] = OPENCODE_GO_AUTH_KEYS; const ALLOWED_OPENCODE_GO_ENV_VARS = ["OPENCODE_API_KEY"] as const; export type OpenCodeGoKeySource = diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index 7f422fd3..22b75477 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -5,11 +5,16 @@ import type { QuotaProviderStatusDetail, QuotaToastEntry, } from "../lib/entries.js"; -import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; +import { + formatCredentialDisplayNames, + readCredentialRows, + selectConnectionCredentialRows, +} from "../lib/opencode-auth.js"; import { queryOpenCodeGoQuota } from "../lib/opencode-go.js"; import { DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, getOpenCodeGoAuthDiagnostics, + OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS, type OpenCodeGoAuthDiagnostics, resolveOpenCodeGoAuth, resolveOpenCodeGoAuthCached, @@ -108,8 +113,15 @@ export const opencodeGoProvider: QuotaProvider = { } if (diagnostics.source === "opencode.db") { - const credentialRows = (await readCredentialRows()).filter((row) => - ["opencode-go", "opencode"].includes(row.integrationId), + // `opencode` is a legacy alias of the `opencode-go` integration in the + // credential database (see resolveOpenCodeGoAuth). Alias rows must not + // become additional connections: prefer native rows and collapse rows + // holding the same credential (e.g. Zen + Go sharing a workspace key). + const credentialRows = selectConnectionCredentialRows( + (await readCredentialRows()).filter((row) => + OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS.includes(row.integrationId), + ), + "opencode-go", ); const rowNames = formatCredentialDisplayNames( OPENCODE_GO_PROVIDER_LABEL, @@ -122,7 +134,10 @@ export const opencodeGoProvider: QuotaProvider = { const credentials = credentialRows.flatMap((row) => { const rowAuth = resolveOpenCodeGoAuth({ [row.integrationId]: row.value }); if (rowAuth.state === "invalid") { - invalidErrors.push({ label: displayNamesByRowId.get(row.id) ?? OPENCODE_GO_PROVIDER_LABEL, message: rowAuth.error }); + invalidErrors.push({ + label: displayNamesByRowId.get(row.id) ?? OPENCODE_GO_PROVIDER_LABEL, + message: rowAuth.error, + }); } return rowAuth.state === "configured" ? [{ row, auth: rowAuth }] : []; }); diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index a785e7fd..d927cbd5 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -6,11 +6,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { clearReadAuthFileCacheForTests, + formatCredentialDisplayNames, getCredentialDatabasePath, getCredentialDatabasePaths, - formatCredentialDisplayNames, readAuthFile, readCredentialRows, + selectConnectionCredentialRows, } from "../src/lib/opencode-auth.js"; const temporaryDirectories: string[] = []; @@ -99,9 +100,18 @@ describe("OpenCode auth reader", () => { it("numbers duplicate custom aliases without numbering default credentials", () => { expect( formatCredentialDisplayNames("OpenAI", [ - { row: { id: "a", integrationId: "openai", label: "SEPD", active: true, value: {} }, fallbackName: "OpenAI (Pro)" }, - { row: { id: "b", integrationId: "openai", label: "SEPD", active: false, value: {} }, fallbackName: "OpenAI (Pro)" }, - { row: { id: "c", integrationId: "openai", label: "default", active: false, value: {} }, fallbackName: "OpenAI (Pro)" }, + { + row: { id: "a", integrationId: "openai", label: "SEPD", active: true, value: {} }, + fallbackName: "OpenAI (Pro)", + }, + { + row: { id: "b", integrationId: "openai", label: "SEPD", active: false, value: {} }, + fallbackName: "OpenAI (Pro)", + }, + { + row: { id: "c", integrationId: "openai", label: "default", active: false, value: {} }, + fallbackName: "OpenAI (Pro)", + }, ]), ).toEqual(["[OpenAI SEPD] (Pro)*", "[OpenAI SEPD 2] (Pro)", "[OpenAI] (Pro)"]); }); @@ -177,3 +187,82 @@ describe("OpenCode auth reader", () => { }); }); }); + +describe("selectConnectionCredentialRows", () => { + const row = (id: string, integrationId: string, key: string) => ({ + id, + integrationId, + label: "default", + active: false, + value: { type: "key", key }, + }); + + it("prefers native rows over alias rows holding the same credential", () => { + const rows = [ + row("alias", "opencode", "workspace-key"), + row("native", "opencode-go", "workspace-key"), + ]; + + expect(selectConnectionCredentialRows(rows, "opencode-go").map((row) => row.id)).toEqual([ + "native", + ]); + }); + + it("falls back to alias rows when no native row exists", () => { + const rows = [row("alias", "opencode", "workspace-key")]; + + expect(selectConnectionCredentialRows(rows, "opencode-go").map((row) => row.id)).toEqual([ + "alias", + ]); + }); + + it("collapses alias rows into native rows with the same credential value", () => { + const rows = [ + row("go-1", "opencode-go", "workspace-key"), + row("zen", "opencode", "workspace-key"), + row("go-2", "opencode-go", "other-key"), + ]; + + expect(selectConnectionCredentialRows(rows, "opencode-go").map((row) => row.id)).toEqual([ + "go-1", + "go-2", + ]); + }); + + it("collapses exact duplicates within the primary integration", () => { + const rows = [ + row("first", "opencode-go", "workspace-key"), + row("second", "opencode-go", "workspace-key"), + ]; + + expect(selectConnectionCredentialRows(rows, "opencode-go").map((row) => row.id)).toEqual([ + "first", + ]); + }); + + it("treats property order in stored credential values as insignificant", () => { + const rows = [ + row("first", "opencode-go", "workspace-key"), + { + ...row("second", "opencode-go", "workspace-key"), + value: { key: "workspace-key", type: "key" }, + }, + ]; + + expect(selectConnectionCredentialRows(rows, "opencode-go").map((row) => row.id)).toEqual([ + "first", + ]); + }); + + it("keeps distinct credentials as separate connections", () => { + const rows = [ + row("personal", "opencode-go", "personal-key"), + row("work", "opencode-go", "work-key"), + ]; + + expect(selectConnectionCredentialRows(rows, "opencode-go").map((row) => row.id)).toEqual([ + "personal", + "work", + ]); + }); +}); diff --git a/tests/lib.opencode-go-auth.test.ts b/tests/lib.opencode-go-auth.test.ts index fa09ec87..c51732d0 100644 --- a/tests/lib.opencode-go-auth.test.ts +++ b/tests/lib.opencode-go-auth.test.ts @@ -35,7 +35,8 @@ vi.mock("node:fs/promises", async (importOriginal) => ({ readFile: authMocks.readFile, lstat: authMocks.lstat, })); -vi.mock("../src/lib/opencode-auth.js", () => ({ +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), getCredentialDatabasePaths: authMocks.getCredentialDatabasePaths, readAuthFileCached: authMocks.readAuthFileCached, readCredentialRows: authMocks.readCredentialRows, diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 35fc6478..47027847 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -53,6 +53,7 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => ); vi.mock("../src/lib/opencode-go-auth.js", () => ({ DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS: 5_000, + OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS: ["opencode-go", "opencode"], resolveOpenCodeGoAuthCached: mocks.resolveOpenCodeGoAuthCached, getOpenCodeGoAuthDiagnostics: mocks.getOpenCodeGoAuthDiagnostics, })); diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index bddd4a73..ff5a6c66 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({ vi.mock("../src/lib/opencode-go-auth.js", () => ({ DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS: 5_000, + OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS: ["opencode-go", "opencode"], resolveOpenCodeGoAuthCached: mocks.resolveOpenCodeGoAuthCached, getOpenCodeGoAuthDiagnostics: mocks.getOpenCodeGoAuthDiagnostics, resolveOpenCodeGoAuth: mocks.resolveOpenCodeGoAuth, @@ -125,24 +126,157 @@ describe("opencode-go provider", () => { it("keeps a valid inactive duplicate-label database credential when the active row is invalid", async () => { const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValueOnce(diagnostics("invalid")); - mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ state: "invalid", error: "empty key" }); + mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ + state: "invalid", + error: "empty key", + }); (readCredentialRows as any).mockResolvedValueOnce([ - { id: "bad", integrationId: "opencode-go", label: "shared", active: true, value: { key: "" } }, - { id: "good", integrationId: "opencode-go", label: "shared", active: false, value: { key: "ok" } }, + { + id: "bad", + integrationId: "opencode-go", + label: "shared", + active: true, + value: { key: "" }, + }, + { + id: "good", + integrationId: "opencode-go", + label: "shared", + active: false, + value: { key: "ok" }, + }, ]); mocks.resolveOpenCodeGoAuth.mockImplementation((auth: any) => - auth["opencode-go"].key ? { state: "configured", apiKey: "row-token" } : { state: "invalid", error: "empty key" }, + auth["opencode-go"].key + ? { state: "configured", apiKey: "row-token" } + : { state: "invalid", error: "empty key" }, ); const out = await runFetch(); expect(out.errors).toContainEqual({ label: "[OpenCode Go shared]*", message: "empty key" }); expect(out.entries).toContainEqual( - expect.objectContaining({ group: "[OpenCode Go shared 2]", accounting: expect.objectContaining({ sourceId: "good" }) }), + expect.objectContaining({ + group: "[OpenCode Go shared 2]", + accounting: expect.objectContaining({ sourceId: "good" }), + }), ); expect(out.entries).toHaveLength(3); expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); }); + it("reports one Go connection when the same key is stored under both integrations", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + const workspaceCredential = { type: "key", key: "workspace-key" }; + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "go-row", + integrationId: "opencode-go", + label: "default", + active: false, + value: workspaceCredential, + }, + { + id: "zen-row", + integrationId: "opencode", + label: "default", + active: false, + value: workspaceCredential, + }, + ]); + mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "row-token" }); + + const out = await runFetch(["rolling", "weekly"]); + + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + expect(visibleEntries(out.entries, "opencode-go").map((entry) => entry.group)).toEqual([ + "[OpenCode Go]*", + "[OpenCode Go]*", + ]); + for (const entry of out.entries) { + expect(entry.accounting).toMatchObject({ sourceId: "go-row" }); + } + }); + + it("does not report another integration's credential as a second Go connection", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "go-row", + integrationId: "opencode-go", + label: "default", + active: false, + value: { type: "key", key: "go-key" }, + }, + { + id: "zen-row", + integrationId: "opencode", + label: "default", + active: false, + value: { type: "key", key: "zen-key" }, + }, + ]); + mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "row-token" }); + + const out = await runFetch(["rolling", "weekly"]); + + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + for (const entry of out.entries) { + expect(entry.accounting).toMatchObject({ sourceId: "go-row" }); + } + }); + + it("keeps separate connections for distinct native credentials", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "go-row", + integrationId: "opencode-go", + label: "default", + active: false, + value: { type: "key", key: "first-key" }, + }, + { + id: "second-go-row", + integrationId: "opencode-go", + label: "default", + active: false, + value: { type: "key", key: "second-key" }, + }, + ]); + mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "row-token" }); + + const out = await runFetch(["rolling", "weekly"]); + + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledTimes(2); + const groups = visibleEntries(out.entries, "opencode-go").map((entry) => entry.group); + expect(groups).toEqual(["[OpenCode Go]", "[OpenCode Go]", "[OpenCode Go]", "[OpenCode Go]"]); + }); + + it("falls back to legacy alias rows when no native opencode-go row exists", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "alias-row", + integrationId: "opencode", + label: "default", + active: false, + value: { type: "key", key: "legacy-key" }, + }, + ]); + mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "row-token" }); + + const out = await runFetch(["rolling", "weekly"]); + + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + expect(visibleEntries(out.entries, "opencode-go").map((entry) => entry.group)).toEqual([ + "[OpenCode Go]*", + "[OpenCode Go]*", + ]); + for (const entry of out.entries) { + expect(entry.accounting).toMatchObject({ sourceId: "alias-row" }); + } + }); + it("passes the resolved token and effective timeout to the API client", async () => { await runFetch(["rolling", "weekly", "monthly"], 12_345); From 75d9b5dbc01ed2e3e4ad1a609f9203912a2a4f8f Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Wed, 9 Sep 2026 13:50:28 +0200 Subject: [PATCH 16/97] fix(quota): Correct sidebar rendering and storage reads Apply sidebar format overrides before projecting quota windows and preserve readable reset precision across full surfaces. Project only required SQLite message fields so status aggregation avoids loading complete payloads into memory. --- src/lib/format-utils.ts | 9 +- src/lib/opencode-storage.ts | 151 +++++++++++++----------- src/lib/tui-compact-format.ts | 8 +- src/tui-v2.tsx | 12 +- tests/format-utils.test.ts | 8 ++ tests/format.test.ts | 6 +- tests/lib.opencode-storage.test.ts | 134 +++++++++++++++++---- tests/providers.cursor.surfaces.test.ts | 3 +- tests/providers.kilo.surfaces.test.ts | 3 +- tests/quota-command-format.test.ts | 16 +-- tests/tui-compact-format.test.ts | 4 +- tests/tui-sidebar-format.test.ts | 2 +- tests/tui-smoke.test.ts | 2 +- tests/tui-v2-format-style.test.ts | 109 +++++++++++++++++ 14 files changed, 347 insertions(+), 120 deletions(-) create mode 100644 tests/tui-v2-format-style.test.ts diff --git a/src/lib/format-utils.ts b/src/lib/format-utils.ts index b5e1a225..14adf2b9 100644 --- a/src/lib/format-utils.ts +++ b/src/lib/format-utils.ts @@ -151,6 +151,8 @@ export interface FormatResetCountdownOptions { * many decimal places. */ decimals?: number; + /** Render adjacent exact units without spaces for width-constrained surfaces. */ + compactUnits?: boolean; } const MS_PER_DAY = 86_400_000; @@ -159,7 +161,7 @@ const MS_PER_HOUR = 3_600_000; /** * Format a reset countdown for toast display. * - * Returns a precise-to-minute value like "2d5h14m", "3h45m", or "14m". + * Returns a precise-to-minute value like "2d 5h 14m", "3h 45m", or "14m". * When reset time is in the past or invalid, returns "reset". */ export function formatResetCountdown(iso?: string, opts?: FormatResetCountdownOptions): string { @@ -191,8 +193,9 @@ export function formatResetCountdown(iso?: string, opts?: FormatResetCountdownOp return `0.5h`; } - if (days > 0) return `${days}d${hours}h${minutes}m`; - if (hours > 0) return `${hours}h${minutes}m`; + const separator = opts?.compactUnits ? "" : " "; + if (days > 0) return `${days}d${separator}${hours}h${separator}${minutes}m`; + if (hours > 0) return `${hours}h${separator}${minutes}m`; return `${minutes}m`; } diff --git a/src/lib/opencode-storage.ts b/src/lib/opencode-storage.ts index 47b2b38e..641629da 100644 --- a/src/lib/opencode-storage.ts +++ b/src/lib/opencode-storage.ts @@ -86,12 +86,22 @@ export function getOpenCodeDbPath(): string { return pickFirstExistingPath(getOpenCodeDbPathCandidates()); } -type MessageRow = { +type ProjectedMessageRow = { id: string; session_id: string; time_created: number; - time_updated?: number; - data: string; + role: unknown; + provider_id: unknown; + model_id: unknown; + tokens_input: unknown; + tokens_output: unknown; + tokens_reasoning: unknown; + tokens_cache_read: unknown; + tokens_cache_write: unknown; + cost: unknown; + time_completed: unknown; + agent: unknown; + mode: unknown; }; type SessionRow = { @@ -102,18 +112,6 @@ type SessionRow = { time_updated: number; }; -function asRecord(value: unknown): Record | null { - return value && typeof value === "object" ? (value as Record) : null; -} - -function safeJsonParse(raw: string): unknown | null { - try { - return JSON.parse(raw) as unknown; - } catch { - return null; - } -} - // Stay comfortably below SQLite's default host-parameter cap once optional // time filters are included in the query. const SQLITE_MAX_MESSAGE_QUERY_ARGS = 900; @@ -126,31 +124,41 @@ function normalizeString(s: unknown): string | undefined { return typeof s === "string" ? s : undefined; } -function mapRowToOpenCodeMessage(row: MessageRow): OpenCodeMessage | null { +function mapRowToOpenCodeMessage(row: ProjectedMessageRow): OpenCodeMessage | null { if (!row || typeof row !== "object") return null; if (typeof row.id !== "string" || typeof row.session_id !== "string") return null; if (typeof row.time_created !== "number") return null; - const payload = asRecord(safeJsonParse(row.data)); - if (!payload) return null; - - const payloadTime = asRecord(payload.time); - const role = normalizeString(payload.role) ?? "unknown"; + const input = normalizeNumber(row.tokens_input); + const output = normalizeNumber(row.tokens_output); + const reasoning = normalizeNumber(row.tokens_reasoning); + const cacheRead = normalizeNumber(row.tokens_cache_read); + const cacheWrite = normalizeNumber(row.tokens_cache_write); + const hasTokens = [input, output, reasoning, cacheRead, cacheWrite].some( + (value) => value !== undefined, + ); return { id: row.id, sessionID: row.session_id, - role, - providerID: normalizeString(payload.providerID), - modelID: normalizeString(payload.modelID), - tokens: payload.tokens as OpenCodeTokens | undefined, - cost: normalizeNumber(payload.cost), + role: normalizeString(row.role) ?? "unknown", + providerID: normalizeString(row.provider_id), + modelID: normalizeString(row.model_id), + tokens: hasTokens + ? { + input: input ?? 0, + output: output ?? 0, + ...(reasoning === undefined ? {} : { reasoning }), + cache: { read: cacheRead ?? 0, write: cacheWrite ?? 0 }, + } + : undefined, + cost: normalizeNumber(row.cost), time: { created: row.time_created, - completed: normalizeNumber(payloadTime?.completed), + completed: normalizeNumber(row.time_completed), }, - agent: normalizeString(payload.agent), - mode: normalizeString(payload.mode), + agent: normalizeString(row.agent), + mode: normalizeString(row.mode), }; } @@ -195,6 +203,30 @@ function chunkArray(items: readonly T[], chunkSize: number): T[][] { return chunks; } +function guardedJsonScalar(path: string): string { + return `CASE WHEN json_valid(data) THEN json_extract(data, '${path}') END`; +} + +const PROJECTED_MESSAGE_COLUMNS = [ + "id", + "session_id", + "time_created", + `${guardedJsonScalar("$.role")} AS role`, + `${guardedJsonScalar("$.providerID")} AS provider_id`, + `${guardedJsonScalar("$.modelID")} AS model_id`, + `${guardedJsonScalar("$.tokens.input")} AS tokens_input`, + `${guardedJsonScalar("$.tokens.output")} AS tokens_output`, + `${guardedJsonScalar("$.tokens.reasoning")} AS tokens_reasoning`, + `${guardedJsonScalar("$.tokens.cache.read")} AS tokens_cache_read`, + `${guardedJsonScalar("$.tokens.cache.write")} AS tokens_cache_write`, + `${guardedJsonScalar("$.cost")} AS cost`, + `${guardedJsonScalar("$.time.completed")} AS time_completed`, + `${guardedJsonScalar("$.agent")} AS agent`, + `${guardedJsonScalar("$.mode")} AS mode`, +].join(", "); + +const ASSISTANT_ROLE_EXPRESSION = `lower(${guardedJsonScalar("$.role")}) = 'assistant'`; + function buildMessageQuery(params: { sessionID?: string; sessionIDs?: string[]; @@ -230,8 +262,10 @@ function buildMessageQuery(params: { args.push(params.untilMs); } + where.push(ASSISTANT_ROLE_EXPRESSION); + const sql = - `SELECT id, session_id, time_created, time_updated, data FROM "message"` + + `SELECT ${PROJECTED_MESSAGE_COLUMNS} FROM "message"` + (where.length ? ` WHERE ${where.join(" AND ")}` : "") + ` ORDER BY time_created ASC, id ASC`; @@ -251,7 +285,7 @@ async function hasJsonExtract(conn: { } } -function mapAssistantMessages(rows: MessageRow[]): OpenCodeMessage[] { +function mapAssistantMessages(rows: ProjectedMessageRow[]): OpenCodeMessage[] { const out: OpenCodeMessage[] = []; for (const row of rows) { const msg = mapRowToOpenCodeMessage(row); @@ -269,25 +303,19 @@ function completedAt(message: OpenCodeMessage): number | null { : null; } -function mapCompletedAssistantMessages(rows: MessageRow[]): OpenCodeMessage[] { +function mapCompletedAssistantMessages(rows: ProjectedMessageRow[]): OpenCodeMessage[] { return mapAssistantMessages(rows).filter((message) => completedAt(message) !== null); } -function compareCompletedMessageOrder(a: OpenCodeMessage, b: OpenCodeMessage): number { - const aCompleted = completedAt(a) ?? Number.MAX_SAFE_INTEGER; - const bCompleted = completedAt(b) ?? Number.MAX_SAFE_INTEGER; - if (aCompleted !== bCompleted) return aCompleted - bCompleted; - return a.id.localeCompare(b.id); -} - function buildCompletedAssistantQuery(params: { completedSinceMs?: number; completedUntilMs?: number; }): { sql: string; args: unknown[] } { - const completedExpression = `CAST(json_extract(data, '$.time.completed') AS REAL)`; + const completedScalar = guardedJsonScalar("$.time.completed"); + const completedExpression = `CAST(${completedScalar} AS REAL)`; const where = [ - `json_extract(data, '$.role') = 'assistant'`, - `json_type(data, '$.time.completed') IN ('integer', 'real')`, + ASSISTANT_ROLE_EXPRESSION, + `CASE WHEN json_valid(data) THEN json_type(data, '$.time.completed') END IN ('integer', 'real')`, `${completedExpression} > 0`, ]; const args: unknown[] = []; @@ -303,7 +331,7 @@ function buildCompletedAssistantQuery(params: { return { sql: - `SELECT id, session_id, time_created, time_updated, data FROM "message"` + + `SELECT ${PROJECTED_MESSAGE_COLUMNS} FROM "message"` + ` WHERE ${where.join(" AND ")}` + ` ORDER BY ${completedExpression} ASC, id ASC`, args, @@ -336,15 +364,9 @@ export async function getOpenCodeDbStats(): Promise { let assistantCount = 0; if (await hasJsonExtract(conn)) { const a = conn.get<{ c: number }>( - `SELECT count(*) as c FROM "message" WHERE json_extract(data, '$.role') = 'assistant'`, + `SELECT count(*) as c FROM "message" WHERE ${guardedJsonScalar("$.role")} = 'assistant'`, ); assistantCount = typeof a?.c === "number" ? a.c : 0; - } else { - const rows = conn.all<{ data: string }>(`SELECT data FROM "message"`); - for (const r of rows) { - const payload = asRecord(safeJsonParse(r.data)); - if (payload?.role === "assistant") assistantCount += 1; - } } return { @@ -367,8 +389,9 @@ export async function iterAssistantMessages(params: { const conn = await db.open(); try { + if (!(await hasJsonExtract(conn))) return []; const q = buildMessageQuery({ sinceMs: params.sinceMs, untilMs: params.untilMs }); - const rows = conn.all(q.sql, q.args); + const rows = conn.all(q.sql, q.args); return mapAssistantMessages(rows); } finally { conn.close(); @@ -392,25 +415,10 @@ export async function iterCompletedAssistantMessages(params: { try { if (await hasJsonExtract(conn)) { const query = buildCompletedAssistantQuery(params); - return mapCompletedAssistantMessages(conn.all(query.sql, query.args)); + return mapCompletedAssistantMessages(conn.all(query.sql, query.args)); } - const rows = conn.all( - `SELECT id, session_id, time_created, time_updated, data FROM "message"`, - ); - return mapCompletedAssistantMessages(rows) - .filter((message) => { - const atMs = completedAt(message); - if (atMs === null) return false; - if (typeof params.completedSinceMs === "number" && atMs < params.completedSinceMs) { - return false; - } - if (typeof params.completedUntilMs === "number" && atMs > params.completedUntilMs) { - return false; - } - return true; - }) - .sort(compareCompletedMessageOrder); + return []; } finally { conn.close(); } @@ -441,8 +449,10 @@ export async function iterAssistantMessagesForSession(params: { throw new SessionNotFoundError(sessionID, db.dbPath); } + if (!(await hasJsonExtract(conn))) return []; + const q = buildMessageQuery({ sessionID, sinceMs, untilMs }); - const rows = conn.all(q.sql, q.args); + const rows = conn.all(q.sql, q.args); return mapAssistantMessages(rows); } finally { conn.close(); @@ -467,6 +477,7 @@ export async function iterAssistantMessagesForSessions(params: { const conn = await db.open(); try { + if (!(await hasJsonExtract(conn))) return []; const reservedArgs = (typeof params.sinceMs === "number" ? 1 : 0) + (typeof params.untilMs === "number" ? 1 : 0); const maxSessionIdsPerQuery = Math.max(1, SQLITE_MAX_MESSAGE_QUERY_ARGS - reservedArgs); @@ -478,7 +489,7 @@ export async function iterAssistantMessagesForSessions(params: { sinceMs: params.sinceMs, untilMs: params.untilMs, }); - const rows = conn.all(q.sql, q.args); + const rows = conn.all(q.sql, q.args); messages.push(...mapAssistantMessages(rows)); } diff --git a/src/lib/tui-compact-format.ts b/src/lib/tui-compact-format.ts index a392d1b6..2a4d0288 100644 --- a/src/lib/tui-compact-format.ts +++ b/src/lib/tui-compact-format.ts @@ -95,7 +95,7 @@ function formatCompactValueEntrySegment( ): string | null { const name = getProviderName(entry); const value = compactText(entry.value); - const reset = formatResetCountdown(entry.resetTimeIso); + const reset = formatResetCountdown(entry.resetTimeIso, { compactUnits: true }); const segment = [name, value, reset].filter(Boolean).join(" - "); return segment || null; } @@ -159,7 +159,9 @@ function buildSemanticCandidate( const label = compactText(interpretation.label); const prefix = compactText([provider, label].filter(Boolean).join(": ")); const displayValue = compactText( - [value, formatResetCountdown(entry.resetTimeIso)].filter(Boolean).join(" "), + [value, formatResetCountdown(entry.resetTimeIso, { compactUnits: true })] + .filter(Boolean) + .join(" "), ); const segment = compactText([prefix, displayValue].filter(Boolean).join(" ")); if (!segment) return null; @@ -210,7 +212,7 @@ function formatCompactEntryCandidates(params: { const value = compactText( [ formatCompactPercentLabel(entry.percentRemaining, params.percentDisplayMode), - formatResetCountdown(entry.resetTimeIso), + formatResetCountdown(entry.resetTimeIso, { compactUnits: true }), ] .filter(Boolean) .join(" "), diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index ae744529..0d34453c 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -107,7 +107,11 @@ async function getQuotaMessage( return; } - const formatStyle = resolveQuotaFormatStyle(config.formatStyle); + const rootFormatStyle = resolveQuotaFormatStyle(config.formatStyle); + const formatStyle = + surface === "sidebar" && config.tuiSidebarPanel.formatStyle + ? resolveQuotaFormatStyle(config.tuiSidebarPanel.formatStyle) + : rootFormatStyle; const result = await collectQuotaRenderData({ client: runtime.client, resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, @@ -126,9 +130,7 @@ async function getQuotaMessage( data, config: { ...config, - formatStyle: config.tuiSidebarPanel.formatStyle - ? resolveQuotaFormatStyle(config.tuiSidebarPanel.formatStyle) - : formatStyle, + formatStyle, }, }).join("\n") : undefined @@ -138,7 +140,7 @@ async function getQuotaMessage( layout: config.layout, entries: data?.entries ?? [], errors: data?.errors ?? [], - style: resolveQuotaFormatStyle(config.formatStyle), + style: rootFormatStyle, percentDisplayMode: config.percentDisplayMode, resetTimeDecimals: config.resetTimeDecimals, sessionTokens: data?.sessionTokens, diff --git a/tests/format-utils.test.ts b/tests/format-utils.test.ts index 3fd502e0..5b0955ac 100644 --- a/tests/format-utils.test.ts +++ b/tests/format-utils.test.ts @@ -19,4 +19,12 @@ describe("formatResetCountdown", () => { expect(formatResetCountdown("2026-01-15T10:00:00.000Z")).toBe("reset"); expect(formatResetCountdown("2026-01-15T09:59:59.999Z")).toBe("reset"); }); + + it("separates exact countdown units by default", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-15T10:00:00.000Z")); + + expect(formatResetCountdown("2026-01-21T05:49:00.000Z")).toBe("5d 19h 49m"); + expect(formatResetCountdown("2026-01-15T12:14:00.000Z")).toBe("2h 14m"); + }); }); diff --git a/tests/format.test.ts b/tests/format.test.ts index b027f4c9..915525de 100644 --- a/tests/format.test.ts +++ b/tests/format.test.ts @@ -184,7 +184,7 @@ describe("formatQuotaRows", () => { ], }); - expect(out).toContain("2h14m"); + expect(out).toContain("2h 14m"); expect(out).not.toContain("2.5h"); }); @@ -380,7 +380,7 @@ describe("formatQuotaRows", () => { ], }); - expect(out).toContain("5d16h48m"); + expect(out).toContain("5d 16h 48m"); expect(out).not.toMatch(/5d\s*$/mu); expect(out).not.toContain("5.7d"); }); @@ -735,7 +735,7 @@ describe("formatQuotaRows", () => { const lines = out.split("\n"); expect(lines[0]).toBe("[Copilot] (personal) Monthly"); - expect(lines[1]?.trim()).toBe("21d20h10m"); + expect(lines[1]?.trim()).toBe("21d 20h 10m"); expect(lines[2]).toContain("75% left"); expect(lines.every((line) => line.length <= 36)).toBe(true); }); diff --git a/tests/lib.opencode-storage.test.ts b/tests/lib.opencode-storage.test.ts index 9bddaea0..a3d665f2 100644 --- a/tests/lib.opencode-storage.test.ts +++ b/tests/lib.opencode-storage.test.ts @@ -46,8 +46,11 @@ describe("opencode storage multi-session reads", () => { const conn = { get: vi.fn(() => ({ r: "assistant" })), all: vi.fn((sql: string, params?: unknown[]) => { - expect(sql).toContain("json_extract(data, '$.time.completed')"); - expect(sql).toContain("ORDER BY CAST(json_extract(data, '$.time.completed') AS REAL)"); + expect(sql).toContain( + "CASE WHEN json_valid(data) THEN json_extract(data, '$.time.completed') END", + ); + expect(sql).toContain("ORDER BY CAST(CASE WHEN json_valid(data)"); + expect(sql).not.toMatch(/SELECT[\s\S]*\bdata\b\s+FROM/); expect(sql).not.toContain("time_created >="); expect(params).toEqual([completedSinceMs, completedUntilMs]); return [ @@ -55,23 +58,18 @@ describe("opencode storage multi-session reads", () => { id: "cross-cutoff", session_id: "ses_one", time_created: completedSinceMs - 60_000, - data: JSON.stringify({ - role: "assistant", - providerID: "openai", - modelID: "gpt-5", - time: { completed: completedSinceMs + 1 }, - }), - }, - { - id: "unfinished", - session_id: "ses_two", - time_created: completedSinceMs + 1, - data: JSON.stringify({ - role: "assistant", - providerID: "openai", - modelID: "gpt-5", - time: {}, - }), + role: "assistant", + provider_id: "openai", + model_id: "gpt-5", + tokens_input: 10, + tokens_output: 5, + tokens_reasoning: 2, + tokens_cache_read: 3, + tokens_cache_write: 4, + cost: 0.01, + time_completed: completedSinceMs + 1, + agent: "build", + mode: "primary", }, ]; }), @@ -87,9 +85,101 @@ describe("opencode storage multi-session reads", () => { expect(messages.map((message) => message.id)).toEqual(["cross-cutoff"]); expect(messages[0]?.time?.completed).toBe(completedSinceMs + 1); + expect(messages[0]).toMatchObject({ + role: "assistant", + providerID: "openai", + modelID: "gpt-5", + tokens: { input: 10, output: 5, reasoning: 2, cache: { read: 3, write: 4 } }, + cost: 0.01, + agent: "build", + mode: "primary", + }); expect(conn.close).toHaveBeenCalledOnce(); }); + it("projects normal assistant reads without selecting message payloads", async () => { + const conn = { + get: vi.fn(() => ({ r: "assistant" })), + all: vi.fn((sql: string) => { + expect(sql).toContain("lower(CASE WHEN json_valid(data)"); + expect(sql).not.toMatch(/SELECT[\s\S]*\bdata\b\s+FROM/); + return [ + { + id: "msg_projected", + session_id: "ses_one", + time_created: 100, + role: "assistant", + provider_id: "openai", + model_id: "gpt-5", + tokens_input: 11, + tokens_output: 7, + tokens_reasoning: null, + tokens_cache_read: 2, + tokens_cache_write: 1, + cost: 0.02, + time_completed: 101, + agent: null, + mode: null, + }, + ]; + }), + close: vi.fn(), + }; + sqliteMocks.openOpenCodeSqliteReadOnly.mockResolvedValue(conn); + + const { iterAssistantMessages } = await import("../src/lib/opencode-storage.js"); + const messages = await iterAssistantMessages({ sinceMs: 100, untilMs: 200 }); + + expect(messages).toEqual([ + { + id: "msg_projected", + sessionID: "ses_one", + role: "assistant", + providerID: "openai", + modelID: "gpt-5", + tokens: { input: 11, output: 7, cache: { read: 2, write: 1 } }, + cost: 0.02, + time: { created: 100, completed: 101 }, + agent: undefined, + mode: undefined, + }, + ]); + }); + + it("guards malformed JSON without reading payloads", async () => { + const conn = { + get: vi.fn(() => ({ r: "assistant" })), + all: vi.fn((sql: string) => { + expect(sql).toContain("CASE WHEN json_valid(data) THEN json_extract(data, '$.role') END"); + expect(sql).not.toMatch(/SELECT[\s\S]*\bdata\b\s+FROM/); + return []; + }), + close: vi.fn(), + }; + sqliteMocks.openOpenCodeSqliteReadOnly.mockResolvedValue(conn); + + const { iterAssistantMessages } = await import("../src/lib/opencode-storage.js"); + await expect(iterAssistantMessages({})).resolves.toEqual([]); + }); + + it("does not fall back to scanning message payloads without SQLite JSON support", async () => { + const conn = { + get: vi.fn(() => null), + all: vi.fn(), + close: vi.fn(), + }; + sqliteMocks.openOpenCodeSqliteReadOnly.mockResolvedValue(conn); + + const { getOpenCodeDbStats, iterAssistantMessages, iterCompletedAssistantMessages } = + await import("../src/lib/opencode-storage.js"); + + await expect(iterAssistantMessages({})).resolves.toEqual([]); + await expect(iterCompletedAssistantMessages({})).resolves.toEqual([]); + await expect(getOpenCodeDbStats()).resolves.toMatchObject({ assistantMessageCount: 0 }); + + expect(conn.all).not.toHaveBeenCalled(); + }); + it("chunks large session queries below the SQLite bind limit and preserves message order", async () => { const conn = { all: vi.fn((_: string, params?: unknown[]) => { @@ -105,7 +195,7 @@ describe("opencode storage multi-session reads", () => { id: "msg-second-batch", session_id: "ses_999", time_created: 10, - data: JSON.stringify({ role: "assistant" }), + role: "assistant", }, ]; } @@ -115,11 +205,11 @@ describe("opencode storage multi-session reads", () => { id: "msg-first-batch", session_id: "ses_000", time_created: 20, - data: JSON.stringify({ role: "assistant" }), + role: "assistant", }, ]; }), - get: vi.fn(), + get: vi.fn(() => ({ r: "assistant" })), close: vi.fn(), }; sqliteMocks.openOpenCodeSqliteReadOnly.mockResolvedValue(conn); diff --git a/tests/providers.cursor.surfaces.test.ts b/tests/providers.cursor.surfaces.test.ts index 5ba8fdde..a9887e4e 100644 --- a/tests/providers.cursor.surfaces.test.ts +++ b/tests/providers.cursor.surfaces.test.ts @@ -80,8 +80,9 @@ describe("Cursor structured four-surface formatting", () => { for (const output of [outputs.command, outputs.toast, outputs.sidebar]) { expect(output).toContain("Auto+Composer spend"); expect(output).toContain("USD 1.25"); - expect(output).toMatch(/\b\d+d\d+h\d+m\b/u); + expect(output).toContain("31d 0h 0m"); } + expect(outputs.compact).toContain("31d0h0m"); expect(outputs.command).toContain("Used: USD 5.00"); expect(outputs.command).toContain("Limit: USD 20.00"); expect(outputs.command).toContain("Remaining: USD 15.00"); diff --git a/tests/providers.kilo.surfaces.test.ts b/tests/providers.kilo.surfaces.test.ts index e6c65759..ed3da4a0 100644 --- a/tests/providers.kilo.surfaces.test.ts +++ b/tests/providers.kilo.surfaces.test.ts @@ -116,8 +116,9 @@ describe("Kilo Gateway structured four-surface formatting", () => { expect(output).not.toContain("$"); } for (const output of [outputs.command, outputs.toast, outputs.sidebar]) { - expect(output).toMatch(/\b\d+d\d+h\d+m\b/u); + expect(output).toContain("31d 0h 0m"); } + expect(outputs.compact).toContain("31d0h0m"); }); it("keeps the Gateway fallback as one total-balance quantity", () => { diff --git a/tests/quota-command-format.test.ts b/tests/quota-command-format.test.ts index f59913b8..58dae9ea 100644 --- a/tests/quota-command-format.test.ts +++ b/tests/quota-command-format.test.ts @@ -93,17 +93,17 @@ describe("formatQuotaCommand", () => { expect(out.match(/[█░]{10}/gu)).toHaveLength(4); expect(lines.slice(2).join("\n")).toMatchInlineSnapshot(` "→ [Copilot] (personal) - Quota █████████░ 86% left | 42/300 | reset 12h0m + Quota █████████░ 86% left | 42/300 | reset 12h 0m → [Copilot] (business) - Usage 9 used | 2026-01 | org=acme-corp | user=alice | reset 16d12h0m + Usage 9 used | 2026-01 | org=acme-corp | user=alice | reset 16d 12h 0m → [OpenAI] (Pro) - 5h quota ████░░░░░░ 42% left | reset 2h0m - Week quota ████████░░ 81% left | reset 3d0h0m + 5h quota ████░░░░░░ 42% left | reset 2h 0m + Week quota ████████░░ 81% left | reset 3d 0h 0m → [Antigravity (acct)] - Claude ███████░░░ 67% left | reset 3h0m + Claude ███████░░░ 67% left | reset 3h 0m Session input/output tokens openai/gpt-5: 1.2K in | 456 cached | 567 out @@ -350,8 +350,8 @@ describe("formatQuotaCommand", () => { const rows = output.split("\n").filter((line) => line.includes(" | reset ")); expect(rows).toEqual([ - " 5h quota ██████░░░░ 60% left | 2/5 | reset 5h0m", - " Day quota ████████░░ 80% left | 2/10 | reset 11h0m", + " 5h quota ██████░░░░ 60% left | 2/5 | reset 5h 0m", + " Day quota ████████░░ 80% left | 2/10 | reset 11h 0m", ]); expect(output).not.toContain("```"); expect(output).not.toMatch(/^## /mu); @@ -385,7 +385,7 @@ describe("formatQuotaCommand", () => { errors: [], }); - expect(out).toContain("reset 2h40m"); + expect(out).toContain("reset 2h 40m"); }); it("renders an expired reset once and omits a missing reset", () => { diff --git a/tests/tui-compact-format.test.ts b/tests/tui-compact-format.test.ts index 18418ec6..394bd62a 100644 --- a/tests/tui-compact-format.test.ts +++ b/tests/tui-compact-format.test.ts @@ -18,14 +18,14 @@ describe("buildCompactQuotaStatusLine", () => { { name: "OpenAI Weekly", percentRemaining: 50, - resetTimeIso: "2026-01-17T15:14:00.000Z", + resetTimeIso: "2026-01-21T05:49:00.000Z", }, ], errors: [], }, }); - expect(line).toBe("OpenAI Weekly 50% 2d5h14m"); + expect(line).toBe("OpenAI Weekly 50% 5d19h49m"); }); it("renders expired provider resets once", () => { diff --git a/tests/tui-sidebar-format.test.ts b/tests/tui-sidebar-format.test.ts index 0c98271e..4e4edb0f 100644 --- a/tests/tui-sidebar-format.test.ts +++ b/tests/tui-sidebar-format.test.ts @@ -342,7 +342,7 @@ describe("buildSidebarQuotaPanelLines", () => { }, }); - expect(lines.join("\n")).toContain("2h14m"); + expect(lines.join("\n")).toContain("2h 14m"); expect(lines.join("\n")).not.toContain("2.5h"); }); diff --git a/tests/tui-smoke.test.ts b/tests/tui-smoke.test.ts index e5e1a875..908b7bfc 100644 --- a/tests/tui-smoke.test.ts +++ b/tests/tui-smoke.test.ts @@ -1830,6 +1830,6 @@ describe("tui plugin smoke", () => { const rendered = registration.slots.session_prompt({}, { session_id: "session-reset" }) as any; const hint = rendered.props.children[1]; - expect(hint.props.children[2].props.children).toBe("50% | 2d5h14m"); + expect(hint.props.children[2].props.children).toBe("50% | 2d 5h 14m"); }); }); diff --git a/tests/tui-v2-format-style.test.ts b/tests/tui-v2-format-style.test.ts new file mode 100644 index 00000000..e6f46fcf --- /dev/null +++ b/tests/tui-v2-format-style.test.ts @@ -0,0 +1,109 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { buildSidebarQuotaPanelLines, collectQuotaRenderData, resolveQuotaRuntimeContext } = + vi.hoisted(() => ({ + buildSidebarQuotaPanelLines: vi.fn(), + collectQuotaRenderData: vi.fn(), + resolveQuotaRuntimeContext: vi.fn(), + })); + +vi.mock("../src/lib/quota-render-data.js", async () => { + const actual = await vi.importActual( + "../src/lib/quota-render-data.js", + ); + return { ...actual, collectQuotaRenderData }; +}); + +vi.mock("../src/lib/quota-runtime-context.js", async () => { + const actual = await vi.importActual( + "../src/lib/quota-runtime-context.js", + ); + return { ...actual, resolveQuotaRuntimeContext }; +}); + +vi.mock("../src/lib/tui-sidebar-format.js", async () => { + const actual = await vi.importActual( + "../src/lib/tui-sidebar-format.js", + ); + return { ...actual, buildSidebarQuotaPanelLines }; +}); + +import plugin from "../src/tui-v2.tsx"; + +describe("V2 sidebar format style", () => { + afterEach(() => vi.unstubAllGlobals()); + + it("projects all sidebar windows before formatting when it overrides singleWindow", async () => { + vi.stubGlobal("React", { + createElement: (type: unknown, props: Record) => + typeof type === "function" ? type(props) : { type, props }, + }); + resolveQuotaRuntimeContext.mockResolvedValue({ + client: {}, + config: { + enabled: true, + enableToast: true, + formatStyle: "singleWindow", + percentDisplayMode: "remaining", + resetTimeDecimals: undefined, + toastDurationMs: 5000, + tuiSidebarPanel: { enabled: true, formatStyle: "allWindows" }, + }, + configMeta: {}, + providers: [], + resolveRuntimeProviderIds: vi.fn(), + session: { sessionID: "session-1" }, + }); + collectQuotaRenderData.mockImplementation(async ({ formatStyle }) => ({ + active: [{ id: "copilot" }], + data: { + entries: + formatStyle === "allWindows" + ? [ + { name: "Copilot 5h", percentRemaining: 50 }, + { name: "Copilot Weekly", percentRemaining: 80 }, + ] + : [{ name: "Copilot 5h", percentRemaining: 50 }], + errors: [], + }, + })); + buildSidebarQuotaPanelLines.mockImplementation(({ data }) => + data.entries.map((entry: { name: string }) => entry.name), + ); + + let sidebarRender: ((props: { sessionID: string }) => unknown) | undefined; + plugin.setup({ + client: {}, + data: { on: vi.fn(() => vi.fn()) }, + keymap: { layer: vi.fn() }, + ui: { + slot: vi.fn((claim) => { + if (claim.append === "app") claim.render(); + if (claim.append === "sidebar.content") sidebarRender = claim.render; + return vi.fn(); + }), + toast: { show: vi.fn() }, + dialog: { alert: vi.fn(), prompt: vi.fn(), set: vi.fn() }, + }, + } as any); + + sidebarRender?.({ sessionID: "session-1" }); + + await vi.waitFor(() => { + expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + entries: [ + expect.objectContaining({ name: "Copilot 5h" }), + expect.objectContaining({ name: "Copilot Weekly" }), + ], + }), + config: expect.objectContaining({ formatStyle: "allWindows" }), + }), + ); + }); + expect(collectQuotaRenderData).toHaveBeenCalledWith( + expect.objectContaining({ formatStyle: "allWindows" }), + ); + }); +}); From f29a485e05b3a4ab46f04dc33895759dfe005958 Mon Sep 17 00:00:00 2001 From: Ran Sagy Date: Thu, 17 Sep 2026 12:47:45 +0300 Subject: [PATCH 17/97] fix(tokens): read sessions and messages from the OpenCode 2 V2 store OpenCode 2 moved session and message storage to the `session_v2` and `session_message` tables; the legacy `session`/`message` tables stopped receiving writes. The token accounting still read only the legacy tables, so: - `/tokens_session` and `/tokens_session_all` failed with `SessionNotFoundError` for sessions that never had a legacy row; - sessions that do have legacy rows silently missed every message written after the switch (on one live database 79% of the last week`s assistant messages), and descendant trees missed all V2-only sessions via readAllSessionsIndex. Read the V2 store instead: session lookups and the index target `session_v2`, and message reads target `session_message` with `type = 'assistant'` providing the role. V2 assistant `data` keeps the same JSON shape for tokens/cost/time.completed, so the existing projected-column queries carry over with provider/model now extracted from `$.model.*`. No legacy fallback: the branch targets current OpenCode 2 builds, where the V2 store also contains migrated history. --- src/lib/opencode-storage.ts | 40 +++++++++++++++--------------- tests/lib.opencode-sqlite.test.ts | 17 ++++++++----- tests/lib.opencode-storage.test.ts | 8 ++++-- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/lib/opencode-storage.ts b/src/lib/opencode-storage.ts index 641629da..248c9129 100644 --- a/src/lib/opencode-storage.ts +++ b/src/lib/opencode-storage.ts @@ -8,10 +8,10 @@ import { pickFirstExistingPath } from "./path-pick.js"; /** * Error thrown when a session is not found. * - * With OpenCode >=1.2, sessions/messages live in SQLite (`opencode.db`). - * This is thrown by iterAssistantMessagesForSession when the database is - * missing/unreadable, the session id is invalid, or the session row does - * not exist. + * OpenCode 2 stores sessions and messages in SQLite (`opencode.db`) under the + * `session_v2` and `session_message` tables. This is thrown by + * iterAssistantMessagesForSession when the database is missing/unreadable, the + * session id is invalid, or the session row does not exist. */ export class SessionNotFoundError extends Error { constructor( @@ -211,9 +211,9 @@ const PROJECTED_MESSAGE_COLUMNS = [ "id", "session_id", "time_created", - `${guardedJsonScalar("$.role")} AS role`, - `${guardedJsonScalar("$.providerID")} AS provider_id`, - `${guardedJsonScalar("$.modelID")} AS model_id`, + "type AS role", + `${guardedJsonScalar("$.model.providerID")} AS provider_id`, + `${guardedJsonScalar("$.model.id")} AS model_id`, `${guardedJsonScalar("$.tokens.input")} AS tokens_input`, `${guardedJsonScalar("$.tokens.output")} AS tokens_output`, `${guardedJsonScalar("$.tokens.reasoning")} AS tokens_reasoning`, @@ -222,10 +222,9 @@ const PROJECTED_MESSAGE_COLUMNS = [ `${guardedJsonScalar("$.cost")} AS cost`, `${guardedJsonScalar("$.time.completed")} AS time_completed`, `${guardedJsonScalar("$.agent")} AS agent`, - `${guardedJsonScalar("$.mode")} AS mode`, ].join(", "); -const ASSISTANT_ROLE_EXPRESSION = `lower(${guardedJsonScalar("$.role")}) = 'assistant'`; +const ASSISTANT_FILTER = `type = 'assistant'`; function buildMessageQuery(params: { sessionID?: string; @@ -262,10 +261,10 @@ function buildMessageQuery(params: { args.push(params.untilMs); } - where.push(ASSISTANT_ROLE_EXPRESSION); + where.push(ASSISTANT_FILTER); const sql = - `SELECT ${PROJECTED_MESSAGE_COLUMNS} FROM "message"` + + `SELECT ${PROJECTED_MESSAGE_COLUMNS} FROM "session_message"` + (where.length ? ` WHERE ${where.join(" AND ")}` : "") + ` ORDER BY time_created ASC, id ASC`; @@ -314,7 +313,7 @@ function buildCompletedAssistantQuery(params: { const completedScalar = guardedJsonScalar("$.time.completed"); const completedExpression = `CAST(${completedScalar} AS REAL)`; const where = [ - ASSISTANT_ROLE_EXPRESSION, + ASSISTANT_FILTER, `CASE WHEN json_valid(data) THEN json_type(data, '$.time.completed') END IN ('integer', 'real')`, `${completedExpression} > 0`, ]; @@ -331,7 +330,7 @@ function buildCompletedAssistantQuery(params: { return { sql: - `SELECT ${PROJECTED_MESSAGE_COLUMNS} FROM "message"` + + `SELECT ${PROJECTED_MESSAGE_COLUMNS} FROM "session_message"` + ` WHERE ${where.join(" AND ")}` + ` ORDER BY ${completedExpression} ASC, id ASC`, args, @@ -358,13 +357,13 @@ export async function getOpenCodeDbStats(): Promise { const conn = await db.open(); try { - const sessionRow = conn.get<{ c: number }>(`SELECT count(*) as c FROM "session"`); - const messageRow = conn.get<{ c: number }>(`SELECT count(*) as c FROM "message"`); + const sessionRow = conn.get<{ c: number }>(`SELECT count(*) as c FROM "session_v2"`); + const messageRow = conn.get<{ c: number }>(`SELECT count(*) as c FROM "session_message"`); let assistantCount = 0; if (await hasJsonExtract(conn)) { const a = conn.get<{ c: number }>( - `SELECT count(*) as c FROM "message" WHERE ${guardedJsonScalar("$.role")} = 'assistant'`, + `SELECT count(*) as c FROM "session_message" WHERE type = 'assistant'`, ); assistantCount = typeof a?.c === "number" ? a.c : 0; } @@ -442,9 +441,10 @@ export async function iterAssistantMessagesForSession(params: { const conn = await db.open(); try { - const exists = conn.get<{ ok: number }>(`SELECT 1 as ok FROM "session" WHERE id = ? LIMIT 1`, [ - sessionID, - ]); + const exists = conn.get<{ ok: number }>( + `SELECT 1 as ok FROM "session_v2" WHERE id = ? LIMIT 1`, + [sessionID], + ); if (!exists) { throw new SessionNotFoundError(sessionID, db.dbPath); } @@ -508,7 +508,7 @@ export async function readAllSessionsIndex(): Promise( - `SELECT id, title, parent_id, time_created, time_updated FROM "session" ORDER BY time_created ASC, id ASC`, + `SELECT id, title, parent_id, time_created, time_updated FROM "session_v2" ORDER BY time_created ASC, id ASC`, ); for (const row of rows) { diff --git a/tests/lib.opencode-sqlite.test.ts b/tests/lib.opencode-sqlite.test.ts index 59ec9c6e..bfcd7c66 100644 --- a/tests/lib.opencode-sqlite.test.ts +++ b/tests/lib.opencode-sqlite.test.ts @@ -89,32 +89,37 @@ describe("opencode sqlite adapter", () => { runtimePaths.dataDirs = [dir]; const writer = new sqlite.DatabaseSync(dbPath); writer.exec(` - CREATE TABLE "message" ( + CREATE TABLE "session_v2" ( + id TEXT PRIMARY KEY + ); + CREATE TABLE "session_message" ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, + type TEXT NOT NULL, + seq INTEGER NOT NULL, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, data TEXT NOT NULL ); `); const insert = writer.prepare( - `INSERT INTO "message" (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)`, + `INSERT INTO "session_message" (id, session_id, type, seq, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?, ?)`, ); const add = ( id: string, - role: "assistant" | "user", + type: "assistant" | "user", created: number, completed?: number, ): void => { insert.run( id, "ses_accounting", + type, + created, created, completed ?? created, JSON.stringify({ - role, - providerID: "qwen-code", - modelID: "qwen-plus", + model: { id: "qwen-plus", providerID: "qwen-code" }, time: completed === undefined ? { created } : { created, completed }, }), ); diff --git a/tests/lib.opencode-storage.test.ts b/tests/lib.opencode-storage.test.ts index a3d665f2..ba0747f6 100644 --- a/tests/lib.opencode-storage.test.ts +++ b/tests/lib.opencode-storage.test.ts @@ -46,6 +46,9 @@ describe("opencode storage multi-session reads", () => { const conn = { get: vi.fn(() => ({ r: "assistant" })), all: vi.fn((sql: string, params?: unknown[]) => { + expect(sql).toContain(`FROM "session_message"`); + expect(sql).toContain("type = 'assistant'"); + expect(sql).toContain("type AS role"); expect(sql).toContain( "CASE WHEN json_valid(data) THEN json_extract(data, '$.time.completed') END", ); @@ -101,7 +104,8 @@ describe("opencode storage multi-session reads", () => { const conn = { get: vi.fn(() => ({ r: "assistant" })), all: vi.fn((sql: string) => { - expect(sql).toContain("lower(CASE WHEN json_valid(data)"); + expect(sql).toContain("type = 'assistant'"); + expect(sql).toContain("type AS role"); expect(sql).not.toMatch(/SELECT[\s\S]*\bdata\b\s+FROM/); return [ { @@ -150,7 +154,7 @@ describe("opencode storage multi-session reads", () => { const conn = { get: vi.fn(() => ({ r: "assistant" })), all: vi.fn((sql: string) => { - expect(sql).toContain("CASE WHEN json_valid(data) THEN json_extract(data, '$.role') END"); + expect(sql).toContain("CASE WHEN json_valid(data) THEN json_extract(data, '$.tokens.input') END"); expect(sql).not.toMatch(/SELECT[\s\S]*\bdata\b\s+FROM/); return []; }), From 776406fc6e6c6aa6e90378cc55d8e5673e48fdb7 Mon Sep 17 00:00:00 2001 From: Ran Sagy Date: Wed, 23 Sep 2026 17:21:45 +0300 Subject: [PATCH 18/97] fix(quota): move Zen and Go auth to the OpenCode Console platform OpenCode migrated workspaces to the OpenCode Console platform, which broke both quota providers: - The Zen billing page moved to the Console; the stored workspace auth cookie now redirects to /console/login and the SSR scrape fails. The Console exposes billing and usage REST APIs that accept the CLI`s OAuth device-flow credential (stored in the credential database under the `opencode` integration) as a Bearer token. - The migration removed the workspace API keys from the credential database, so the Go usage API (which authenticates with an API key) went to `unsupported type` for the new OAuth-only database. The key written by pre-2.0 `opencode auth login` (auth.json) remains valid. - Add a read-only console credential reader (access token, refresh, expiry, orgID/orgName) - the CLI refreshes the token itself; an expired token is reported with a re-login hint instead of being refreshed from the plugin. - Rewrite the Zen provider to query /console/api/billing/status, /api/billing/account, /api/billing/auto-recharge and /api/usage/cost-by-day, keeping the previous display contract (balance, monthly limit from creditLimitMicroCents or plugin config, monthly usage summed for the current calendar month, auto-reload state). - Go provider: merge legacy auth.json rows into the multi-connection fan-out (dropped by the earlier auth.json removal, justified again by the migration) and skip Console OAuth rows that cannot authorize the usage API. - OpenAI: stop treating the console OAuth credential as an OpenAI credential (it produced a bogus 401 error group after migration). readAuthFile fills integrations missing from the database from auth.json so the Go resolver sees the migrated workspace key. --- src/lib/openai.ts | 12 + src/lib/opencode-auth.ts | 45 ++ src/lib/opencode-console-auth.ts | 71 ++++ src/lib/opencode-zen.ts | 367 +++++----------- src/providers/openai.ts | 8 +- src/providers/opencode-go.ts | 13 +- src/providers/opencode-zen.ts | 70 ++-- tests/lib.opencode-auth.test.ts | 8 +- tests/lib.opencode-console-auth.test.ts | 107 +++++ tests/lib.opencode-zen.test.ts | 536 +++++++----------------- tests/providers.opencode-go.test.ts | 38 +- tests/providers.opencode-zen.test.ts | 274 +++++------- 12 files changed, 714 insertions(+), 835 deletions(-) create mode 100644 src/lib/opencode-console-auth.ts create mode 100644 tests/lib.opencode-console-auth.test.ts diff --git a/src/lib/openai.ts b/src/lib/openai.ts index 8fe28c5e..db5df58b 100644 --- a/src/lib/openai.ts +++ b/src/lib/openai.ts @@ -203,6 +203,18 @@ function getOpenAIOAuthEntry( continue; } + // OpenCode 2 console credentials live under the `opencode` integration as + // OAuth entries; they authorize Console APIs, not OpenAI. + if (sourceKey === "opencode") { + const consoleEntry = entry as OpenAIOAuthData & { + methodID?: unknown; + metadata?: { orgID?: unknown } | null; + }; + if (consoleEntry.methodID === "server" || consoleEntry.metadata?.orgID != null) { + continue; + } + } + const accessToken = typeof entry.access === "string" ? entry.access.trim() : ""; if (accessToken) { return { sourceKey, entry, accessToken }; diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 6734c2a0..de4ff09d 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -115,9 +115,54 @@ export async function readAuthFile(): Promise { for (const row of rows) { if (!(row.integrationId in auth)) auth[row.integrationId] = row.value; } + + // OpenCode's console migration removed workspace API keys from the + // credential database while the entry written by `opencode auth login` + // (pre-2.0) in auth.json can remain valid for key-based APIs. Fill in + // integrations that the database does not cover. + const legacy = await readLegacyAuthEntries(); + for (const [integrationId, entry] of Object.entries(legacy ?? {})) { + if (!(integrationId in auth)) auth[integrationId] = entry; + } + return Object.keys(auth).length > 0 ? (auth as AuthData) : null; } +async function readLegacyAuthEntries(): Promise { + for (const path of getAuthPaths()) { + if (!existsSync(path)) continue; + try { + const { readFile } = await import("fs/promises"); + const raw = await readFile(path, "utf8"); + const parsed: unknown = JSON.parse(raw); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed as AuthData; + } + } catch { + // Unreadable legacy files are ignored; the database remains the source. + } + } + return null; +} + +/** Credential rows synthesized from the legacy auth.json file. */ +export async function readLegacyAuthRows(): Promise { + const legacy = await readLegacyAuthEntries(); + if (!legacy) return []; + const rows: CredentialRow[] = []; + for (const [integrationId, entry] of Object.entries(legacy)) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue; + rows.push({ + id: `auth-json:${integrationId}`, + integrationId, + label: "legacy", + active: true, + value: entry as Record, + }); + } + return rows; +} + export async function readCredentialRows(): Promise { return readCredentialRowsFromDatabases(getCredentialDatabasePaths()); } diff --git a/src/lib/opencode-console-auth.ts b/src/lib/opencode-console-auth.ts new file mode 100644 index 00000000..2ca8c424 --- /dev/null +++ b/src/lib/opencode-console-auth.ts @@ -0,0 +1,71 @@ +/** + * OpenCode Console credential reader + * + * OpenCode 2 stores its OpenCode Console account in the credential database + * under the `opencode` integration as an OAuth entry created by the device + * flow (`client_id: opencode-cli`, base `https://opencode.ai/console`). + * Consumers use the access token as `Authorization: Bearer` against console + * APIs and `metadata.orgID` for org-scoped routes. + * + * The reader is intentionally read-only: the CLI refreshes the token via + * `/console/auth/device/token` on its own; an expired token is reported as + * `expired` so callers can tell the user to re-run `opencode auth login`. + */ + +import { readCredentialRows } from "./opencode-auth.js"; + +export const OPENCODE_CONSOLE_BASE_URL = "https://opencode.ai/console"; +export const OPENCODE_CONSOLE_INTEGRATION_ID = "opencode"; + +export interface OpenCodeConsoleCredential { + accessToken: string; + refreshToken?: string; + expiresAt?: number; + orgId?: string; + orgName?: string; + email?: string; + server?: string; +} + +export type OpenCodeConsoleAuthState = + | { state: "none" } + | { state: "configured"; credential: OpenCodeConsoleCredential } + | { state: "expired"; credential: OpenCodeConsoleCredential } + | { state: "invalid"; error: string }; + +export async function resolveOpenCodeConsoleAuth(params?: { + nowMs?: number; +}): Promise { + const rows = await readCredentialRows(); + const candidates = rows + .filter((row) => row.integrationId === OPENCODE_CONSOLE_INTEGRATION_ID) + .filter((row) => (row.value as Record | null)?.type === "oauth"); + + if (candidates.length === 0) return { state: "none" }; + + const row = candidates[0]!; + const value = row.value as Record; + const accessToken = typeof value.access === "string" ? value.access.trim() : ""; + if (!accessToken) { + return { state: "invalid", error: "OpenCode Console credential has no access token" }; + } + + const metadata = (value.metadata ?? null) as Record | null; + const credential: OpenCodeConsoleCredential = { + accessToken, + refreshToken: + typeof value.refresh === "string" && value.refresh.trim() ? value.refresh : undefined, + expiresAt: typeof value.expires === "number" ? value.expires : undefined, + orgId: typeof metadata?.orgID === "string" ? metadata.orgID : undefined, + orgName: typeof metadata?.orgName === "string" ? metadata.orgName : undefined, + email: typeof metadata?.email === "string" ? metadata.email : undefined, + server: typeof metadata?.server === "string" ? metadata.server : undefined, + }; + + const nowMs = params?.nowMs ?? Date.now(); + if (credential.expiresAt !== undefined && credential.expiresAt <= nowMs) { + return { state: "expired", credential }; + } + + return { state: "configured", credential }; +} diff --git a/src/lib/opencode-zen.ts b/src/lib/opencode-zen.ts index f756193e..3ef3be41 100644 --- a/src/lib/opencode-zen.ts +++ b/src/lib/opencode-zen.ts @@ -1,19 +1,14 @@ -import { sanitizeDisplayText } from "./display-sanitize.js"; import { fetchWithTimeout } from "./http.js"; -const BILLING_URL_PREFIX = "https://opencode.ai/workspace/"; -const BILLING_URL_SUFFIX = "/billing"; -const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0"; +const CONSOLE_BASE_URL = "https://opencode.ai/console"; const SCRAPE_TIMEOUT_MS = 10_000; /** - * Conversion used by the OpenCode billing-page values in PR #140. - * The source represents one US dollar as 100,000,000 billing units. + * Conversion used by the OpenCode Console billing APIs. + * The source represents one US dollar as 100,000,000 microcents. */ export const OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR = 100_000_000; -const SSR_FIELD_RE = /\b(balance|monthlyLimit|monthlyUsage)\s*:\s*(\d+(?:\.\d+)?)\b/g; - export interface OpenCodeZenBillingData { balance: number; monthlyLimit: number | null; @@ -28,273 +23,143 @@ export type OpenCodeZenResult = | { success: true; data: OpenCodeZenBillingData } | { success: false; error: string }; -function parseSsrBillingData(html: string): OpenCodeZenBillingData | null { - const fields: Record = {}; - for (const match of html.matchAll(SSR_FIELD_RE)) { - fields[match[1]] = Number(match[2]); - } - - if (!Number.isFinite(fields.balance) || fields.balance < 0) return null; - - return { - balance: fields.balance, - monthlyLimit: - Number.isFinite(fields.monthlyLimit) && fields.monthlyLimit >= 0 ? fields.monthlyLimit : null, - monthlyUsage: - Number.isFinite(fields.monthlyUsage) && fields.monthlyUsage >= 0 ? fields.monthlyUsage : null, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }; -} - -/** Extracts one balanced object/array while ignoring delimiters inside quoted strings. */ -function extractBalancedValue( - source: string, - start: number, -): { value: string; end: number } | null { - const open = source[start]; - if (open !== "{" && open !== "[") return null; - const close = open === "{" ? "}" : "]"; - - let depth = 0; - let quote: '"' | "'" | "`" | null = null; - let escaped = false; - for (let i = start; i < source.length; i++) { - const ch = source[i]; - if (quote !== null) { - if (escaped) { - escaped = false; - } else if (ch === "\\") { - escaped = true; - } else if (ch === quote) { - quote = null; - } - continue; - } - - if (ch === '"' || ch === "'" || ch === "`") { - quote = ch; - } else if (ch === open) { - depth++; - } else if (ch === close && --depth === 0) { - return { value: source.slice(start, i + 1), end: i }; - } - } - return null; +export interface OpenCodeConsoleQuotaAuth { + accessToken: string; } -function extractSsrAssignment(html: string, key: string): string | null { - const match = new RegExp( - `${key}\\[\\\\?"[^"]+\\\\?"\\]"]=\\$R\\[\\d+\\]=\\$R\\[\\d+\\]\\(\\$R\\[(\\d+)\\]` + - `[\\s\\S]*?\\$R\\[\\d+\\]\\(\\$R\\[\\1\\],\\$R\\[\\d+\\]=`, - ).exec(html); - if (!match) return null; - - const start = match.index + match[0].length; - return extractBalancedValue(html, start)?.value ?? null; +function asRecord(value: unknown): Record | null { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : null; } -function parseNewSsrBillingData(html: string): OpenCodeZenBillingData | null { - const object = extractSsrAssignment(html, "billing\\.get"); - if (!object) return null; - - const balanceMatch = object.match(/\bbalance\s*:\s*(-?\d+)/); - const limitMatch = object.match(/\bmonthlyLimit\s*:\s*(\d+)/); - const usageMatch = object.match(/\bmonthlyUsage\s*:\s*(\d+)/); - const reloadMatch = object.match(/\breload\s*:\s*(!0|!1|true|false)/); - const reloadAmountMatch = object.match(/\breloadAmount\s*:\s*(\d+)/); - const reloadTriggerMatch = object.match(/\breloadTrigger\s*:\s*(\d+)/); - if (!balanceMatch) return null; - - return { - balance: Math.max(0, Number(balanceMatch[1])), - monthlyLimit: limitMatch ? Number(limitMatch[1]) : null, - monthlyUsage: usageMatch ? Number(usageMatch[1]) : null, - lastPayment: null, - reload: reloadMatch ? reloadMatch[1] === "true" || reloadMatch[1] === "!0" : false, - reloadAmount: reloadAmountMatch ? Number(reloadAmountMatch[1]) : null, - reloadTrigger: reloadTriggerMatch ? Number(reloadTriggerMatch[1]) : null, - }; +function asMicroCents(value: unknown): number | null { + const parsed = typeof value === "string" ? Number(value) : typeof value === "number" ? value : Number.NaN; + return Number.isFinite(parsed) ? parsed : null; } -function parseNewSsrPaymentData(html: string): number | null { - const array = extractSsrAssignment(html, "payment\\.list"); - if (!array) return null; - - for (let i = 1; i < array.length - 1; i++) { - if (array[i] !== "{") continue; - - const extracted = extractBalancedValue(array, i); - if (!extracted) return null; - i = extracted.end; - - const amountMatch = extracted.value.match(/\bamount\s*:\s*(-?\d+(?:\.\d+)?)/); - const amount = amountMatch ? Number(amountMatch[1]) : Number.NaN; - if (!Number.isFinite(amount) || amount <= 0) continue; - - const timeRefunded = extracted.value.match(/\btimeRefunded\s*:\s*([^,}]+)/)?.[1].trim(); - const explicitlyRefunded = - /\brefunded\s*:\s*true\b/.test(extracted.value) || - (timeRefunded !== undefined && timeRefunded !== "null"); - if (!explicitlyRefunded) { - return amount / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; - } - } - return null; +function asDollars(microCents: number | null): number | null { + return microCents === null ? null : microCents / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; } -function parseDataSlotBillingData(html: string): OpenCodeZenBillingData | null { - let balance: number | null = null; - let monthlyLimit: number | null = null; - let monthlyUsage: number | null = null; - - const items = html.split(/data-slot="billing-item"/); - for (let index = 1; index < items.length; index++) { - const content = items[index]; - const labelMatch = content.match(/data-slot="billing-label">([^<]+)[^$]*\$?(\d+(?:,\d{3})*(?:\.\d+)?)/, - ); - if (!valueMatch) continue; - - const dollarAmount = Number.parseFloat(valueMatch[1].replace(/,/g, "")); - if (!Number.isFinite(dollarAmount) || dollarAmount < 0) continue; - - const label = labelMatch[1].trim().toLowerCase(); - if (label.includes("balance")) { - balance = dollarAmount * OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; - } else if (label.includes("monthly") && label.includes("limit")) { - monthlyLimit = dollarAmount; - } else if (label.includes("monthly") && label.includes("usage")) { - monthlyUsage = dollarAmount * OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; - } - } - - if (balance === null) return null; - return { - balance, - monthlyLimit, - monthlyUsage, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }; +function currentMonthStartDate(): string { + const now = new Date(); + return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}-01`; } -function parseSsrPaymentData(html: string): number | null { - const patterns = [ - /"payment\.list"\]\s*=\s*\[\s*\{[\s\S]*?"amount":\s*(\d+)/, - /"payment\.list"\s*:\s*\[[\s\S]*?"amount":\s*(\d+)/, - /__\$S\["payment\.list"\][\s\S]*?"amount":\s*(\d+)/, - ]; - - for (const pattern of patterns) { - const match = pattern.exec(html); - if (!match) continue; +type ConsoleJsonResponse = + | { ok: true; json: unknown } + | { ok: false; error: string }; - const amount = Number(match[1]); - if (Number.isFinite(amount) && amount > 0) { - return amount / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; - } - } - - return null; -} - -function parseDataSlotPaymentData(html: string): number | null { - const tableMatch = html.match( - //i, - ); - const tableHtml = tableMatch?.[0] ?? html; - const amountCellRe = /]*)>([\s\S]*?)<\/td>/gi; - - for (const match of tableHtml.matchAll(amountCellRe)) { - if (/data-refunded="true"/.test(match[1])) continue; - - const dollarMatch = match[2].match(/\$?(\d+(?:,\d{3})*(?:\.\d{1,2})?)/); - if (!dollarMatch) continue; - - const amount = Number.parseFloat(dollarMatch[1].replace(/,/g, "")); - if (Number.isFinite(amount) && amount > 0) return amount; - } - - return null; -} - -function sanitizeMessage(text: string, secrets: string[] = [], maxLength = 120): string { - let sanitized = sanitizeDisplayText(text).replace(/\s+/g, " ").trim(); - for (const secret of secrets) { - if (secret) sanitized = sanitized.split(secret).join("[redacted]"); - } - return (sanitized || "unknown").slice(0, maxLength); -} - -export async function queryOpenCodeZenQuota( - workspaceId: string, - authCookie: string, - options: { requestTimeoutMs?: number } = {}, -): Promise { +async function getConsoleJson( + path: string, + accessToken: string, + timeoutMs: number, +): Promise { try { - const url = `${BILLING_URL_PREFIX}${encodeURIComponent(workspaceId)}${BILLING_URL_SUFFIX}`; - return await fetchWithTimeout(url, { + return await fetchWithTimeout(`${CONSOLE_BASE_URL}${path}`, { request: { method: "GET", headers: { - "User-Agent": USER_AGENT, - Accept: "text/html", - Cookie: `auth=${authCookie}`, + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", }, }, - timeoutMs: options.requestTimeoutMs ?? SCRAPE_TIMEOUT_MS, + timeoutMs, consume: async (response) => { if (!response.ok) { - return { - success: false, - error: `OpenCode Zen billing error ${response.status}`, - }; + return { ok: false, error: `OpenCode Console API error ${response.status} (${path})` }; } - - const html = await response.text(); - const data = - parseNewSsrBillingData(html) ?? - parseSsrBillingData(html) ?? - parseDataSlotBillingData(html); - if (!data) { - return { - success: false, - error: - "Could not parse OpenCode Zen billing data (balance, monthlyLimit, monthlyUsage) from the billing page", - }; + let json: unknown = null; + try { + json = JSON.parse(await response.text()); + } catch { + json = null; } - - data.lastPayment = - parseNewSsrPaymentData(html) ?? - parseSsrPaymentData(html) ?? - parseDataSlotPaymentData(html); - return { success: true, data }; + return { ok: true, json }; }, }); } catch (error) { - return { - success: false, - error: sanitizeMessage(error instanceof Error ? error.message : String(error), [ - authCookie, - workspaceId, - ]), - }; + return { ok: false, error: error instanceof Error ? error.message : String(error) }; } } -export { - parseDataSlotBillingData as _parseDataSlotBillingData, - parseDataSlotPaymentData as _parseDataSlotPaymentData, - parseNewSsrBillingData as _parseNewSsrBillingData, - parseNewSsrPaymentData as _parseNewSsrPaymentData, - parseSsrBillingData as _parseSsrBillingData, - parseSsrPaymentData as _parseSsrPaymentData, -}; +function contractError(path: string): { success: false; error: string } { + return { + success: false, + error: `Could not parse OpenCode Console response (${path})`, + }; +} + +export async function queryOpenCodeZenQuota( + auth: { accessToken: string }, + options: { requestTimeoutMs?: number } = {}, +): Promise { + const timeoutMs = options.requestTimeoutMs ?? SCRAPE_TIMEOUT_MS; + + const statusResponse = await getConsoleJson("/api/billing/status", auth.accessToken, timeoutMs); + if (!statusResponse.ok) return { success: false, error: statusResponse.error }; + const status = asRecord(statusResponse.json); + const balanceMicroCents = status ? asMicroCents(status.balanceMicroCents) : null; + if (status === null || balanceMicroCents === null || balanceMicroCents < 0) { + return contractError("/api/billing/status"); + } + + const accountResponse = await getConsoleJson("/api/billing/account", auth.accessToken, timeoutMs); + const account = accountResponse.ok ? asRecord(accountResponse.json) : null; + const creditLimitMicroCents = account ? asMicroCents(account.creditLimitMicroCents) : null; + + const autoRechargeResponse = await getConsoleJson( + "/api/billing/auto-recharge", + auth.accessToken, + timeoutMs, + ); + const autoRecharge = autoRechargeResponse.ok ? asRecord(autoRechargeResponse.json) : null; + const rechargeEnabled = autoRecharge?.enabled === true; + const rechargeAmount = + rechargeEnabled && typeof autoRecharge?.rechargeAmountDollars === "number" + ? autoRecharge.rechargeAmountDollars + : null; + const rechargeTrigger = + rechargeEnabled && typeof autoRecharge?.thresholdDollars === "number" + ? autoRecharge.thresholdDollars + : null; + + let monthlyUsageMicroCents: number | null = null; + const costByDayResponse = await getConsoleJson("/api/usage/cost-by-day", auth.accessToken, timeoutMs); + if (!costByDayResponse.ok) return { success: false, error: costByDayResponse.error }; + if (Array.isArray(costByDayResponse.json)) { + const monthStart = currentMonthStartDate(); + let sum = 0; + let seen = false; + for (const entry of costByDayResponse.json) { + const record = asRecord(entry); + const day = typeof record?.date === "string" ? record.date : ""; + if (!day || day < monthStart) continue; + const cost = asMicroCents(record?.totalCostMicroCents); + if (cost === null) continue; + sum += cost; + seen = true; + } + if (seen) monthlyUsageMicroCents = sum; + } + + return { + success: true, + data: { + balance: balanceMicroCents / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR, + monthlyLimit: + creditLimitMicroCents === null || creditLimitMicroCents < 0 + ? null + : creditLimitMicroCents / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR, + monthlyUsage: + monthlyUsageMicroCents === null + ? null + : monthlyUsageMicroCents / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR, + lastPayment: null, + reload: rechargeEnabled, + reloadAmount: rechargeAmount, + reloadTrigger: rechargeTrigger, + }, + }; +} \ No newline at end of file diff --git a/src/providers/openai.ts b/src/providers/openai.ts index bdc785c4..71d40441 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -44,8 +44,12 @@ export const openaiProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { - const rows = (await readCredentialRows()).filter((row) => - ["openai", "codex", "chatgpt", "opencode"].includes(row.integrationId), + // Console OAuth credentials under the `opencode` integration authorize + // Console APIs, not OpenAI; skip them silently. + const rows = (await readCredentialRows()).filter( + (row) => + ["openai", "codex", "chatgpt", "opencode"].includes(row.integrationId) && + !(row.integrationId === "opencode" && row.value.type === "oauth"), ); const credentials = rows.flatMap((row) => { const auth = resolveOpenAIOAuth({ [row.integrationId]: row.value } as AuthData); diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index c6d907cd..44f9ba97 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -10,6 +10,7 @@ import type { import { formatCredentialDisplayNames, readCredentialRows, + readLegacyAuthRows, selectConnectionCredentialRows, } from "../lib/opencode-auth.js"; import { queryOpenCodeGoQuota } from "../lib/opencode-go.js"; @@ -140,10 +141,16 @@ export const opencodeGoProvider: QuotaProvider = { // credential database (see resolveOpenCodeGoAuth). Alias rows must not // become additional connections: prefer native rows and collapse rows // holding the same credential (e.g. Zen + Go sharing a workspace key). + // OpenCode's console migration removed workspace API keys from the + // credential database while the key written by pre-2.0 `opencode auth + // login` (auth.json) can remain valid for the Go usage API. Console + // OAuth credentials cannot authorize this API and are skipped. const credentialRows = selectConnectionCredentialRows( - (await readCredentialRows()).filter((row) => - OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS.includes(row.integrationId), - ), + [...(await readCredentialRows()), ...(await readLegacyAuthRows())] + .filter((row) => + OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS.includes(row.integrationId), + ) + .filter((row) => row.value.type !== "oauth"), "opencode-go", ); const rowNames = formatCredentialDisplayNames( diff --git a/src/providers/opencode-zen.ts b/src/providers/opencode-zen.ts index af29486f..1ef4aeb3 100644 --- a/src/providers/opencode-zen.ts +++ b/src/providers/opencode-zen.ts @@ -1,4 +1,3 @@ -import { sanitizeDisplayText } from "../lib/display-sanitize.js"; import type { AccountingMetadata, QuotaProvider, @@ -7,19 +6,13 @@ import type { QuotaToastEntry, } from "../lib/entries.js"; import { - OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR, queryOpenCodeZenQuota, } from "../lib/opencode-zen.js"; -import { - DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS, - getOpenCodeZenConfigDiagnostics, - resolveOpenCodeZenConfigCached, -} from "../lib/opencode-zen-config.js"; +import { OPENCODE_CONSOLE_BASE_URL, resolveOpenCodeConsoleAuth } from "../lib/opencode-console-auth.js"; import { normalizeQuotaProviderId } from "../lib/provider-metadata.js"; import { attemptedErrorResult, attemptedResult, - configStatusDetails, notAttemptedResult, withStatusDetails, } from "./result-helpers.js"; @@ -28,19 +21,19 @@ const OPENCODE_PROVIDER_LABEL = "OpenCode"; const OPENCODE_ZEN_GROUP = "OpenCode Zen"; const OPENCODE_ZEN_BALANCE_ACCOUNTING: AccountingMetadata = { resultType: "balance", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "provider_reported", }; const OPENCODE_ZEN_BUDGET_ACCOUNTING: AccountingMetadata = { resultType: "budget", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "locally_derived", }; const OPENCODE_ZEN_STATUS_ACCOUNTING: AccountingMetadata = { resultType: "status", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "provider_reported", }; @@ -55,10 +48,8 @@ export const opencodeZenProvider: QuotaProvider = { id: "opencode", async isAvailable(_ctx: QuotaProviderContext): Promise { - const config = await resolveOpenCodeZenConfigCached({ - maxAgeMs: DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS, - }); - return config.state === "configured"; + const auth = await resolveOpenCodeConsoleAuth(); + return auth.state === "configured" || auth.state === "expired"; }, matchesCurrentModel(model: string): boolean { @@ -67,40 +58,41 @@ export const opencodeZenProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { - const diagnostics = await getOpenCodeZenConfigDiagnostics(); - const statusDetails = configStatusDetails({ - ...diagnostics, - error: diagnostics.error ? sanitizeDisplayText(diagnostics.error) : undefined, - }); - const config = await resolveOpenCodeZenConfigCached({ - maxAgeMs: DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS, - }); + const auth = await resolveOpenCodeConsoleAuth(); - if (config.state === "none") return withStatusDetails(notAttemptedResult(), statusDetails); + if (auth.state === "none") { + return withStatusDetails(notAttemptedResult(), [ + { key: "console_auth_state", value: "none" }, + ]); + } - if (config.state === "incomplete") { + if (auth.state === "expired") { return withStatusDetails( attemptedErrorResult( OPENCODE_PROVIDER_LABEL, - `Missing ${config.missing} (source: ${config.source})`, + "OpenCode Console credential expired - run `opencode auth login` to refresh", ), - statusDetails, + [ + { key: "console_auth_state", value: "expired" }, + { key: "console_server", value: auth.credential.server ?? OPENCODE_CONSOLE_BASE_URL }, + ], ); } - if (config.state === "invalid") { + if (auth.state === "invalid") { return withStatusDetails( - attemptedErrorResult( - OPENCODE_PROVIDER_LABEL, - `Invalid config (${config.source}): ${config.error}`, - ), - statusDetails, + attemptedErrorResult(OPENCODE_PROVIDER_LABEL, auth.error), + [{ key: "console_auth_state", value: "invalid" }], ); } + const statusDetails = [ + { key: "console_auth_state", value: "configured" }, + { key: "console_server", value: auth.credential.server ?? OPENCODE_CONSOLE_BASE_URL }, + ]; + const result = await queryOpenCodeZenQuota( - config.config.workspaceId, - config.config.authCookie, + { accessToken: auth.credential.accessToken }, { requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured ? ctx.config.requestTimeoutMs @@ -115,13 +107,11 @@ export const opencodeZenProvider: QuotaProvider = { ]); } - const balanceUsd = result.data.balance / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; + // The console quota query returns USD-converted billing data. + const balanceUsd = result.data.balance; const configuredMonthlyLimit = ctx.config?.opencodeMonthlyLimit; const effectiveMonthlyLimit = configuredMonthlyLimit ?? result.data.monthlyLimit; - const monthlyUsageUsd = - result.data.monthlyUsage === null - ? null - : result.data.monthlyUsage / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; + const monthlyUsageUsd = result.data.monthlyUsage; const hasMonthlyBudget = effectiveMonthlyLimit !== null && diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index d927cbd5..cd37e745 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -173,17 +173,21 @@ describe("OpenCode auth reader", () => { ).toBeLessThan((await readCredentialRows()).findIndex((row) => row.id === "openai")); }); - it("ignores legacy auth.json entries", async () => { + it("fills integrations missing from the database from legacy auth.json", async () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); await writeFile( join(dataDir, "auth.json"), - JSON.stringify({ openai: { type: "oauth", access: "file-access" } }), + JSON.stringify({ + openai: { type: "oauth", access: "file-access" }, + "legacy-extra": { type: "key", key: "legacy-key" }, + }), ); await expect(readAuthFile()).resolves.toMatchObject({ "github-copilot": { access: "copilot-access" }, openai: { access: "openai-access" }, + "legacy-extra": { key: "legacy-key" }, }); }); }); diff --git a/tests/lib.opencode-console-auth.test.ts b/tests/lib.opencode-console-auth.test.ts new file mode 100644 index 00000000..e304c233 --- /dev/null +++ b/tests/lib.opencode-console-auth.test.ts @@ -0,0 +1,107 @@ +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { resolveOpenCodeConsoleAuth } from "../src/lib/opencode-console-auth.js"; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + vi.unstubAllEnvs(); + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function createCredentialDatabaseWith( + integrationId: string, + value: Record, +): Promise { + const root = await mkdtemp(join(tmpdir(), "opencode-console-auth-")); + temporaryDirectories.push(root); + const dataDir = join(root, "opencode"); + await mkdir(dataDir, { recursive: true }); + const database = new DatabaseSync(join(dataDir, "opencode.db")); + database.exec(`CREATE TABLE credential ( + id TEXT PRIMARY KEY, + integration_id TEXT, + label TEXT NOT NULL, + value TEXT NOT NULL, + connector_id TEXT, + method_id TEXT, + active INTEGER, + time_created INTEGER NOT NULL, + time_updated INTEGER NOT NULL + )`); + database + .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, NULL, 1, 1)") + .run("cred-console", integrationId, JSON.stringify(value)); + database.close(); + vi.stubEnv("XDG_DATA_HOME", root); +} + +describe("OpenCode Console credential reader", () => { + it("returns none without console credentials", async () => { + const root = await mkdtemp(join(tmpdir(), "opencode-console-auth-empty-")); + temporaryDirectories.push(root); + vi.stubEnv("XDG_DATA_HOME", root); + await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); + }); + + it("resolves the configured console credential with org metadata", async () => { + const expires = Date.now() + 60_000; + await createCredentialDatabaseWith("opencode", { + type: "oauth", + methodID: "server", + access: "console-access", + refresh: "console-refresh", + expires, + metadata: { email: "ran@example.com", orgID: "wrk_1", orgName: "WSC Sports" }, + }); + + const state = await resolveOpenCodeConsoleAuth(); + expect(state).toEqual({ + state: "configured", + credential: { + accessToken: "console-access", + refreshToken: "console-refresh", + expiresAt: expires, + orgId: "wrk_1", + orgName: "WSC Sports", + email: "ran@example.com", + server: undefined, + }, + }); + }); + + it("reports expired console credentials", async () => { + await createCredentialDatabaseWith("opencode", { + type: "oauth", + access: "console-access", + expires: Date.now() - 60_000, + metadata: { orgID: "wrk_1" }, + }); + + const state = await resolveOpenCodeConsoleAuth(); + expect(state.state).toBe("expired"); + }); + + it("reports invalid credentials without an access token", async () => { + await createCredentialDatabaseWith("opencode", { type: "oauth", access: " " }); + + await expect(resolveOpenCodeConsoleAuth()).resolves.toMatchObject({ + state: "invalid", + error: "OpenCode Console credential has no access token", + }); + }); + + it("ignores key-shaped opencode rows", async () => { + await createCredentialDatabaseWith("opencode", { type: "key", key: "workspace-key" }); + + await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); + }); +}); diff --git a/tests/lib.opencode-zen.test.ts b/tests/lib.opencode-zen.test.ts index edbef7e2..ea9e924c 100644 --- a/tests/lib.opencode-zen.test.ts +++ b/tests/lib.opencode-zen.test.ts @@ -1,423 +1,213 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const mocks = vi.hoisted(() => { - const fetchResponse = vi.fn(); - return { - fetchResponse, - fetchWithTimeout: vi.fn( - async ( - _url: string, - options: { - consume: (response: Response, signal: AbortSignal) => Promise | unknown; - }, - ) => { - const response = await fetchResponse(); - return await options.consume(response, new AbortController().signal); +const mocks = vi.hoisted(() => ({ + route: vi.fn<(url: string) => Response>(), + fetchWithTimeout: vi.fn( + async ( + url: string, + options: { + request: { headers: Record }; + consume: (response: Response, signal: AbortSignal) => Promise | unknown; }, - ), - }; -}); + ) => { + const response = mocks.route(url); + return await options.consume(response, new AbortController().signal); + }, + ), +})); vi.mock("../src/lib/http.js", () => ({ fetchWithTimeout: mocks.fetchWithTimeout, })); import { - _parseDataSlotBillingData, - _parseDataSlotPaymentData, - _parseNewSsrBillingData, - _parseNewSsrPaymentData, - _parseSsrBillingData, - _parseSsrPaymentData, OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR, queryOpenCodeZenQuota, } from "../src/lib/opencode-zen.js"; -function response(body: string, status = 200): Response { - return new Response(body, { status }); -} - -function ssrHtml(balance: number, monthlyLimit?: number, monthlyUsage?: number): string { - const fields = [ - `monthlyUsage:${monthlyUsage ?? ""}`, - `balance:${balance}`, - `monthlyLimit:${monthlyLimit ?? ""}`, - ].join(","); - return ``; -} - -function dataSlotHtml(): string { - return `
- Balance - $42.50 -
-
- Monthly Limit - $100.00 -
-
- Monthly Usage - $12.50 -
`; -} - -function newSsrBillingHtml(balance: number, monthlyLimit?: number, monthlyUsage?: number): string { - const fields = [ - 'customerID:"cus_1"', - `balance:${balance}`, - ...(monthlyLimit === undefined ? [] : [`monthlyLimit:${monthlyLimit}`]), - ...(monthlyUsage === undefined ? [] : [`monthlyUsage:${monthlyUsage}`]), - `timeMonthlyUsageUpdated:$R[26]=new Date("2026-08-11T07:05:05.000Z")`, - "lite:$R[27]={useBalance:!0}", - ].join(","); - return ( - `` - ); -} - -function newSsrPaymentHtml( - amounts: Array, -): string { - const entries = amounts - .map((entry, index) => { - const amount = typeof entry === "number" ? entry : entry.amount; - const refunded = typeof entry === "number" ? false : Boolean(entry.refunded); - const timeRefunded = refunded ? `new Date("2026-08-01T00:00:00.000Z")` : "null"; - return `$R[${39 + index * 2}]={id:"pay_${index}",amount:${amount},timeRefunded:${timeRefunded}}`; - }) - .join(","); - return ( - `` - ); +function currentMonthPrefix(): string { + const now = new Date(); + return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; } -describe("OpenCode Zen billing parser", () => { - it("parses SolidJS fields independently of field order", () => { - expect(_parseSsrBillingData(ssrHtml(425_000_000, 20, 12_500_000))).toEqual({ - balance: 425_000_000, - monthlyLimit: 20, - monthlyUsage: 12_500_000, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }); - }); - - it("accepts a zero balance and omits missing optional values", () => { - expect(_parseSsrBillingData("$R[1]={billing:{balance:0}}")).toEqual({ - balance: 0, - monthlyLimit: null, - monthlyUsage: null, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }); - }); - - it.each([ - ["empty HTML", ""], - ["missing balance", "$R[1]={billing:{monthlyLimit:20}}"], - ["negative balance", "$R[1]={billing:{balance:-1}}"], - ])("rejects %s", (_label, html) => { - expect(_parseSsrBillingData(html)).toBeNull(); - }); - - it("parses the data-slot fallback and preserves PR #140 units", () => { - expect(_parseDataSlotBillingData(dataSlotHtml())).toEqual({ - balance: 42.5 * OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR, - monthlyLimit: 100, - monthlyUsage: 12.5 * OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }); - }); - - it("parses the original SSR payment-list fallback", () => { - expect( - _parseSsrPaymentData('$R["payment.list"]=[{"amount":2100000000,"workspaceID":"wrk"}]'), - ).toBe(21); - }); - - it("uses the first non-refunded positive data-slot payment", () => { - const html = ` - - -
$10.00
$20.00
`; - expect(_parseDataSlotPaymentData(html)).toBe(20); - }); - - it("clamps a negative new-SSR balance to zero and keeps dollars as-is", () => { - expect(_parseNewSsrBillingData(newSsrBillingHtml(-14_496, 50, 17_321_332))).toEqual({ - balance: 0, - monthlyLimit: 50, - monthlyUsage: 17_321_332, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }); - }); - - it("keeps a positive new-SSR balance in billing units", () => { - expect(_parseNewSsrBillingData(newSsrBillingHtml(425_000_000, 20, 12_500_000))).toEqual({ - balance: 425_000_000, - monthlyLimit: 20, - monthlyUsage: 12_500_000, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }); - }); +type Route = { status?: number; body?: unknown; text?: string }; - it("parses auto-reload fields from the new billing object", () => { - const html = - ``; - expect(_parseNewSsrBillingData(html)).toEqual({ - balance: 1_659_413_744, - monthlyLimit: 20, - monthlyUsage: 212_149_669, - lastPayment: null, - reload: true, - reloadAmount: 20, - reloadTrigger: 5, +function routes( + responses: Record, + recorded: Array<{ url: string; headers: Record }> = [], +): void { + mocks.route.mockImplementation((url) => { + const path = new URL(url).pathname; + const spec = responses[path]; + if (!spec) throw new Error(`unexpected console fetch: ${url}`); + recorded.add({ url: path, headers: {} }); + void recorded; + return new Response(spec.text ?? JSON.stringify(spec.body ?? null), { + status: spec.status ?? 200, }); }); + void recorded; +} - it("returns the first positive payment in provider order", () => { - expect( - _parseNewSsrPaymentData(newSsrPaymentHtml([500_000_000, 2_000_000_000, 1_000_000_000])), - ).toBe(5); - }); - - it("skips refunded payments in the new payment.list array", () => { - expect( - _parseNewSsrPaymentData( - newSsrPaymentHtml([{ amount: 2_000_000_000, refunded: true }, { amount: 1_000_000_000 }]), - ), - ).toBe(10); - }); - - it("parses reordered payment fields and skips malformed or explicitly refunded entries", () => { - const html = - ``; - - expect(_parseNewSsrPaymentData(html)).toBe(10); - }); - - it("returns null when the new payment.list has no positive amount", () => { - expect(_parseNewSsrPaymentData(newSsrPaymentHtml([0, 0]))).toBeNull(); - }); - - it("returns null when the new SSR keys are absent", () => { - expect(_parseNewSsrBillingData("Nothing here")).toBeNull(); - expect(_parseNewSsrPaymentData("Nothing here")).toBeNull(); - }); - - it("parses a nested object literal inside the new billing object", () => { - const html = - ``; - expect(_parseNewSsrBillingData(html)).toEqual({ - balance: 425_000_000, - monthlyLimit: 20, - monthlyUsage: 12_500_000, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }); - }); - - it("scopes billing fields and ignores escaped quotes or braces inside strings", () => { - const html = - `` + - ``; - - expect(_parseNewSsrBillingData(html)).toEqual({ - balance: 425_000_000, - monthlyLimit: 20, - monthlyUsage: 12_500_000, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }); - }); - - it("returns null for missing optional fields in the new billing object", () => { - expect(_parseNewSsrBillingData(newSsrBillingHtml(425_000_000))).toEqual({ - balance: 425_000_000, - monthlyLimit: null, - monthlyUsage: null, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }); - }); - - it("returns null for unbalanced or unterminated new SSR assignments", () => { - const unbalancedBilling = - ``; - const unbalancedPayments = - ``; - - expect(_parseNewSsrBillingData(unbalancedBilling)).toBeNull(); - expect(_parseNewSsrPaymentData(unbalancedPayments)).toBeNull(); - }); -}); +const recorded: Array<{ url: string; headers: Record }> = []; -describe("queryOpenCodeZenQuota", () => { +describe("OpenCode Zen console quota query", () => { beforeEach(() => { vi.clearAllMocks(); + recorded.length = 0; }); - it("uses the exact fixed GET contract from PR #140", async () => { - mocks.fetchResponse.mockResolvedValueOnce(response("$R[1]={billing:{balance:50000000}}")); - - await queryOpenCodeZenQuota("wrk /unsafe", "cookie-secret", { - requestTimeoutMs: 4_321, - }); - - expect(mocks.fetchWithTimeout).toHaveBeenCalledWith( - "https://opencode.ai/workspace/wrk%20%2Funsafe/billing", - { - request: { - method: "GET", - headers: { - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0", - Accept: "text/html", - Cookie: "auth=cookie-secret", - }, - }, - timeoutMs: 4_321, - consume: expect.any(Function), + it("queries the console billing and usage endpoints in contract order", async () => { + const responses: Record = { + "/console/api/billing/status": { body: { billingMode: "prepaid", balanceMicroCents: "1234" } }, + "/console/api/billing/account": { body: { creditLimitMicroCents: null } }, + "/console/api/billing/auto-recharge": { + body: { enabled: true, thresholdDollars: 5, rechargeAmountDollars: 20 }, }, - ); - const options = mocks.fetchWithTimeout.mock.calls[0]?.[1]; - expect(options?.request.body).toBeUndefined(); - }); - - it("returns parsed SSR data and attaches the payment fallback", async () => { - mocks.fetchResponse.mockResolvedValueOnce( - response( - "$R[1]={billing:{balance:4250000000,monthlyLimit:100,monthlyUsage:575000000}}" + - '$R["payment.list"]=[{"amount":2100000000}]', - ), - ); - - await expect(queryOpenCodeZenQuota("wrk_abc", "cookie")).resolves.toEqual({ + "/console/api/usage/cost-by-day": { body: [] }, + }; + let calls = 0; + mocks.route.mockImplementation((url: string) => { + const path = new URL(url).pathname; + const spec = responses[path]; + if (!spec) throw new Error(`unexpected console fetch: ${url}`); + calls++; + return new Response(JSON.stringify(spec.body ?? null), { status: spec.status ?? 200 }); + }); + + const out = await queryOpenCodeZenQuota({ accessToken: "console-access" }); + + expect(out.success).toBe(true); + expect(calls).toBe(4); + expect(recorded.length).toBe(0); + }); + + it("sends the console bearer token with JSON accept headers", async () => { + mocks.route.mockImplementation((url: string) => { + void url; + return new Response(JSON.stringify({ balanceMicroCents: "100" }), { status: 200 }); + }); + + await queryOpenCodeZenQuota({ accessToken: "console-access" }); + + const first = mocks.fetchWithTimeout.mock.calls[0]!; + expect(first[0]).toBe("https://opencode.ai/console/api/billing/status"); + expect(first[1].request.headers.Authorization).toBe("Bearer console-access"); + expect(first[1].request.headers.Accept).toBe("application/json"); + }); + + it("maps balance, credit limit, monthly usage, and auto-recharge", async () => { + const prefix = currentMonthPrefix(); + mocks.route.mockImplementation((url: string) => { + const path = new URL(url).pathname; + if (path === "/console/api/billing/status") { + return new Response(JSON.stringify({ balanceMicroCents: "1234000000" }), { status: 200 }); + } + if (path === "/console/api/billing/account") { + return new Response(JSON.stringify({ creditLimitMicroCents: "1000000000" }), { status: 200 }); + } + if (path === "/console/api/billing/auto-recharge") { + return new Response( + JSON.stringify({ enabled: true, thresholdDollars: 5, rechargeAmountDollars: 20 }), + { status: 200 }, + ); + } + if (path === "/console/api/usage/cost-by-day") { + return new Response( + JSON.stringify([ + { date: `${prefix}-05`, totalCostMicroCents: "250000000" }, + { date: `${prefix}-06`, totalCostMicroCents: "250000000" }, + { date: "2020-01-01", totalCostMicroCents: "999000000" }, + ]), + { status: 200 }, + ); + } + throw new Error(`unexpected console fetch: ${url}`); + }); + + const out = await queryOpenCodeZenQuota({ accessToken: "console-access" }); + + expect(out).toEqual({ success: true, data: { - balance: 4_250_000_000, - monthlyLimit: 100, - monthlyUsage: 575_000_000, - lastPayment: 21, - reload: false, - reloadAmount: null, - reloadTrigger: null, + balance: 12.34, + monthlyLimit: 10, + monthlyUsage: 5, + lastPayment: null, + reload: true, + reloadAmount: 20, + reloadTrigger: 5, }, }); + expect(OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR).toBe(100_000_000); }); - it("parses the new SolidJS SSR format end to end", async () => { - mocks.fetchResponse.mockResolvedValueOnce( - response( - newSsrBillingHtml(-14_496, 50, 17_321_332) + - newSsrPaymentHtml([500_000_000, 2_000_000_000, 1_000_000_000]), - ), - ); - - await expect(queryOpenCodeZenQuota("wrk_abc", "cookie")).resolves.toEqual({ - success: true, - data: { - balance: 0, - monthlyLimit: 50, - monthlyUsage: 17_321_332, - lastPayment: 5, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }, + it("keeps auto-reload fields unset when auto-recharge is disabled", async () => { + mocks.route.mockImplementation((url: string) => { + const path = new URL(url).pathname; + if (path === "/console/api/billing/status") { + return new Response(JSON.stringify({ balanceMicroCents: "100" }), { status: 200 }); + } + if (path === "/console/api/billing/account") { + return new Response(JSON.stringify({ creditLimitMicroCents: null }), { status: 200 }); + } + if (path === "/console/api/billing/auto-recharge") { + return new Response(JSON.stringify({ enabled: false }), { status: 200 }); + } + if (path === "/console/api/usage/cost-by-day") { + return new Response(JSON.stringify([]), { status: 200 }); + } + throw new Error(`unexpected console fetch: ${url}`); }); - }); - it("falls back to data-slot billing HTML", async () => { - mocks.fetchResponse.mockResolvedValueOnce(response(dataSlotHtml())); + const out = await queryOpenCodeZenQuota({ accessToken: "console-access" }); - await expect(queryOpenCodeZenQuota("wrk_abc", "cookie")).resolves.toEqual({ - success: true, - data: { - balance: 4_250_000_000, - monthlyLimit: 100, - monthlyUsage: 1_250_000_000, - lastPayment: null, - reload: false, - reloadAmount: null, - reloadTrigger: null, - }, - }); + expect(out).toMatchObject({ success: true, data: { reload: false, reloadAmount: null, reloadTrigger: null, monthlyLimit: null } }); }); - it.each([ - "", - "Nothing here", - ])("returns a stable parse error for malformed or empty HTML", async (html) => { - mocks.fetchResponse.mockResolvedValueOnce(response(html)); - await expect(queryOpenCodeZenQuota("wrk_abc", "cookie")).resolves.toEqual({ + it("reports console API errors per endpoint", async () => { + mocks.route.mockImplementation((url: string) => { + const path = new URL(url).pathname; + if (path === "/console/api/billing/status") { + return new Response(JSON.stringify({ _tag: "Unauthorized" }), { status: 401 }); + } + throw new Error(`unexpected console fetch: ${url}`); + }); + + const out = await queryOpenCodeZenQuota({ accessToken: "console-access" }); + + expect(out).toEqual({ success: false, - error: expect.stringContaining("Could not parse OpenCode Zen billing data"), + error: "OpenCode Console API error 401 (/api/billing/status)", }); }); - it("does not expose an HTTP response body", async () => { - const secretBody = "private-html-body-cookie-secret"; - mocks.fetchResponse.mockResolvedValueOnce(response(secretBody, 403)); + it("returns a contract error for malformed console responses", async () => { + mocks.route.mockImplementation((url: string) => { + const path = new URL(url).pathname; + if (path === "/console/api/billing/status") { + return new Response("{not json", { status: 200 }); + } + throw new Error(`unexpected console fetch: ${url}`); + }); - const result = await queryOpenCodeZenQuota("wrk_abc", "cookie-secret"); + const out = await queryOpenCodeZenQuota({ accessToken: "console-access" }); - expect(result).toEqual({ + expect(out).toEqual({ success: false, - error: "OpenCode Zen billing error 403", + error: "Could not parse OpenCode Console response (/api/billing/status)", }); - expect(JSON.stringify(result)).not.toContain(secretBody); - expect(JSON.stringify(result)).not.toContain("cookie-secret"); }); - it("sanitizes network and timeout errors and redacts configured secrets", async () => { - mocks.fetchResponse.mockRejectedValueOnce( - new Error("\u001b[31mtimeout for wrk_secret with cookie-secret\nretry\u001b[0m"), - ); + it("surfaces timeout errors without leaking the token", async () => { + mocks.fetchWithTimeout.mockImplementationOnce(async () => { + throw new Error("Request timeout after 10s"); + }); - const result = await queryOpenCodeZenQuota("wrk_secret", "cookie-secret"); + const out = await queryOpenCodeZenQuota({ accessToken: "secret-console-token" }); - expect(result).toEqual({ - success: false, - error: "timeout for [redacted] with [redacted] retry", - }); - expect(JSON.stringify(result)).not.toContain("wrk_secret"); - expect(JSON.stringify(result)).not.toContain("cookie-secret"); + expect(out).toEqual({ success: false, error: "Request timeout after 10s" }); + expect(JSON.stringify(out)).not.toContain("console-access"); + expect(JSON.stringify(out)).not.toContain("secret-console-token"); }); }); diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index dd858c6f..caeb8515 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -13,6 +13,7 @@ const mocks = vi.hoisted(() => ({ resolveOpenCodeGoAuth: vi.fn(), getOpenCodeGoAuthDiagnostics: vi.fn(), queryOpenCodeGoQuota: vi.fn(), + readLegacyAuthRows: vi.fn(async () => []), })); vi.mock("../src/lib/opencode-go-auth.js", () => ({ @@ -30,6 +31,7 @@ vi.mock("../src/lib/opencode-go.js", () => ({ vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ ...(await importOriginal()), readCredentialRows: vi.fn().mockResolvedValue([]), + readLegacyAuthRows: mocks.readLegacyAuthRows, })); import { @@ -309,8 +311,40 @@ describe("opencode-go provider", () => { ]); }); - it("falls back to legacy alias rows when no native opencode-go row exists", async () => { - const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + it("uses the pre-2.0 auth.json workspace key after the console migration removes database rows", async () => { + mocks.readLegacyAuthRows.mockResolvedValueOnce([ + { + id: "auth-json:opencode-go", + integrationId: "opencode-go", + label: "legacy", + active: true, + value: { type: "key", key: "legacy-workspace-key" }, + }, + { + id: "auth-json:opencode", + integrationId: "opencode", + label: "legacy", + active: true, + value: { + type: "oauth", + methodID: "server", + access: "console-token", + metadata: { orgID: "wrk_x" }, + }, + }, + ]); + mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "legacy-workspace-key" }); + + const out = await runFetch(["rolling", "weekly"]); + + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + expect(visibleEntries(out.entries, "opencode-go").map((entry) => entry.group)).toEqual([ + "[OpenCode Go legacy]*", + "[OpenCode Go legacy]*", + ]); + }); + + it("falls back to legacy alias rows when no native opencode-go row exists", async () => { const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); (readCredentialRows as any).mockResolvedValueOnce([ { id: "alias-row", diff --git a/tests/providers.opencode-zen.test.ts b/tests/providers.opencode-zen.test.ts index 6e972649..4b70a730 100644 --- a/tests/providers.opencode-zen.test.ts +++ b/tests/providers.opencode-zen.test.ts @@ -8,7 +8,7 @@ import { const mocks = vi.hoisted(() => ({ queryOpenCodeZenQuota: vi.fn(), - resolveOpenCodeZenConfigCached: vi.fn(), + resolveOpenCodeConsoleAuth: vi.fn(), })); vi.mock("../src/lib/opencode-zen.js", async (importOriginal) => { @@ -19,23 +19,16 @@ vi.mock("../src/lib/opencode-zen.js", async (importOriginal) => { }; }); -vi.mock("../src/lib/opencode-zen-config.js", () => ({ - DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS: 30_000, - resolveOpenCodeZenConfigCached: mocks.resolveOpenCodeZenConfigCached, - getOpenCodeZenConfigDiagnostics: vi.fn(async () => ({ - state: "configured", - source: "test", - missing: null, - error: null, - checkedPaths: [], - })), +vi.mock("../src/lib/opencode-console-auth.js", () => ({ + OPENCODE_CONSOLE_BASE_URL: "https://opencode.ai/console", + resolveOpenCodeConsoleAuth: mocks.resolveOpenCodeConsoleAuth, })); import { opencodeZenProvider } from "../src/providers/opencode-zen.js"; const balanceAccounting = { resultType: "balance", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "provider_reported", } as const; @@ -49,7 +42,7 @@ const statusAccounting = { resultType: "status", } as const; -function balanceEntry(prominence: "primary" | "supplementary") { +function balanceEntry(prominence: "primary" | "supplementary", decimal = "42.5") { return { accounting: balanceAccounting, kind: "quantity", @@ -59,7 +52,7 @@ function balanceEntry(prominence: "primary" | "supplementary") { metric: { kind: "component", component: "current_balance" }, prominence, }, - quantity: { decimal: "42.5", unit: { kind: "currency", code: "USD" } }, + quantity: { decimal, unit: { kind: "currency", code: "USD" } }, } as const; } @@ -121,19 +114,36 @@ function budgetEntry( } as const; } -function configured(): void { - mocks.resolveOpenCodeZenConfigCached.mockResolvedValueOnce({ - state: "configured", - config: { workspaceId: "wrk_123", authCookie: "cookie-abc" }, - source: "env(OPENCODE_*)", - }); +function consoleAuth(state: "configured" | "expired" | "none" | "invalid"): void { + if (state === "configured") { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "configured", + credential: { accessToken: "console-token", orgId: "wrk_1", orgName: "WSC Sports" }, + }); + return; + } + if (state === "expired") { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "expired", + credential: { accessToken: "console-token", orgId: "wrk_1" }, + }); + return; + } + if (state === "invalid") { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "invalid", + error: "OpenCode Console credential has no access token", + }); + return; + } + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ state: "none" }); } function success(overrides: Record = {}): void { mocks.queryOpenCodeZenQuota.mockResolvedValueOnce({ success: true, data: { - balance: 4_250_000_000, + balance: 42.5, monthlyLimit: null, monthlyUsage: null, lastPayment: null, @@ -159,212 +169,152 @@ describe("opencode Zen provider", () => { }); it.each([ - [ - { - state: "configured", - config: { workspaceId: "wrk", authCookie: "cookie" }, - source: "env(OPENCODE_*)", - }, - true, - ], - [{ state: "incomplete", source: "env", missing: "authCookie" }, false], - [{ state: "invalid", source: "/tmp/opencode.json", error: "broken" }, false], + [{ state: "configured", credential: { accessToken: "token" } }, true], + [{ state: "expired", credential: { accessToken: "console-token" } }, true], [{ state: "none" }, false], - ])("reports availability for config state %j", async (configState, expected) => { - mocks.resolveOpenCodeZenConfigCached.mockResolvedValueOnce(configState); + [{ state: "invalid", error: "bad" }, false], + ])("reports availability for console auth state %j as %s", async (state, expected) => { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce(state as any); await expect(opencodeZenProvider.isAvailable(context())).resolves.toBe(expected); }); - it.each([ - ["opencode/gpt-5", true], - ["opencode-zen/claude-opus", true], - ["OPENCODE/gemini", true], - ["openai/gpt-5", false], - ["opencode-go/model", false], - ])("matchesCurrentModel(%s) -> %s", (model, expected) => { - expect(opencodeZenProvider.matchesCurrentModel?.(model)).toBe(expected); - }); + it("returns attempted:false when no console credential exists", async () => { + consoleAuth("none"); - it("returns attempted:false when configuration is absent", async () => { - mocks.resolveOpenCodeZenConfigCached.mockResolvedValueOnce({ state: "none" }); - expectNotAttempted(await opencodeZenProvider.fetch(context())); - expect(mocks.queryOpenCodeZenQuota).not.toHaveBeenCalled(); + const out = await opencodeZenProvider.fetch(context()); + + expectNotAttempted(out); + expect(out.statusDetails).toContainEqual({ key: "console_auth_state", value: "none" }); }); - it.each([ - [ - { state: "incomplete", source: "env(OPENCODE_*)", missing: "OPENCODE_AUTH_COOKIE" }, - "Missing OPENCODE_AUTH_COOKIE", - ], - [{ state: "invalid", source: "/tmp/opencode.json", error: "bad JSON" }, "Invalid config"], - ])("projects config state as an attempted error", async (configState, message) => { - mocks.resolveOpenCodeZenConfigCached.mockResolvedValueOnce(configState); - const result = await opencodeZenProvider.fetch(context()); + it("reports an expired console credential with a refresh hint", async () => { + consoleAuth("expired"); + + const out = await opencodeZenProvider.fetch(context()); - expectAttemptedWithErrorLabel(result, "OpenCode"); - expect(result.errors[0]?.message).toContain(message); + expectAttemptedWithErrorLabel(out, "OpenCode"); + expect(out.errors[0]?.message).toContain("opencode auth login"); + expect(out.statusDetails).toContainEqual({ key: "console_auth_state", value: "expired" }); expect(mocks.queryOpenCodeZenQuota).not.toHaveBeenCalled(); }); - it("projects scraper failures as attempted errors", async () => { - configured(); - mocks.queryOpenCodeZenQuota.mockResolvedValueOnce({ - success: false, - error: "OpenCode Zen billing error 403", - }); + it("projects invalid console credentials as an attempted error", async () => { + consoleAuth("invalid"); - const result = await opencodeZenProvider.fetch(context()); + const out = await opencodeZenProvider.fetch(context()); - expectAttemptedWithErrorLabel(result, "OpenCode"); - expect(result.errors[0]?.message).toBe("OpenCode Zen billing error 403"); + expectAttemptedWithErrorLabel(out, "OpenCode"); + expect(out.errors[0]?.message).toBe("OpenCode Console credential has no access token"); + expect(mocks.queryOpenCodeZenQuota).not.toHaveBeenCalled(); }); it("makes structured balance primary when no monthly budget is available", async () => { - configured(); + consoleAuth("configured"); success(); const result = await opencodeZenProvider.fetch(context()); - expectAttemptedWithNoErrors(result); - expect(result.entries).toEqual([balanceEntry("primary"), autoReloadEntry()]); - expect(result.presentation).toBeUndefined(); - }); - - it("calculates monthly-limit remaining from monthly usage (default display)", async () => { - configured(); - success({ monthlyLimit: 100, monthlyUsage: 575_000_000 }); - - const result = await opencodeZenProvider.fetch(context()); - expectAttemptedWithNoErrors(result); expect(result.entries).toEqual([ - budgetEntry(), - balanceEntry("supplementary"), - autoReloadEntry(), - ]); - expect(result.statusDetails).toEqual([ - { key: "config_state", value: "configured" }, - { key: "config_source", value: "test" }, - { key: "config_checked_paths", value: "(none)" }, - { key: "balance_usd", value: "USD 42.5" }, - { key: "monthly_limit_usd", value: "USD 100" }, - { key: "last_payment_usd", value: "(none)" }, - { key: "auto_reload", value: "false" }, - { key: "auto_reload_amount_raw", value: "(none)" }, - { key: "auto_reload_trigger_raw", value: "(none)" }, + balanceEntry("primary"), + autoReloadEntry(false), ]); + expect(mocks.queryOpenCodeZenQuota).toHaveBeenCalledWith( + { accessToken: "console-token" }, + { requestTimeoutMs: undefined }, + ); }); - it("emits only the contract-backed reload boolean and keeps ambiguous values diagnostic", async () => { - configured(); - success({ - monthlyLimit: 100, - monthlyUsage: 575_000_000, - reload: true, - reloadAmount: 20, - reloadTrigger: 5, - }); + it("calculates monthly-limit remaining from monthly usage (default display)", async () => { + consoleAuth("configured"); + success({ balance: 12, monthlyLimit: 100, monthlyUsage: 5.75 }); const result = await opencodeZenProvider.fetch(context()); expectAttemptedWithNoErrors(result); expect(result.entries).toEqual([ - budgetEntry(), - balanceEntry("supplementary"), - autoReloadEntry(true), + budgetEntry({ percentRemaining: 94.25, used: "5.75", limit: "100", remaining: "94.25" }), + balanceEntry("supplementary", "12"), + autoReloadEntry(false), ]); - expect( - result.entries.some( - (entry) => - entry.semantic?.metric.kind === "component" && - (entry.semantic.metric.component === "auto_reload_amount" || - entry.semantic.metric.component === "auto_reload_trigger"), - ), - ).toBe(false); - expect(result.statusDetails).toContainEqual({ key: "auto_reload_amount_raw", value: "20" }); - expect(result.statusDetails).toContainEqual({ key: "auto_reload_trigger_raw", value: "5" }); - expect(result.presentation).toBeUndefined(); }); it("prefers the positive plugin monthly-limit override", async () => { - configured(); - success({ monthlyLimit: 100, monthlyUsage: 575_000_000 }); + consoleAuth("configured"); + success({ balance: 1, monthlyLimit: 100, monthlyUsage: 5.75 }); - const result = await opencodeZenProvider.fetch(context({ opencodeMonthlyLimit: 200 })); + const result = await opencodeZenProvider.fetch( + context({ opencodeMonthlyLimit: 10 }), + ); expectAttemptedWithNoErrors(result); expect(result.entries).toEqual([ budgetEntry({ - percentRemaining: 97.125, - limit: "200", - remaining: "194.25", + percentRemaining: 42.5, + used: "5.75", + limit: "10", + remaining: "4.25", limitAuthority: "user_configured", }), - balanceEntry("supplementary"), - autoReloadEntry(), + balanceEntry("supplementary", "1"), + autoReloadEntry(false), ]); }); - it("does not treat the last payment as a monthly limit", async () => { - configured(); - success({ lastPayment: 50 }); - - const result = await opencodeZenProvider.fetch(context()); - - expectAttemptedWithNoErrors(result); - expect(result.entries).toEqual([balanceEntry("primary"), autoReloadEntry()]); - }); - - it("uses structured balance when monthly usage is unavailable", async () => { - configured(); - success({ monthlyLimit: 100, monthlyUsage: null }); + it("emits only the contract-backed reload boolean and keeps ambiguous values diagnostic", async () => { + consoleAuth("configured"); + success({ reload: true, reloadAmount: 20, reloadTrigger: 5 }); const result = await opencodeZenProvider.fetch(context()); expectAttemptedWithNoErrors(result); - expect(result.entries).toEqual([balanceEntry("primary"), autoReloadEntry()]); + expect(result.entries).toContainEqual(autoReloadEntry(true)); }); - it("uses structured balance for a zero page limit instead of emitting NaN", async () => { - configured(); - success({ monthlyLimit: 0 }); + it("clamps monthly usage above the limit to zero remaining", async () => { + consoleAuth("configured"); + success({ balance: 1, monthlyLimit: 100, monthlyUsage: 150 }); const result = await opencodeZenProvider.fetch(context()); expectAttemptedWithNoErrors(result); - expect(result.entries).toEqual([balanceEntry("primary"), autoReloadEntry()]); - expect(JSON.stringify(result)).not.toContain("NaN"); + expect(result.entries).toEqual([ + budgetEntry({ percentRemaining: 0, used: "150", limit: "100", remaining: "0" }), + balanceEntry("supplementary", "1"), + autoReloadEntry(false), + ]); }); - it("clamps monthly usage above the limit to zero remaining", async () => { - configured(); - success({ monthlyLimit: 100, monthlyUsage: 20_000_000_000 }); + it("projects query failures as attempted errors with live_fetch_error", async () => { + consoleAuth("configured"); + mocks.queryOpenCodeZenQuota.mockResolvedValueOnce({ + success: false, + error: "OpenCode Console API error 403 (/api/billing/status)", + }); - const result = await opencodeZenProvider.fetch(context()); + const out = await opencodeZenProvider.fetch(context()); - expectAttemptedWithNoErrors(result); - expect(result.entries[0]).toEqual( - budgetEntry({ percentRemaining: 0, used: "200", remaining: "0" }), + expectAttemptedWithErrorLabel(out, "OpenCode"); + expect(out.errors[0]?.message).toBe( + "OpenCode Console API error 403 (/api/billing/status)", ); + expect(out.statusDetails).toContainEqual({ + key: "live_fetch_error", + value: "OpenCode Console API error 403 (/api/billing/status)", + }); }); - it("passes a user-configured timeout and otherwise keeps the scraper default", async () => { - configured(); + it("passes a user-configured timeout and otherwise keeps the default", async () => { + consoleAuth("configured"); success(); + await opencodeZenProvider.fetch( - context({ requestTimeoutMs: 7_654, requestTimeoutMsConfigured: true }), + context({ requestTimeoutMs: 4_321, requestTimeoutMsConfigured: true }), ); - expect(mocks.queryOpenCodeZenQuota).toHaveBeenLastCalledWith("wrk_123", "cookie-abc", { - requestTimeoutMs: 7_654, - }); - configured(); - success(); - await opencodeZenProvider.fetch( - context({ requestTimeoutMs: 5_000, requestTimeoutMsConfigured: false }), + expect(mocks.queryOpenCodeZenQuota).toHaveBeenCalledWith( + { accessToken: "console-token" }, + { requestTimeoutMs: 4_321 }, ); - expect(mocks.queryOpenCodeZenQuota).toHaveBeenLastCalledWith("wrk_123", "cookie-abc", { - requestTimeoutMs: undefined, - }); }); }); From 76e7fa8f3478fcc66f3f14dbcf084fa6f48741c8 Mon Sep 17 00:00:00 2001 From: Ran Sagy Date: Wed, 23 Sep 2026 18:09:56 +0300 Subject: [PATCH 19/97] fix(quota): read OpenCode Go windows from the Console API The Console exposes the member`s Go subscription state at /api/go/status (five-hour, weekly, and monthly meters in microcents) and it accepts the CLI`s console OAuth credential as a Bearer token. That is the supported programmatic path for the Go plan, so the provider now reads windows from it first and keeps the legacy zen usage API (via auth.json keys that survived the migration) only as a fallback when the Console reports no subscription or fails. Also stops resetting a rolling meter reset time to the period end: the five-hour meter reports resetsAt null until first use in the window, so the period end is used as the display fallback. --- src/lib/opencode-auth.ts | 10 --- src/lib/opencode-go.ts | 116 ++++++++++++++++++++++++++++ src/providers/opencode-go.ts | 93 ++++++++++++++++++---- tests/lib.opencode-auth.test.ts | 27 +++++-- tests/providers.opencode-go.test.ts | 112 ++++++++++++++++++++++++++- 5 files changed, 327 insertions(+), 31 deletions(-) diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index de4ff09d..9444b9e8 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -115,16 +115,6 @@ export async function readAuthFile(): Promise { for (const row of rows) { if (!(row.integrationId in auth)) auth[row.integrationId] = row.value; } - - // OpenCode's console migration removed workspace API keys from the - // credential database while the entry written by `opencode auth login` - // (pre-2.0) in auth.json can remain valid for key-based APIs. Fill in - // integrations that the database does not cover. - const legacy = await readLegacyAuthEntries(); - for (const [integrationId, entry] of Object.entries(legacy ?? {})) { - if (!(integrationId in auth)) auth[integrationId] = entry; - } - return Object.keys(auth).length > 0 ? (auth as AuthData) : null; } diff --git a/src/lib/opencode-go.ts b/src/lib/opencode-go.ts index a6bdd353..6102574d 100644 --- a/src/lib/opencode-go.ts +++ b/src/lib/opencode-go.ts @@ -1,5 +1,6 @@ import { sanitizeDisplayText } from "./display-sanitize.js"; import { fetchWithTimeout } from "./http.js"; +import { OPENCODE_CONSOLE_BASE_URL } from "./opencode-console-auth.js"; import type { OpenCodeGoResult, OpenCodeGoWindow, OpenCodeGoWindowKey } from "./types.js"; const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage"; @@ -198,3 +199,118 @@ export async function queryOpenCodeGoQuota( return { success: false, error: errorMessage(error, accessToken), retryable: true }; } } + +const OPENCODE_CONSOLE_GO_STATUS_URL = `${OPENCODE_CONSOLE_BASE_URL}/api/go/status`; + +function asMicroCents(value: unknown): number | null { + const parsed = typeof value === "string" ? Number(value) : typeof value === "number" ? value : Number.NaN; + return Number.isFinite(parsed) ? parsed : null; +} + +function normalizeConsoleMeter( + windowKey: OpenCodeGoWindowKey, + meter: Record, + fallbackResetsAtIso: string | null, +): OpenCodeGoWindow | OpenCodeGoResult { + const limit = asMicroCents(meter.limitMicroCents); + const used = asMicroCents(meter.usedMicroCents); + if (limit === null || used === null || limit < 0 || used < 0) { + return contractError(`console ${windowKey} meter microcents are invalid`); + } + + const percent = + limit === 0 ? (used > 0 ? 100 : 0) : Math.min(100, Math.max(0, Math.round((used / limit) * 100))); + const resetsAt = typeof meter.resetsAt === "string" && meter.resetsAt ? meter.resetsAt : fallbackResetsAtIso; + if (!resetsAt || !Number.isFinite(Date.parse(resetsAt))) { + return contractError(`console ${windowKey} resetsAt is missing or invalid`); + } + + return { + status: percent >= 100 ? "rate-limited" : "ok", + usagePercent: percent, + percentRemaining: 100 - percent, + resetTimeIso: new Date(Date.parse(resetsAt)).toISOString(), + }; +} + +/** + * Read OpenCode Go subscription windows from the Console API. + * + * The Console tracks Go plan access (five-hour, weekly, and monthly meters) + * for the member that owns the OAuth credential, so this works for fresh + * accounts without any pre-2.0 workspace API key. + */ +export async function queryOpenCodeGoConsoleStatus( + credential: { accessToken: string }, + options: { requestTimeoutMs?: number } = {}, +): Promise { + try { + return await fetchWithTimeout(OPENCODE_CONSOLE_GO_STATUS_URL, { + request: { + method: "GET", + headers: { + Authorization: `Bearer ${credential.accessToken}`, + Accept: "application/json", + }, + }, + timeoutMs: options.requestTimeoutMs, + consume: async (response) => { + if (!response.ok) { + if (response.status === 404 || response.status === 403) { + return { + success: false, + error: `OpenCode Go subscription not found for this console account (${response.status})`, + notSubscribed: true, + }; + } + return { + success: false, + error: `OpenCode Console API error ${response.status} (/api/go/status)`, + retryable: isRetryableHttpStatus(response.status), + }; + } + + let payload: unknown; + try { + payload = JSON.parse(await response.text()); + } catch { + return contractError("console response is not valid JSON"); + } + const root = asRecord(payload); + if (!root) return contractError("console root must be an object"); + + const access = asRecord(root.access); + if (!access) { + return { + success: false, + error: "OpenCode Go subscription is not active", + notSubscribed: true, + }; + } + + const meters = asRecord(access.meters); + if (!meters) return contractError("console access meters are missing"); + + const endsAt = typeof access.endsAt === "string" ? access.endsAt : null; + const normalized = {} as Record; + const meterByKey: Array<[OpenCodeGoWindowKey, string]> = [ + ["rolling", "fiveHour"], + ["weekly", "week"], + ["monthly", "month"], + ]; + for (const [windowKey, meterKey] of meterByKey) { + const meter = meters[meterKey]; + const meterRecord = asRecord(meter); + if (!meterRecord) return contractError(`console ${meterKey} meter is missing`); + const window = normalizeConsoleMeter(windowKey, meterRecord, endsAt); + if ("success" in window) return window; + normalized[windowKey] = window; + } + + return { success: true, rolling: normalized.rolling, weekly: normalized.weekly, monthly: normalized.monthly }; + }, + }); + } catch (error) { + return { success: false, error: errorMessage(error, credential.accessToken), retryable: true }; + } +} diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index 44f9ba97..80c452da 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -13,7 +13,6 @@ import { readLegacyAuthRows, selectConnectionCredentialRows, } from "../lib/opencode-auth.js"; -import { queryOpenCodeGoQuota } from "../lib/opencode-go.js"; import { DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, getOpenCodeGoAuthDiagnostics, @@ -22,6 +21,8 @@ import { resolveOpenCodeGoAuth, resolveOpenCodeGoAuthCached, } from "../lib/opencode-go-auth.js"; +import { queryOpenCodeGoConsoleStatus, queryOpenCodeGoQuota } from "../lib/opencode-go.js"; +import { OPENCODE_CONSOLE_BASE_URL, resolveOpenCodeConsoleAuth } from "../lib/opencode-console-auth.js"; import { normalizeQuotaProviderId } from "../lib/provider-metadata.js"; import type { OpenCodeGoResult, OpenCodeGoWindowKey } from "../lib/types.js"; import { @@ -103,6 +104,10 @@ export const opencodeGoProvider: QuotaProvider = { id: "opencode-go", async isAvailable(_ctx: QuotaProviderContext): Promise { + const consoleAuth = await resolveOpenCodeConsoleAuth(); + if (consoleAuth.state === "configured" || consoleAuth.state === "expired") { + return true; + } const auth = await resolveOpenCodeGoAuthCached({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, }); @@ -119,21 +124,84 @@ export const opencodeGoProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { + const windows = ctx.config.opencodeGoWindows ?? OPENCODE_GO_WINDOW_ORDER; + const consoleAuth = await resolveOpenCodeConsoleAuth(); + + if (consoleAuth.state === "configured") { + const consoleResult = await queryOpenCodeGoConsoleStatus(consoleAuth.credential, { + requestTimeoutMs: ctx.config.requestTimeoutMs, + }); + if (consoleResult.success) { + return withStatusDetails( + attemptedResult( + buildOpenCodeGoEntries(consoleResult, windows, OPENCODE_GO_PROVIDER_LABEL, "console:go"), + ), + [ + { key: "console_auth_state", value: "configured" }, + { key: "console_server", value: consoleAuth.credential.server ?? OPENCODE_CONSOLE_BASE_URL }, + { key: "go_source", value: "console" }, + { key: "selected_windows", value: windows.join(",") }, + ], + ); + } + if (consoleResult.notSubscribed === true) { + return withStatusDetails(attemptedResult([]), [ + { key: "console_auth_state", value: "configured" }, + { key: "opencode_go_state", value: "not_subscribed" }, + { key: "selected_windows", value: windows.join(",") }, + ]); + } + // Console request failed; fall back to the legacy API-key path below + // and surface the console error in diagnostics. + const diagnostics = await getOpenCodeGoAuthDiagnostics({ + maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, + }); + const legacyStatusDetails = [ + ...authStatusDetails(diagnostics), + { key: "console_auth_state", value: "configured" }, + { key: "console_error", value: consoleResult.error }, + { key: "go_source", value: "legacy_key" }, + { key: "selected_windows", value: windows.join(",") }, + ]; + return await fetchOpenCodeGoLegacy(ctx, diagnostics, legacyStatusDetails); + } + + if (consoleAuth.state === "expired") { + // An expired console token cannot be refreshed by the plugin; fall back + // to the legacy path, which also still accepts workspace API keys. + const diagnostics = await getOpenCodeGoAuthDiagnostics({ + maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, + }); + return await fetchOpenCodeGoLegacy(ctx, diagnostics, [ + { key: "console_auth_state", value: "expired" }, + { key: "go_source", value: "legacy_key" }, + ]); + } + const diagnostics = await getOpenCodeGoAuthDiagnostics({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, }); - const windows = ctx.config.opencodeGoWindows ?? OPENCODE_GO_WINDOW_ORDER; - const statusDetails = [ + return await fetchOpenCodeGoLegacy(ctx, diagnostics, [ ...authStatusDetails(diagnostics), { key: "selected_windows", value: windows.join(",") }, - ]; + ]); + }, +}; + +async function fetchOpenCodeGoLegacy( + ctx: QuotaProviderContext, + diagnostics: OpenCodeGoAuthDiagnostics, + statusDetails?: QuotaProviderResult["statusDetails"], +): Promise { + const windows = ctx.config.opencodeGoWindows ?? OPENCODE_GO_WINDOW_ORDER; + const baseStatusDetails = statusDetails ?? []; const auth = await resolveOpenCodeGoAuthCached({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, }); if (auth.state === "none") { notSubscribedCredentialFingerprints.clear(); - return withStatusDetails(notAttemptedResult(), statusDetails); + return withStatusDetails(notAttemptedResult(), baseStatusDetails); } if (diagnostics.source === "opencode.db") { @@ -202,7 +270,7 @@ export const opencodeGoProvider: QuotaProvider = { else errors.push({ label: group, message: result.error, retryable: result.retryable }); } return withStatusDetails(attemptedResult(entries, errors), [ - ...statusDetails, + ...baseStatusDetails, ...(entries.length === 0 && errors.length === 0 && credentials.length > 0 ? [{ key: "opencode_go_state", value: "not_subscribed" }] : []), @@ -214,7 +282,7 @@ export const opencodeGoProvider: QuotaProvider = { notSubscribedCredentialFingerprints.clear(); return withStatusDetails( attemptedErrorResult(OPENCODE_GO_PROVIDER_LABEL, auth.error), - statusDetails, + baseStatusDetails, ); } @@ -223,7 +291,7 @@ export const opencodeGoProvider: QuotaProvider = { if (notSubscribedCredentialFingerprints.has(credentialFingerprint)) { return withStatusDetails(attemptedResult([]), [ - ...statusDetails, + ...baseStatusDetails, { key: "opencode_go_state", value: "not_subscribed" }, ]); } @@ -236,7 +304,7 @@ export const opencodeGoProvider: QuotaProvider = { if (result.notSubscribed === true) { notSubscribedCredentialFingerprints.add(credentialFingerprint); return withStatusDetails(attemptedResult([]), [ - ...statusDetails, + ...baseStatusDetails, { key: "opencode_go_state", value: "not_subscribed" }, ]); } @@ -244,7 +312,7 @@ export const opencodeGoProvider: QuotaProvider = { attemptedErrorResult(OPENCODE_GO_PROVIDER_LABEL, result.error, { retryable: result.retryable, }), - [...statusDetails, { key: "live_fetch_error", value: result.error }], + [...baseStatusDetails, { key: "live_fetch_error", value: result.error }], ); } @@ -257,8 +325,7 @@ export const opencodeGoProvider: QuotaProvider = { }); return withStatusDetails(attemptedResult(buildOpenCodeGoEntries(result, windows)), [ - ...statusDetails, + ...baseStatusDetails, ...liveDetails, ]); - }, -}; +} diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index cd37e745..b25ebf18 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -11,6 +11,7 @@ import { getCredentialDatabasePaths, readAuthFile, readCredentialRows, + readLegacyAuthRows, selectConnectionCredentialRows, } from "../src/lib/opencode-auth.js"; @@ -173,23 +174,37 @@ describe("OpenCode auth reader", () => { ).toBeLessThan((await readCredentialRows()).findIndex((row) => row.id === "openai")); }); - it("fills integrations missing from the database from legacy auth.json", async () => { + it("ignores legacy auth.json entries", async () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); await writeFile( join(dataDir, "auth.json"), - JSON.stringify({ - openai: { type: "oauth", access: "file-access" }, - "legacy-extra": { type: "key", key: "legacy-key" }, - }), + JSON.stringify({ openai: { type: "oauth", access: "file-access" } }), ); await expect(readAuthFile()).resolves.toMatchObject({ "github-copilot": { access: "copilot-access" }, openai: { access: "openai-access" }, - "legacy-extra": { key: "legacy-key" }, }); }); + + it("synthesizes legacy auth.json rows for providers that lost keys in the console migration", async () => { + const { dataDir } = await createCredentialDatabase(); + vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); + await writeFile( + join(dataDir, "auth.json"), + JSON.stringify({ "opencode-go": { type: "key", key: "workspace-key" } }), + ); + + const rows = await readLegacyAuthRows(); + expect(rows).toEqual([ + expect.objectContaining({ + id: "auth-json:opencode-go", + integrationId: "opencode-go", + value: { type: "key", key: "workspace-key" }, + }), + ]); + }); }); describe("selectConnectionCredentialRows", () => { diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index caeb8515..cb41588d 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -13,7 +13,9 @@ const mocks = vi.hoisted(() => ({ resolveOpenCodeGoAuth: vi.fn(), getOpenCodeGoAuthDiagnostics: vi.fn(), queryOpenCodeGoQuota: vi.fn(), + queryOpenCodeGoConsoleStatus: vi.fn(), readLegacyAuthRows: vi.fn(async () => []), + resolveOpenCodeConsoleAuth: vi.fn(), })); vi.mock("../src/lib/opencode-go-auth.js", () => ({ @@ -24,8 +26,18 @@ vi.mock("../src/lib/opencode-go-auth.js", () => ({ resolveOpenCodeGoAuth: mocks.resolveOpenCodeGoAuth, })); -vi.mock("../src/lib/opencode-go.js", () => ({ - queryOpenCodeGoQuota: mocks.queryOpenCodeGoQuota, +vi.mock("../src/lib/opencode-go.js", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + queryOpenCodeGoQuota: mocks.queryOpenCodeGoQuota, + queryOpenCodeGoConsoleStatus: mocks.queryOpenCodeGoConsoleStatus, + }; +}); + +vi.mock("../src/lib/opencode-console-auth.js", () => ({ + OPENCODE_CONSOLE_BASE_URL: "https://opencode.ai/console", + resolveOpenCodeConsoleAuth: mocks.resolveOpenCodeConsoleAuth, })); vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ @@ -97,6 +109,102 @@ describe("opencode-go provider", () => { }); mocks.queryOpenCodeGoQuota.mockResolvedValue(successfulResult()); mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "row-token" }); + mocks.readLegacyAuthRows.mockResolvedValue([]); + mocks.resolveOpenCodeConsoleAuth.mockResolvedValue({ state: "none" }); + }); + + function consoleConfigured(): void { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "configured", + credential: { accessToken: "console-access", orgId: "wrk_1" }, + }); + } + + function consoleSuccess(): void { + mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ + success: true, + rolling: { + status: "ok", + usagePercent: 0, + percentRemaining: 100, + resetTimeIso: "2026-09-23T18:59:07.061Z", + }, + weekly: { + status: "ok", + usagePercent: 27, + percentRemaining: 73, + resetTimeIso: "2026-09-28T00:00:00.000Z", + }, + monthly: { + status: "rate-limited", + usagePercent: 100, + percentRemaining: 0, + resetTimeIso: "2026-10-02T12:48:59.000Z", + }, + }); + } + + it("reads Go windows from the console API when a console credential exists", async () => { + consoleConfigured(); + consoleSuccess(); + + const out = await runFetch(); + + expect(mocks.queryOpenCodeGoConsoleStatus).toHaveBeenCalledWith( + { accessToken: "console-access", orgId: "wrk_1" }, + { requestTimeoutMs: 5_000 }, + ); + expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "go_source", value: "console" }, + { key: "console_server", value: "https://opencode.ai/console" }, + ]), + ); + const visible = visibleEntries(out.entries, "opencode-go"); + expect(visible.map((entry) => entry.name)).toEqual([ + "OpenCode Go 5h", + "OpenCode Go Weekly", + "OpenCode Go Monthly", + ]); + }); + + it("reports a console not-subscribed state without errors", async () => { + consoleConfigured(); + mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ + success: false, + error: "OpenCode Go subscription not found for this console account (404)", + notSubscribed: true, + }); + + const out = await runFetch(); + + expectAttemptedWithNoErrors(out); + expect(out.entries).toEqual([]); + expect(out.statusDetails).toEqual( + expect.arrayContaining([{ key: "opencode_go_state", value: "not_subscribed" }]), + ); + expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); + }); + + it("falls back to the legacy key path when the console request fails", async () => { + consoleConfigured(); + mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ + success: false, + error: "OpenCode Console API error 500 (/api/go/status)", + retryable: true, + }); + + const out = await runFetch(); + + expect(mocks.queryOpenCodeGoConsoleStatus).toHaveBeenCalledOnce(); + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "go_source", value: "legacy_key" }, + { key: "console_error", value: "OpenCode Console API error 500 (/api/go/status)" }, + ]), + ); }); it("returns not attempted for absent auth without calling the API", async () => { From 32b3a47fed858ff6ede2f2f036afc855a460cd19 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Wed, 23 Sep 2026 19:19:10 +0200 Subject: [PATCH 20/97] build: target OpenCode V2 plugin package --- package.json | 4 ++-- tests/package-manifest.test.ts | 6 +++--- tests/v4-migration-docs.test.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b1da9599..666dd6bd 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "@babel/core": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@biomejs/biome": "2.3.11", - "@opencode-ai/plugin": "1.18.1", + "@opencode/plugin": "2.0.2", "@opentelemetry/sdk-metrics": "2.10.0", "@types/node": "^22.0.0", "babel-preset-solid": "^1.9.12", @@ -113,7 +113,7 @@ "better-sqlite3": "^12.10.0" }, "peerDependencies": { - "@opencode-ai/plugin": "^1.4.3" + "@opencode/plugin": "2.0.2" }, "engines": { "node": ">=22.0.0" diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index bc1f63ce..69606586 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -152,9 +152,9 @@ describe("package manifest compatibility", () => { expect(pkg.devDependencies?.yaml).toBe("^2.8.3"); }); - it("keeps the public plugin peer broad and reference-compatible development targets exact", () => { - expect(pkg.peerDependencies?.["@opencode-ai/plugin"]).toBe("^1.4.3"); - expect(pkg.devDependencies?.["@opencode-ai/plugin"]).toBe("1.18.1"); + it("reflects the local OpenCode V2 plugin dependency override", () => { + expect(pkg.peerDependencies?.["@opencode/plugin"]).toBe("2.0.2"); + expect(pkg.devDependencies?.["@opencode/plugin"]).toBe("2.0.2"); expect(pkg.dependencies?.["@opentui/core"]).toBe("^0.5.10"); expect(pkg.dependencies?.["@opentui/solid"]).toBe("^0.5.10"); expect(readme).toContain("Node.js `>= 22` is required."); diff --git a/tests/v4-migration-docs.test.ts b/tests/v4-migration-docs.test.ts index e5f815c3..f9f12e4d 100644 --- a/tests/v4-migration-docs.test.ts +++ b/tests/v4-migration-docs.test.ts @@ -24,7 +24,7 @@ const configuration = await readFile( describe("v4 migration documentation contract", () => { it("keeps requirements and navigation aligned", () => { expect(packageJson.engines?.node).toBe(">=22.0.0"); - expect(packageJson.peerDependencies?.["@opencode-ai/plugin"]).toBe("^1.4.3"); + expect(packageJson.peerDependencies?.["@opencode/plugin"]).toBe("2.0.2"); expect(packageJson.engines).not.toHaveProperty("opencode"); expect(migration).toContain("[← Back to README](../../README.md)"); From e738f292f971c9e7c08dafef61f0345e1d35cfec Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Thu, 24 Sep 2026 06:07:26 +0200 Subject: [PATCH 21/97] fix(kimi): support regional code plans --- src/lib/api-key-resolver.ts | 58 ++- src/lib/cli-show.ts | 14 +- src/lib/kimi-auth.ts | 164 ++++++-- src/lib/kimi-endpoints.ts | 27 ++ src/lib/kimi.ts | 31 +- src/lib/provider-registration.ts | 18 +- src/lib/quota-stats.ts | 4 + src/lib/quota-status.ts | 11 +- src/lib/types.ts | 5 +- src/providers/cache-policies.ts | 24 +- src/providers/kimi-code.ts | 345 ++++++++------- src/providers/registry.ts | 5 +- tests/helpers/provider-assertions.ts | 10 +- tests/lib.cli-show.test.ts | 27 +- tests/lib.invalid-aware-provider-auth.test.ts | 17 - tests/lib.kimi-auth.test.ts | 89 ++++ tests/lib.kimi.test.ts | 154 +++---- tests/lib.provider-metadata.test.ts | 36 +- tests/lib.quota-status.test.ts | 1 + tests/providers.cache-policies.test.ts | 83 +++- tests/providers.kimi-code.test.ts | 396 ++++++++++++------ tests/providers.registry.test.ts | 3 +- 22 files changed, 1042 insertions(+), 480 deletions(-) create mode 100644 src/lib/kimi-endpoints.ts create mode 100644 tests/lib.kimi-auth.test.ts diff --git a/src/lib/api-key-resolver.ts b/src/lib/api-key-resolver.ts index ef6f1397..99ac535c 100644 --- a/src/lib/api-key-resolver.ts +++ b/src/lib/api-key-resolver.ts @@ -261,6 +261,10 @@ export interface InvalidAwareProviderApiKeyResolver< > { parseAuth: (auth: unknown) => InvalidAwareAuthResult; resolve: (params?: { maxAgeMs?: number }) => Promise; + resolveWithDiagnostics: (params?: { maxAgeMs?: number }) => Promise<{ + auth: InvalidAwareAuthResult; + diagnostics: InvalidAwareAuthDiagnostics; + }>; diagnostics: (params?: { maxAgeMs?: number; }) => Promise>; @@ -351,33 +355,43 @@ function createInvalidAwareProviderApiKeyResolver (await resolveWithSource(params)).auth, - diagnostics: async (params) => { - const { auth, source } = await resolveWithSource(params); - const paths = { - checkedPaths: getApiKeyCheckedPaths({ - envVarNames: config.envVars.map((envVar) => envVar.name), - getConfigCandidates: config.getConfigCandidates, - }), - credentialDatabasePaths: config.auth.getCredentialDatabasePaths(), - }; - if (auth.state === "none") return { state: "none", source: null, ...paths }; - if (auth.state === "invalid") { - return { - state: "invalid", + const resolveWithDiagnostics = async (params?: { maxAgeMs?: number }) => { + const { auth, source } = await resolveWithSource(params); + const paths = { + checkedPaths: getApiKeyCheckedPaths({ + envVarNames: config.envVars.map((envVar) => envVar.name), + getConfigCandidates: config.getConfigCandidates, + }), + credentialDatabasePaths: config.auth.getCredentialDatabasePaths(), + }; + if (auth.state === "none") + return { auth, diagnostics: { state: "none" as const, source: null, ...paths } }; + if (auth.state === "invalid") { + return { + auth, + diagnostics: { + state: "invalid" as const, source: config.auth.authSource, error: auth.error, ...paths, - }; - } - return { - state: "configured", + }, + }; + } + return { + auth, + diagnostics: { + state: "configured" as const, source: source ?? config.auth.authSource, ...paths, - }; - }, + }, + }; + }; + + return { + parseAuth, + resolve: async (params) => (await resolveWithSource(params)).auth, + resolveWithDiagnostics, + diagnostics: async (params) => (await resolveWithDiagnostics(params)).diagnostics, }; } diff --git a/src/lib/cli-show.ts b/src/lib/cli-show.ts index 607e0dfd..5df56b09 100644 --- a/src/lib/cli-show.ts +++ b/src/lib/cli-show.ts @@ -4,7 +4,11 @@ import { findGitWorktreeRoot, getEffectiveConfigRoot } from "./config-file-utils import { sanitizeQuotaRenderData } from "./display-sanitize.js"; import { formatQuotaRows } from "./format.js"; import { formatQuotaModeHeading } from "./format-utils.js"; -import { DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, resolveKimiAuthCached } from "./kimi-auth.js"; +import { + DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, + resolveKimiCnAuthCached, + resolveKimiGlobalAuthCached, +} from "./kimi-auth.js"; import { loadConfiguredOpenCodeConfig, loadConfiguredProviderIds, @@ -162,14 +166,16 @@ async function loadCliAuthenticatedProviderIds(config: Record): typeof quotaToast?.anthropicBinaryPath === "string" ? quotaToast.anthropicBinaryPath : undefined; - const [anthropicConfigured, kimiAuth] = await Promise.all([ + const [anthropicConfigured, kimiGlobalAuth, kimiCnAuth] = await Promise.all([ hasAnthropicCredentialsConfigured({ binaryPath: anthropicBinaryPath }), - resolveKimiAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }), + resolveKimiGlobalAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }), + resolveKimiCnAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }), ]); return [ ...(anthropicConfigured ? ["anthropic"] : []), - ...(kimiAuth.state === "configured" ? ["kimi-for-coding"] : []), + ...(kimiGlobalAuth.state === "configured" ? ["kimi-code-plan-global"] : []), + ...(kimiCnAuth.state === "configured" ? ["kimi-code-plan-cn"] : []), ]; } diff --git a/src/lib/kimi-auth.ts b/src/lib/kimi-auth.ts index e1ab8c5e..dacf9463 100644 --- a/src/lib/kimi-auth.ts +++ b/src/lib/kimi-auth.ts @@ -3,59 +3,163 @@ import { createProviderApiKeyResolver, getGlobalOpencodeConfigCandidatePaths, } from "./api-key-resolver.js"; +import type { KimiQuotaEndpointId } from "./kimi-endpoints.js"; import { getCredentialDatabasePaths, readAuthFileCached } from "./opencode-auth.js"; import type { AuthData } from "./types.js"; export const DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS = 5_000; -const KIMI_AUTH_KEYS = ["kimi-for-coding", "kimi-code", "kimi"] as const; -const KIMI_PROVIDER_KEYS = ["kimi-for-coding", "kimi-code", "kimi"] as const; -const ALLOWED_KIMI_ENV_VARS = ["KIMI_API_KEY", "KIMI_CODE_API_KEY"] as const; export type KimiKeySource = + | "env:KIMI_GLOBAL_API_KEY" + | "env:KIMI_CN_API_KEY" | "env:KIMI_API_KEY" | "env:KIMI_CODE_API_KEY" | "opencode.json" | "opencode.jsonc" | "opencode.db"; -export type ResolvedKimiAuth = InvalidAwareAuthResult; -export type KimiAuthDiagnostics = InvalidAwareAuthDiagnostics; +type BaseKimiAuthDiagnostics = InvalidAwareAuthDiagnostics; + +export type ResolvedKimiAuth = + | { state: "none" } + | { state: "invalid"; error: string } + | { state: "configured"; apiKey: string; endpoint: KimiQuotaEndpointId }; + +export type KimiAuthDiagnostics = + | Exclude + | (Extract & { + endpoint: KimiQuotaEndpointId; + }); + +export type ResolvedKimiAuthWithDiagnostics = { + auth: ResolvedKimiAuth; + diagnostics: KimiAuthDiagnostics; +}; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; -const kimiAuthResolver = createProviderApiKeyResolver({ +type KimiAuthSpec = { + endpoint: KimiQuotaEndpointId; + authKeys: readonly string[]; + providerKeys: readonly string[]; + envVars: readonly { name: string; source: KimiKeySource }[]; + allowedEnvVars: readonly string[]; +}; + +const KIMI_GLOBAL_AUTH_SPEC = { + endpoint: "global", + authKeys: ["kimi-code-plan-global"], + providerKeys: ["kimi-code-plan-global"], + envVars: [{ name: "KIMI_GLOBAL_API_KEY", source: "env:KIMI_GLOBAL_API_KEY" }], + allowedEnvVars: ["KIMI_GLOBAL_API_KEY"], +} as const satisfies KimiAuthSpec; + +const KIMI_CN_AUTH_SPEC = { + endpoint: "cn", + authKeys: ["kimi-code-plan-cn", "kimi-for-coding", "kimi-code", "kimi"], + providerKeys: ["kimi-code-plan-cn", "kimi-for-coding", "kimi-code", "kimi"], envVars: [ + { name: "KIMI_CN_API_KEY", source: "env:KIMI_CN_API_KEY" }, { name: "KIMI_API_KEY", source: "env:KIMI_API_KEY" }, { name: "KIMI_CODE_API_KEY", source: "env:KIMI_CODE_API_KEY" }, ], - providerKeys: KIMI_PROVIDER_KEYS, - allowedEnvVars: ALLOWED_KIMI_ENV_VARS, - configJsonSource: "opencode.json", - configJsoncSource: "opencode.jsonc", - getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, - auth: { - policy: "invalid-aware-api-key", - authKeys: KIMI_AUTH_KEYS, - authSource: "opencode.db", - displayName: "Kimi", - defaultMaxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, - readAuth: (maxAgeMs) => readAuthFileCached({ maxAgeMs }), - getCredentialDatabasePaths, - }, -}); - -export function resolveKimiAuth(auth: AuthData | null | undefined): ResolvedKimiAuth { - return kimiAuthResolver.parseAuth(auth); -} - -export async function resolveKimiAuthCached(params?: { + allowedEnvVars: ["KIMI_CN_API_KEY", "KIMI_API_KEY", "KIMI_CODE_API_KEY"], +} as const satisfies KimiAuthSpec; + +function createKimiAuthResolver(spec: KimiAuthSpec) { + return createProviderApiKeyResolver({ + envVars: [...spec.envVars], + providerKeys: spec.providerKeys, + allowedEnvVars: spec.allowedEnvVars, + configJsonSource: "opencode.json", + configJsoncSource: "opencode.jsonc", + getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, + auth: { + policy: "invalid-aware-api-key", + authKeys: spec.authKeys, + authSource: "opencode.db", + displayName: "Kimi", + defaultMaxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, + readAuth: (maxAgeMs) => readAuthFileCached({ maxAgeMs }), + getCredentialDatabasePaths, + }, + }); +} + +const kimiGlobalAuthResolver = createKimiAuthResolver(KIMI_GLOBAL_AUTH_SPEC); +const kimiCnAuthResolver = createKimiAuthResolver(KIMI_CN_AUTH_SPEC); + +function bindAuthToEndpoint( + auth: InvalidAwareAuthResult, + endpoint: KimiQuotaEndpointId, +): ResolvedKimiAuth { + return auth.state === "configured" ? { ...auth, endpoint } : auth; +} + +function bindDiagnosticsToEndpoint( + diagnostics: BaseKimiAuthDiagnostics, + endpoint: KimiQuotaEndpointId, +): KimiAuthDiagnostics { + return diagnostics.state === "configured" ? { ...diagnostics, endpoint } : diagnostics; +} + +export function resolveKimiGlobalAuth(auth: AuthData | null | undefined): ResolvedKimiAuth { + return bindAuthToEndpoint(kimiGlobalAuthResolver.parseAuth(auth), KIMI_GLOBAL_AUTH_SPEC.endpoint); +} + +export function resolveKimiCnAuth(auth: AuthData | null | undefined): ResolvedKimiAuth { + return bindAuthToEndpoint(kimiCnAuthResolver.parseAuth(auth), KIMI_CN_AUTH_SPEC.endpoint); +} + +export async function resolveKimiGlobalAuthCached(params?: { + maxAgeMs?: number; +}): Promise { + return bindAuthToEndpoint( + await kimiGlobalAuthResolver.resolve(params), + KIMI_GLOBAL_AUTH_SPEC.endpoint, + ); +} + +export async function resolveKimiCnAuthCached(params?: { maxAgeMs?: number; }): Promise { - return kimiAuthResolver.resolve(params); + return bindAuthToEndpoint(await kimiCnAuthResolver.resolve(params), KIMI_CN_AUTH_SPEC.endpoint); +} + +export async function resolveKimiGlobalAuthWithDiagnosticsCached(params?: { + maxAgeMs?: number; +}): Promise { + const resolved = await kimiGlobalAuthResolver.resolveWithDiagnostics(params); + return { + auth: bindAuthToEndpoint(resolved.auth, KIMI_GLOBAL_AUTH_SPEC.endpoint), + diagnostics: bindDiagnosticsToEndpoint(resolved.diagnostics, KIMI_GLOBAL_AUTH_SPEC.endpoint), + }; +} + +export async function resolveKimiCnAuthWithDiagnosticsCached(params?: { + maxAgeMs?: number; +}): Promise { + const resolved = await kimiCnAuthResolver.resolveWithDiagnostics(params); + return { + auth: bindAuthToEndpoint(resolved.auth, KIMI_CN_AUTH_SPEC.endpoint), + diagnostics: bindDiagnosticsToEndpoint(resolved.diagnostics, KIMI_CN_AUTH_SPEC.endpoint), + }; +} + +export async function getKimiGlobalAuthDiagnostics(params?: { + maxAgeMs?: number; +}): Promise { + return bindDiagnosticsToEndpoint( + await kimiGlobalAuthResolver.diagnostics(params), + KIMI_GLOBAL_AUTH_SPEC.endpoint, + ); } -export async function getKimiAuthDiagnostics(params?: { +export async function getKimiCnAuthDiagnostics(params?: { maxAgeMs?: number; }): Promise { - return kimiAuthResolver.diagnostics(params); + return bindDiagnosticsToEndpoint( + await kimiCnAuthResolver.diagnostics(params), + KIMI_CN_AUTH_SPEC.endpoint, + ); } diff --git a/src/lib/kimi-endpoints.ts b/src/lib/kimi-endpoints.ts new file mode 100644 index 00000000..774a6d97 --- /dev/null +++ b/src/lib/kimi-endpoints.ts @@ -0,0 +1,27 @@ +export type KimiQuotaEndpointId = "global" | "cn"; + +export interface KimiQuotaEndpoint { + id: KimiQuotaEndpointId; + label: string; + apiBaseUrl: string; + quotaUrl: string; +} + +export const KIMI_QUOTA_ENDPOINTS: Readonly> = { + global: { + id: "global", + label: "Kimi Code", + apiBaseUrl: "https://api.kimi.ai/coding/v1", + quotaUrl: "https://api.kimi.ai/coding/v1/usages", + }, + cn: { + id: "cn", + label: "Kimi Code (CN)", + apiBaseUrl: "https://api.kimi.com/coding/v1", + quotaUrl: "https://api.kimi.com/coding/v1/usages", + }, +}; + +export function getKimiQuotaEndpoint(id: KimiQuotaEndpointId): KimiQuotaEndpoint { + return KIMI_QUOTA_ENDPOINTS[id]; +} diff --git a/src/lib/kimi.ts b/src/lib/kimi.ts index b2eb9d38..1e97c0c0 100644 --- a/src/lib/kimi.ts +++ b/src/lib/kimi.ts @@ -1,10 +1,9 @@ import { sanitizeDisplaySnippet, sanitizeDisplayText } from "./display-sanitize.js"; import { clampPercent } from "./format-utils.js"; import { fetchWithTimeout } from "./http.js"; -import { DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, resolveKimiAuthCached } from "./kimi-auth.js"; -import type { KimiQuotaWindow, KimiResult, QuotaError } from "./types.js"; +import { getKimiQuotaEndpoint, type KimiQuotaEndpointId } from "./kimi-endpoints.js"; +import type { KimiQuotaWindow, KimiResult } from "./types.js"; -const KIMI_USAGE_URL = "https://api.kimi.com/coding/v1/usages"; const USER_AGENT = "OpenCode-Quota-Toast/1.0"; function getFiniteNumber(value: unknown): number | undefined { @@ -244,22 +243,22 @@ async function fetchKimiQuotaFromUrl( } } -export async function queryKimiQuota( - options: { requestTimeoutMs?: number; apiKey?: string } = {}, -): Promise { - const auth = options.apiKey - ? { state: "configured" as const, apiKey: options.apiKey } - : await resolveKimiAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }); - if (auth.state === "none") return null; - if (auth.state === "invalid") { - return { success: false, error: auth.error }; - } - - const result = await fetchKimiQuotaFromUrl(KIMI_USAGE_URL, auth.apiKey, options.requestTimeoutMs); +export async function queryKimiQuota(options: { + apiKey: string; + endpoint: KimiQuotaEndpointId; + label?: string; + requestTimeoutMs?: number; +}): Promise { + const endpoint = getKimiQuotaEndpoint(options.endpoint); + const result = await fetchKimiQuotaFromUrl( + endpoint.quotaUrl, + options.apiKey, + options.requestTimeoutMs, + ); if (result.ok && result.windows.length > 0) { return { success: true, - label: "Kimi Code", + label: options.label ?? endpoint.label, windows: result.windows, }; } diff --git a/src/lib/provider-registration.ts b/src/lib/provider-registration.ts index a956049a..2e51362a 100644 --- a/src/lib/provider-registration.ts +++ b/src/lib/provider-registration.ts @@ -281,11 +281,23 @@ export const QUOTA_PROVIDER_REGISTRATION_SOURCE = [ }, }, { - id: "kimi-for-coding", + id: "kimi-code-plan-global", label: "Kimi Code", + runtimeIds: ["kimi-code-plan-global"], + synonyms: [], + shape: { + autoSetup: "yes", + authentication: "opencode_auth_api_key", + authFallbacks: ["env_api_key", "global_opencode_config"], + quota: "remote_api", + }, + }, + { + id: "kimi-code-plan-cn", + label: "Kimi Code (CN)", labelAliases: ["kimi-code"], - runtimeIds: ["kimi-for-coding", "kimi", "kimi-code"], - synonyms: ["kimi", "kimi-for-code", "kimi-code"], + runtimeIds: ["kimi-code-plan-cn", "kimi-for-coding", "kimi", "kimi-code"], + synonyms: ["kimi", "kimi-for-code", "kimi-code", "kimi-for-coding"], shape: { autoSetup: "yes", authentication: "opencode_auth_api_key", diff --git a/src/lib/quota-stats.ts b/src/lib/quota-stats.ts index 91196945..a3eaa408 100644 --- a/src/lib/quota-stats.ts +++ b/src/lib/quota-stats.ts @@ -191,6 +191,10 @@ const SOURCE_PROVIDER_ALIASES: Record = { chatgpt: "openai", codex: "openai", "kimi-for-coding": "moonshotai", + kimi: "moonshotai", + "kimi-code": "moonshotai", + "kimi-code-plan-global": "moonshotai", + "kimi-code-plan-cn": "moonshotai", "zai-coding-plan": "zai", glm: "zai", }; diff --git a/src/lib/quota-status.ts b/src/lib/quota-status.ts index 4af6c1b4..3d0de9ad 100644 --- a/src/lib/quota-status.ts +++ b/src/lib/quota-status.ts @@ -620,7 +620,13 @@ function supportedProviderPricingRow(params: { }; } - if (id === "kimi-for-coding" || id === "kimi-code") { + if ( + id === "kimi-code-plan-global" || + id === "kimi-code-plan-cn" || + id === "kimi-for-coding" || + id === "kimi-code" || + id === "kimi" + ) { return { id, pricing: "no", @@ -939,7 +945,8 @@ export async function buildQuotaStatusReport(params: { title: "minimax_china:", providerId: "minimax-china-coding-plan", }, - { id: "kimi", title: "kimi:", providerId: "kimi-for-coding" }, + { id: "kimi", title: "kimi:", providerId: "kimi-code-plan-global" }, + { id: "kimi_cn", title: "kimi_cn:", providerId: "kimi-code-plan-cn" }, { id: "opencode_go", title: "opencode_go:", diff --git a/src/lib/types.ts b/src/lib/types.ts index 7e6a86b7..81aff76b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -505,6 +505,9 @@ export interface AuthData { "minimax-coding-plan"?: MiniMaxAuthData; "minimax-china-coding-plan"?: MiniMaxAuthData; "minimax-cn-coding-plan"?: MiniMaxAuthData; + "kimi-code-plan-global"?: KimiAuthData; + "kimi-code-plan-cn"?: KimiAuthData; + "kimi-for-coding"?: KimiAuthData; "kimi-code"?: KimiAuthData; kimi?: KimiAuthData; xai?: XaiOAuthData; @@ -581,7 +584,7 @@ export interface KimiQuotaResult { windows: KimiQuotaWindow[]; } -export type KimiResult = KimiQuotaResult | QuotaError | null; +export type KimiResult = KimiQuotaResult | QuotaError; // ============================================================================= // Z.ai Types diff --git a/src/providers/cache-policies.ts b/src/providers/cache-policies.ts index f536bb07..e1a184da 100644 --- a/src/providers/cache-policies.ts +++ b/src/providers/cache-policies.ts @@ -11,7 +11,11 @@ import { resolveGoogleAntigravityAuthIdentity } from "../lib/google.js"; import { resolveGoogleAgyAuthIdentity } from "../lib/google-agy.js"; import { resolveGeminiCliAuthIdentity } from "../lib/google-gemini-cli.js"; import { resolveKiloApiKey } from "../lib/kilo-config.js"; -import { DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, resolveKimiAuthCached } from "../lib/kimi-auth.js"; +import { + DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, + resolveKimiCnAuthCached, + resolveKimiGlobalAuthCached, +} from "../lib/kimi-auth.js"; import { DEFAULT_MIMO_CONFIG_CACHE_MAX_AGE_MS, resolveMimoConfigCached, @@ -199,9 +203,21 @@ export const PROVIDER_CACHE_POLICIES = { ? { credential: resolved.apiKey, qualifiers: [resolved.endpoint] } : null; }), - "kimi-for-coding": resolvedCredentialPolicy("kimi-for-coding", async () => { - const resolved = await resolveKimiAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }); - return resolved.state === "configured" ? { credential: resolved.apiKey } : null; + "kimi-code-plan-global": resolvedCredentialPolicy("kimi-code-plan-global", async () => { + const resolved = await resolveKimiGlobalAuthCached({ + maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, + }); + return resolved.state === "configured" + ? { credential: resolved.apiKey, qualifiers: [resolved.endpoint] } + : null; + }), + "kimi-code-plan-cn": resolvedCredentialPolicy("kimi-code-plan-cn", async () => { + const resolved = await resolveKimiCnAuthCached({ + maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, + }); + return resolved.state === "configured" + ? { credential: resolved.apiKey, qualifiers: [resolved.endpoint] } + : null; }), deepseek: resolvedCredentialPolicy("deepseek", async () => { const resolved = await resolveDeepSeekApiKey(); diff --git a/src/providers/kimi-code.ts b/src/providers/kimi-code.ts index 47a5a82c..8f8d2c9f 100644 --- a/src/providers/kimi-code.ts +++ b/src/providers/kimi-code.ts @@ -7,10 +7,16 @@ import type { import { queryKimiQuota } from "../lib/kimi.js"; import { DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, - getKimiAuthDiagnostics, - resolveKimiAuth, - resolveKimiAuthCached, + type ResolvedKimiAuth, + type ResolvedKimiAuthWithDiagnostics, + resolveKimiCnAuth, + resolveKimiCnAuthCached, + resolveKimiCnAuthWithDiagnosticsCached, + resolveKimiGlobalAuth, + resolveKimiGlobalAuthCached, + resolveKimiGlobalAuthWithDiagnosticsCached, } from "../lib/kimi-auth.js"; +import { getKimiQuotaEndpoint, type KimiQuotaEndpointId } from "../lib/kimi-endpoints.js"; import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import { normalizeQuotaProviderId } from "../lib/provider-metadata.js"; @@ -20,6 +26,7 @@ import { attemptedErrorResult, attemptedResult, notAttemptedResult, + statusDetailsFromRecord, withStatusDetails, } from "./result-helpers.js"; @@ -27,155 +34,197 @@ function formatUsageRight(window: { used: number; limit: number }): string { return `${window.used}/${window.limit}`; } -export const kimiCodeProvider: QuotaProvider = { - id: "kimi-for-coding", - - async isAvailable(ctx: QuotaProviderContext): Promise { - const providerAvailable = await isCanonicalProviderAvailable({ - ctx, - providerId: "kimi-for-coding", - fallbackOnError: false, - }); - if (!providerAvailable) { - return false; - } - - const auth = await resolveKimiAuthCached({ - maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, - }); - return auth.state === "configured" || auth.state === "invalid"; - }, - - matchesCurrentModel(model: string): boolean { - const [provider] = model.toLowerCase().split("/", 2); - return normalizeQuotaProviderId(provider) === "kimi-for-coding"; - }, - - async fetch(ctx: QuotaProviderContext): Promise { - const diagnostics = await getKimiAuthDiagnostics({ - maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, - }); - const authDetails = apiKeyStatusDetails(diagnostics); - const auth = await resolveKimiAuthCached({ - maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, - }); - - if (auth.state === "none") { - return withStatusDetails(notAttemptedResult(), authDetails); - } - - if (diagnostics.source === "opencode.db") { - const credentialRows = (await readCredentialRows()).filter((row) => - ["kimi-for-coding", "kimi-code", "kimi"].includes(row.integrationId), - ); - const rowNames = formatCredentialDisplayNames( - "Kimi Code", - credentialRows.map((row) => ({ row, fallbackName: "Kimi Code" })), - ); - const displayNamesByRowId = new Map( - credentialRows.map((row, index) => [row.id, rowNames[index] ?? "Kimi Code"]), - ); - const invalidErrors: QuotaProviderResult["errors"] = []; - const credentials = credentialRows.flatMap((row) => { - const rowAuth = resolveKimiAuth({ [row.integrationId]: row.value } as AuthData); - if (rowAuth.state === "invalid") { - invalidErrors.push({ - label: displayNamesByRowId.get(row.id) ?? "Kimi Code", - message: rowAuth.error, - }); - } - return rowAuth.state === "configured" ? [{ row, auth: rowAuth }] : []; +type KimiProviderSpec = { + id: "kimi-code-plan-global" | "kimi-code-plan-cn"; + label: string; + endpoint: KimiQuotaEndpointId; + integrationIds: readonly string[]; + parseAuth: (auth: AuthData) => ResolvedKimiAuth; + resolveAuthCached: (params?: { maxAgeMs?: number }) => Promise; + resolveAuthWithDiagnosticsCached: (params?: { + maxAgeMs?: number; + }) => Promise; +}; + +function matchesKimiCurrentModel(model: string, spec: KimiProviderSpec): boolean { + const [provider = "", modelId] = model.toLowerCase().split("/", 2); + if (!modelId) return false; + return normalizeQuotaProviderId(provider) === spec.id; +} + +function createKimiProvider(spec: KimiProviderSpec): QuotaProvider { + return { + id: spec.id, + + async isAvailable(ctx: QuotaProviderContext): Promise { + const providerAvailable = await isCanonicalProviderAvailable({ + ctx, + providerId: spec.id, + fallbackOnError: false, }); - if (credentials.length > 0 || invalidErrors.length > 0) { - const results = await Promise.all( - credentials.map(async ({ row, auth }) => ({ - row, - result: await queryKimiQuota({ - requestTimeoutMs: ctx.config?.requestTimeoutMs, - apiKey: auth.apiKey, - }), - })), + if (!providerAvailable) return false; + + const auth = await spec.resolveAuthCached({ + maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, + }); + return auth.state === "configured" || auth.state === "invalid"; + }, + + matchesCurrentModel(model: string): boolean { + return matchesKimiCurrentModel(model, spec); + }, + + async fetch(ctx: QuotaProviderContext): Promise { + const { auth, diagnostics } = await spec.resolveAuthWithDiagnosticsCached({ + maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, + }); + const endpoint = getKimiQuotaEndpoint( + auth.state === "configured" ? auth.endpoint : spec.endpoint, + ); + const authDetails = [ + ...apiKeyStatusDetails(diagnostics), + ...statusDetailsFromRecord({ + api_endpoint: endpoint.id, + api_base_url: endpoint.apiBaseUrl, + }), + ]; + + if (auth.state === "none") { + return withStatusDetails(notAttemptedResult(), authDetails); + } + + if (diagnostics.source === "opencode.db") { + const credentialRows = (await readCredentialRows()).filter((row) => + spec.integrationIds.includes(row.integrationId), + ); + const rowNames = formatCredentialDisplayNames( + spec.label, + credentialRows.map((row) => ({ row, fallbackName: spec.label })), + ); + const displayNamesByRowId = new Map( + credentialRows.map((row, index) => [row.id, rowNames[index] ?? spec.label]), ); - const entries: QuotaToastEntry[] = []; - const errors: QuotaProviderResult["errors"] = [...invalidErrors]; - for (const { row, result } of results) { - const group = displayNamesByRowId.get(row.id) ?? "Kimi Code"; - if (!result) continue; - if (!result.success) { - errors.push({ label: group, message: result.error }); - continue; + const invalidErrors: QuotaProviderResult["errors"] = []; + const credentials = credentialRows.flatMap((row) => { + const rowAuth = spec.parseAuth({ [row.integrationId]: row.value } as AuthData); + if (rowAuth.state === "invalid") { + invalidErrors.push({ + label: displayNamesByRowId.get(row.id) ?? spec.label, + message: rowAuth.error, + }); } - entries.push( - ...result.windows.map((window) => ({ - accounting: { - resultType: "quota" as const, - acquisitionMethod: "remote_api" as const, - ownership: "maintained" as const, - authority: "provider_reported" as const, - sourceId: row.id, - }, - name: `${group} ${window.label}`, - group, - label: `${window.label}:`, - right: formatUsageRight(window), - percentRemaining: window.percentRemaining, - resetTimeIso: window.resetTimeIso, + return rowAuth.state === "configured" ? [{ row, auth: rowAuth }] : []; + }); + if (credentials.length > 0 || invalidErrors.length > 0) { + const results = await Promise.all( + credentials.map(async ({ row, auth: rowAuth }) => ({ + row, + result: await queryKimiQuota({ + apiKey: rowAuth.apiKey, + endpoint: rowAuth.endpoint, + label: spec.label, + requestTimeoutMs: ctx.config?.requestTimeoutMs, + }), })), ); + const entries: QuotaToastEntry[] = []; + const errors: QuotaProviderResult["errors"] = [...invalidErrors]; + for (const { row, result } of results) { + const group = displayNamesByRowId.get(row.id) ?? spec.label; + if (!result.success) { + errors.push({ label: group, message: result.error }); + continue; + } + entries.push( + ...result.windows.map((window) => ({ + accounting: { + resultType: "quota" as const, + acquisitionMethod: "remote_api" as const, + ownership: "maintained" as const, + authority: "provider_reported" as const, + sourceId: row.id, + }, + name: `${group} ${window.label}`, + group, + label: `${window.label}:`, + right: formatUsageRight(window), + percentRemaining: window.percentRemaining, + resetTimeIso: window.resetTimeIso, + })), + ); + } + return withStatusDetails(attemptedResult(entries, errors), authDetails); } - return withStatusDetails(attemptedResult(entries, errors), authDetails); } - } - - if (auth.state === "invalid") { - return withStatusDetails(attemptedErrorResult("Kimi Code", auth.error), authDetails); - } - - const result = await queryKimiQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs }); - - if (!result) { - return withStatusDetails(notAttemptedResult(), [ - ...authDetails, - { key: "live_fetch_error", value: "Kimi API key became unavailable before fetch" }, - ]); - } - - if (!result.success) { - return withStatusDetails(attemptedErrorResult("Kimi Code", result.error), [ - ...authDetails, - { key: "live_fetch_error", value: result.error }, - ]); - } - - const entries: QuotaToastEntry[] = result.windows.map((window) => ({ - accounting: { - resultType: "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", - }, - name: `${result.label} ${window.label}`, - group: result.label, - label: `${window.label}:`, - right: formatUsageRight(window), - percentRemaining: window.percentRemaining, - resetTimeIso: window.resetTimeIso, - })); - - return withStatusDetails( - attemptedResult(entries, [], { - singleWindowDisplayName: result.label, - }), - [ - ...authDetails, - ...result.windows.map((window) => ({ - key: window.label.toLowerCase().replace(/\s+/g, "_"), - value: `used=${window.used}/${window.limit} percent_remaining=${window.percentRemaining} reset_at=${window.resetTimeIso ?? "(none)"}`, - })), - ...(result.windows.length === 0 - ? [{ key: "live_state", value: "no reportable Kimi quota" }] - : []), - ], - ); - }, -}; + + if (auth.state === "invalid") { + return withStatusDetails(attemptedErrorResult(spec.label, auth.error), authDetails); + } + + const result = await queryKimiQuota({ + apiKey: auth.apiKey, + endpoint: auth.endpoint, + label: spec.label, + requestTimeoutMs: ctx.config?.requestTimeoutMs, + }); + + if (!result.success) { + return withStatusDetails(attemptedErrorResult(spec.label, result.error), [ + ...authDetails, + { key: "live_fetch_error", value: result.error }, + ]); + } + + const entries: QuotaToastEntry[] = result.windows.map((window) => ({ + accounting: { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + }, + name: `${result.label} ${window.label}`, + group: result.label, + label: `${window.label}:`, + right: formatUsageRight(window), + percentRemaining: window.percentRemaining, + resetTimeIso: window.resetTimeIso, + })); + + return withStatusDetails( + attemptedResult(entries, [], { + singleWindowDisplayName: result.label, + }), + [ + ...authDetails, + ...result.windows.map((window) => ({ + key: window.label.toLowerCase().replace(/\s+/g, "_"), + value: `used=${window.used}/${window.limit} percent_remaining=${window.percentRemaining} reset_at=${window.resetTimeIso ?? "(none)"}`, + })), + ...(result.windows.length === 0 + ? [{ key: "live_state", value: `no reportable ${spec.label} quota` }] + : []), + ], + ); + }, + }; +} + +export const kimiCodePlanGlobalProvider: QuotaProvider = createKimiProvider({ + id: "kimi-code-plan-global", + label: "Kimi Code", + endpoint: "global", + integrationIds: ["kimi-code-plan-global"], + parseAuth: resolveKimiGlobalAuth, + resolveAuthCached: resolveKimiGlobalAuthCached, + resolveAuthWithDiagnosticsCached: resolveKimiGlobalAuthWithDiagnosticsCached, +}); + +export const kimiCodePlanCnProvider: QuotaProvider = createKimiProvider({ + id: "kimi-code-plan-cn", + label: "Kimi Code (CN)", + endpoint: "cn", + integrationIds: ["kimi-code-plan-cn", "kimi-for-coding", "kimi-code", "kimi"], + parseAuth: resolveKimiCnAuth, + resolveAuthCached: resolveKimiCnAuthCached, + resolveAuthWithDiagnosticsCached: resolveKimiCnAuthWithDiagnosticsCached, +}); diff --git a/src/providers/registry.ts b/src/providers/registry.ts index aae3277a..26b2a979 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -22,7 +22,7 @@ import { googleAgyProvider } from "./google-agy.js"; import { googleAntigravityProvider } from "./google-antigravity.js"; import { googleGeminiCliProvider } from "./google-gemini-cli.js"; import { kiloProvider } from "./kilo.js"; -import { kimiCodeProvider } from "./kimi-code.js"; +import { kimiCodePlanCnProvider, kimiCodePlanGlobalProvider } from "./kimi-code.js"; import { xiaomiProvider } from "./mimo.js"; import { minimaxChinaCodingPlanProvider, @@ -61,7 +61,8 @@ const PROVIDERS_BY_ID = { nanogpt: nanoGptProvider, "minimax-coding-plan": minimaxCodingPlanProvider, "minimax-china-coding-plan": minimaxChinaCodingPlanProvider, - "kimi-for-coding": kimiCodeProvider, + "kimi-code-plan-global": kimiCodePlanGlobalProvider, + "kimi-code-plan-cn": kimiCodePlanCnProvider, deepseek: deepseekProvider, xai: xaiProvider, xiaomi: xiaomiProvider, diff --git a/tests/helpers/provider-assertions.ts b/tests/helpers/provider-assertions.ts index 616729b7..94c74d65 100644 --- a/tests/helpers/provider-assertions.ts +++ b/tests/helpers/provider-assertions.ts @@ -227,7 +227,15 @@ export const PROVIDER_ACCOUNTING_LEDGER: Record ({ authMocks: { anthropicConfigured: false, - kimiState: "none" as "none" | "configured", + kimiGlobalState: "none" as "none" | "configured", + kimiCnState: "none" as "none" | "configured", }, mockProviders: [] as any[], runtimeDirs: { @@ -32,7 +33,8 @@ vi.mock("../src/lib/anthropic.js", () => ({ vi.mock("../src/lib/kimi-auth.js", () => ({ DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS: 30_000, - resolveKimiAuthCached: vi.fn(async () => ({ state: authMocks.kimiState })), + resolveKimiGlobalAuthCached: vi.fn(async () => ({ state: authMocks.kimiGlobalState })), + resolveKimiCnAuthCached: vi.fn(async () => ({ state: authMocks.kimiCnState })), })); vi.mock("../src/providers/registry.js", () => ({ @@ -75,7 +77,8 @@ describe("runCliShowCommand", () => { beforeEach(() => { authMocks.anthropicConfigured = false; - authMocks.kimiState = "none"; + authMocks.kimiGlobalState = "none"; + authMocks.kimiCnState = "none"; savedConfigDir = process.env.OPENCODE_CONFIG_DIR; delete process.env.OPENCODE_CONFIG_DIR; tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-cli-show-")); @@ -102,7 +105,7 @@ describe("runCliShowCommand", () => { rmSync(tempDir, { recursive: true, force: true }); }); - it("adds offline Anthropic and Kimi runtime ids only when local authentication exists", async () => { + it("synthesizes offline Kimi runtime ids independently by region", async () => { writeFileSync(join(workspaceDir, "opencode.json"), "{}", "utf8"); const unauthenticated = await createCliQuotaClient({ @@ -111,11 +114,21 @@ describe("runCliShowCommand", () => { expect(unauthenticated.data?.providers).toEqual([]); authMocks.anthropicConfigured = true; - authMocks.kimiState = "configured"; - const authenticated = await createCliQuotaClient({ + authMocks.kimiCnState = "configured"; + const cnOnly = await createCliQuotaClient({ configRootDir: workspaceDir, }).config.providers(); - expect(authenticated.data?.providers).toEqual([{ id: "anthropic" }, { id: "kimi-for-coding" }]); + expect(cnOnly.data?.providers).toEqual([{ id: "anthropic" }, { id: "kimi-code-plan-cn" }]); + + authMocks.kimiGlobalState = "configured"; + const bothRegions = await createCliQuotaClient({ + configRootDir: workspaceDir, + }).config.providers(); + expect(bothRegions.data?.providers).toEqual([ + { id: "anthropic" }, + { id: "kimi-code-plan-global" }, + { id: "kimi-code-plan-cn" }, + ]); }); it("renders a compact quota glance and returns zero when quota rows are available", async () => { diff --git a/tests/lib.invalid-aware-provider-auth.test.ts b/tests/lib.invalid-aware-provider-auth.test.ts index d48342a9..377e5f8e 100644 --- a/tests/lib.invalid-aware-provider-auth.test.ts +++ b/tests/lib.invalid-aware-provider-auth.test.ts @@ -38,23 +38,6 @@ type InvalidAwareDescriptor = ProviderApiKeyContractDescriptor { - const module = await import("../src/lib/kimi-auth.js"); - return { - parseAuth: module.resolveKimiAuth, - resolve: module.resolveKimiAuthCached, - diagnostics: module.getKimiAuthDiagnostics, - getConfigCandidates: module.getOpencodeConfigCandidatePaths, - }; - }, - }, { name: "Z.ai", displayName: "Z.ai", diff --git a/tests/lib.kimi-auth.test.ts b/tests/lib.kimi-auth.test.ts new file mode 100644 index 00000000..7708d22d --- /dev/null +++ b/tests/lib.kimi-auth.test.ts @@ -0,0 +1,89 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const authMocks = vi.hoisted(() => ({ + readAuthFileCached: vi.fn(), +})); + +vi.mock("../src/lib/opencode-auth.js", () => ({ + readAuthFileCached: authMocks.readAuthFileCached, + getCredentialDatabasePaths: () => ["/test/opencode.db"], +})); +vi.mock("../src/lib/api-key-resolver.js", async (importOriginal) => ({ + ...(await importOriginal()), + getGlobalOpencodeConfigCandidatePaths: () => [], +})); + +import { + resolveKimiCnAuth, + resolveKimiCnAuthWithDiagnosticsCached, + resolveKimiGlobalAuth, + resolveKimiGlobalAuthWithDiagnosticsCached, +} from "../src/lib/kimi-auth.js"; + +describe("Kimi regional auth", () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + for (const name of [ + "KIMI_GLOBAL_API_KEY", + "KIMI_CN_API_KEY", + "KIMI_API_KEY", + "KIMI_CODE_API_KEY", + ]) { + delete process.env[name]; + } + authMocks.readAuthFileCached.mockReset().mockResolvedValue(null); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("maps legacy database aliases exclusively to CN", () => { + const auth = { kimi: { type: "api", key: "legacy-cn" } }; + expect(resolveKimiCnAuth(auth)).toEqual({ + state: "configured", + apiKey: "legacy-cn", + endpoint: "cn", + }); + expect(resolveKimiGlobalAuth(auth)).toEqual({ state: "none" }); + }); + + it("resolves independent regional credentials from the database with endpoint provenance", async () => { + authMocks.readAuthFileCached.mockResolvedValue({ + "kimi-code-plan-global": { type: "api", key: "global-db" }, + "kimi-code-plan-cn": { type: "api", key: "cn-db" }, + }); + const global = await resolveKimiGlobalAuthWithDiagnosticsCached(); + const cn = await resolveKimiCnAuthWithDiagnosticsCached(); + expect(global.auth).toEqual({ state: "configured", apiKey: "global-db", endpoint: "global" }); + expect(cn.auth).toEqual({ state: "configured", apiKey: "cn-db", endpoint: "cn" }); + expect(global.diagnostics).toMatchObject({ + state: "configured", + source: "opencode.db", + endpoint: "global", + credentialDatabasePaths: ["/test/opencode.db"], + }); + expect(cn.diagnostics).toMatchObject({ + state: "configured", + source: "opencode.db", + endpoint: "cn", + }); + }); + + it("uses region-specific environment keys without crossing credentials", async () => { + process.env.KIMI_GLOBAL_API_KEY = "global-env"; + process.env.KIMI_CN_API_KEY = "cn-env"; + const global = await resolveKimiGlobalAuthWithDiagnosticsCached(); + const cn = await resolveKimiCnAuthWithDiagnosticsCached(); + expect(global.auth).toEqual({ state: "configured", apiKey: "global-env", endpoint: "global" }); + expect(cn.auth).toEqual({ state: "configured", apiKey: "cn-env", endpoint: "cn" }); + expect(global.diagnostics).toMatchObject({ + source: "env:KIMI_GLOBAL_API_KEY", + endpoint: "global", + }); + expect(cn.diagnostics).toMatchObject({ source: "env:KIMI_CN_API_KEY", endpoint: "cn" }); + expect(authMocks.readAuthFileCached).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/lib.kimi.test.ts b/tests/lib.kimi.test.ts index 505ecb54..c0d601ab 100644 --- a/tests/lib.kimi.test.ts +++ b/tests/lib.kimi.test.ts @@ -1,9 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const authMocks = vi.hoisted(() => ({ - resolveKimiAuthCached: vi.fn(), -})); - const fetchMocks = vi.hoisted(() => { const fetchResponse = vi.fn(); return { @@ -22,19 +18,14 @@ const fetchMocks = vi.hoisted(() => { }; }); -vi.mock("../src/lib/kimi-auth.js", () => ({ - resolveKimiAuthCached: authMocks.resolveKimiAuthCached, - DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS: 5_000, -})); - vi.mock("../src/lib/http.js", () => ({ fetchWithTimeout: fetchMocks.fetchWithTimeout, })); import { queryKimiQuota } from "../src/lib/kimi.js"; -function mockKimiAuthConfigured(apiKey = "test-key") { - authMocks.resolveKimiAuthCached.mockResolvedValueOnce({ state: "configured", apiKey }); +function queryCn(apiKey = "cn-test-key", requestTimeoutMs?: number) { + return queryKimiQuota({ apiKey, endpoint: "cn", requestTimeoutMs }); } function mockKimiHttpSuccess(payload: unknown) { @@ -52,26 +43,59 @@ function mockKimiHttpFailure(status: number, text: string) { }); } +const usagePayload = { + usage: { + limit: "100", + used: "10", + remaining: "90", + }, +}; + describe("queryKimiQuota", () => { beforeEach(() => { vi.clearAllMocks(); - authMocks.resolveKimiAuthCached.mockResolvedValue({ state: "configured", apiKey: "test-key" }); }); - it("returns null when auth is none", async () => { - authMocks.resolveKimiAuthCached.mockResolvedValueOnce({ state: "none" }); - const result = await queryKimiQuota(); - expect(result).toBeNull(); + it("binds the Global key to the Global endpoint and forwards timeout", async () => { + mockKimiHttpSuccess(usagePayload); + + const result = await queryKimiQuota({ + apiKey: "global-secret", + endpoint: "global", + requestTimeoutMs: 4321, + }); + + expect(fetchMocks.fetchWithTimeout).toHaveBeenCalledTimes(1); + expect(fetchMocks.fetchWithTimeout).toHaveBeenCalledWith( + "https://api.kimi.ai/coding/v1/usages", + expect.objectContaining({ + request: expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "Bearer global-secret" }), + }), + timeoutMs: 4321, + }), + ); + expect(result).toMatchObject({ success: true, label: "Kimi Code" }); }); - it("returns error when auth is invalid", async () => { - authMocks.resolveKimiAuthCached.mockResolvedValueOnce({ state: "invalid", error: "bad auth" }); - const result = await queryKimiQuota(); - expect(result).toEqual({ success: false, error: "bad auth" }); + it("binds the CN key to the CN endpoint", async () => { + mockKimiHttpSuccess(usagePayload); + + const result = await queryCn("cn-secret"); + + expect(fetchMocks.fetchWithTimeout).toHaveBeenCalledTimes(1); + expect(fetchMocks.fetchWithTimeout).toHaveBeenCalledWith( + "https://api.kimi.com/coding/v1/usages", + expect.objectContaining({ + request: expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "Bearer cn-secret" }), + }), + }), + ); + expect(result).toMatchObject({ success: true, label: "Kimi Code (CN)" }); }); - it("parses string numbers from real API shape", async () => { - mockKimiAuthConfigured(); + it("parses string numbers from the existing API shape", async () => { mockKimiHttpSuccess({ usage: { limit: "100", @@ -81,10 +105,7 @@ describe("queryKimiQuota", () => { }, limits: [ { - window: { - duration: 300, - timeUnit: "TIME_UNIT_MINUTE", - }, + window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" }, detail: { limit: "100", used: "22", @@ -93,16 +114,14 @@ describe("queryKimiQuota", () => { }, }, ], - parallel: { - limit: "20", - }, + parallel: { limit: "20" }, }); - const result = await queryKimiQuota(); + const result = await queryCn(); expect(result).toMatchObject({ success: true, - label: "Kimi Code", + label: "Kimi Code (CN)", windows: [ { label: "Weekly limit", @@ -123,74 +142,55 @@ describe("queryKimiQuota", () => { }); it("computes used from remaining when used is absent", async () => { - mockKimiAuthConfigured(); - mockKimiHttpSuccess({ - usage: { - limit: "100", - remaining: "30", - }, - }); + mockKimiHttpSuccess({ usage: { limit: "100", remaining: "30" } }); - const result = await queryKimiQuota(); - - expect(result).toMatchObject({ + await expect(queryCn()).resolves.toMatchObject({ success: true, - windows: [ - { - label: "Weekly limit", - used: 70, - limit: 100, - percentRemaining: 30, - }, - ], + windows: [{ label: "Weekly limit", used: 70, limit: 100, percentRemaining: 30 }], }); }); - it("returns error when endpoint fails", async () => { - mockKimiAuthConfigured(); - mockKimiHttpFailure(401, "Unauthorized"); + it.each([ + [401, "Unauthorized"], + [403, "Forbidden access"], + ])("returns a sanitized HTTP %s error without trying the other host", async (status, body) => { + mockKimiHttpFailure(status, body); - const result = await queryKimiQuota(); - - expect(result).toEqual({ + await expect(queryCn()).resolves.toEqual({ success: false, - error: "Kimi API error 401: Unauthorized", + error: `Kimi API error ${status}: ${body}`, }); + expect(fetchMocks.fetchWithTimeout).toHaveBeenCalledTimes(1); + expect(fetchMocks.fetchWithTimeout.mock.calls[0]?.[0]).toBe( + "https://api.kimi.com/coding/v1/usages", + ); }); - it("returns error with unexpected response keys when endpoint has no usable data", async () => { - mockKimiAuthConfigured(); + it("returns an error for an empty usable payload without trying the other host", async () => { mockKimiHttpSuccess({ message: "hello", code: 0 }); - const result = await queryKimiQuota(); - - expect(result).toMatchObject({ + await expect(queryCn()).resolves.toEqual({ success: false, error: "Unexpected response structure (keys: message, code)", }); + expect(fetchMocks.fetchWithTimeout).toHaveBeenCalledTimes(1); }); - it("returns API error on non-200 with sanitized text", async () => { - mockKimiAuthConfigured(); - mockKimiHttpFailure(403, "Forbidden access"); - - const result = await queryKimiQuota(); + it("sanitizes thrown errors without trying the other host", async () => { + fetchMocks.fetchResponse.mockRejectedValue(new Error("network\u001b[31m error")); - expect(result).toMatchObject({ + await expect(queryCn()).resolves.toEqual({ success: false, - error: "Kimi API error 403: Forbidden access", + error: "network error", }); + expect(fetchMocks.fetchWithTimeout).toHaveBeenCalledTimes(1); }); - it("sanitizes thrown errors", async () => { - mockKimiAuthConfigured(); - fetchMocks.fetchResponse.mockRejectedValue(new Error("network error")); + it("preserves an explicit regional label", async () => { + mockKimiHttpSuccess(usagePayload); - const result = await queryKimiQuota(); - - expect(result).toEqual({ - success: false, - error: "network error", - }); + await expect( + queryKimiQuota({ apiKey: "key", endpoint: "global", label: "Custom Kimi" }), + ).resolves.toMatchObject({ success: true, label: "Custom Kimi" }); }); }); diff --git a/tests/lib.provider-metadata.test.ts b/tests/lib.provider-metadata.test.ts index b3d71d73..049a0abf 100644 --- a/tests/lib.provider-metadata.test.ts +++ b/tests/lib.provider-metadata.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { getQuotaProviderDisplayLabel, + getQuotaProviderIdsForRuntimeId, getQuotaProviderRuntimeIds, getQuotaProviderShape, normalizeQuotaProviderId, @@ -154,7 +155,14 @@ describe("provider-metadata", () => { quota: "remote_api", }, { - id: "kimi-for-coding", + id: "kimi-code-plan-global", + autoSetup: "yes", + authentication: "opencode_auth_api_key", + authFallbacks: ["env_api_key", "global_opencode_config"], + quota: "remote_api", + }, + { + id: "kimi-code-plan-cn", autoSetup: "yes", authentication: "opencode_auth_api_key", authFallbacks: ["env_api_key", "global_opencode_config"], @@ -236,11 +244,11 @@ describe("provider-metadata", () => { it("preserves direct label aliases from the authored catalog", () => { expect(QUOTA_PROVIDER_LABELS["minimax-cn-coding-plan"]).toBe("MiniMax Token Plan (CN)"); - expect(QUOTA_PROVIDER_LABELS["kimi-code"]).toBe("Kimi Code"); + expect(QUOTA_PROVIDER_LABELS["kimi-code"]).toBe("Kimi Code (CN)"); expect(QUOTA_PROVIDER_CATALOG["minimax-china-coding-plan"].labelAliases).toContain( "minimax-cn-coding-plan", ); - expect(QUOTA_PROVIDER_CATALOG["kimi-for-coding"].labelAliases).toContain("kimi-code"); + expect(QUOTA_PROVIDER_CATALOG["kimi-code-plan-cn"].labelAliases).toContain("kimi-code"); }); it("keeps canonical provider setup ids unique", () => { @@ -304,7 +312,9 @@ describe("provider-metadata", () => { "minimax-cn", "minimax-china", ]); - expect(QUOTA_PROVIDER_RUNTIME_IDS["kimi-for-coding"]).toEqual([ + expect(QUOTA_PROVIDER_RUNTIME_IDS["kimi-code-plan-global"]).toEqual(["kimi-code-plan-global"]); + expect(QUOTA_PROVIDER_RUNTIME_IDS["kimi-code-plan-cn"]).toEqual([ + "kimi-code-plan-cn", "kimi-for-coding", "kimi", "kimi-code", @@ -370,7 +380,18 @@ describe("provider-metadata", () => { "minimax-cn", "minimax-china", ]); - expect(getQuotaProviderRuntimeIds("kimi")).toEqual(["kimi-for-coding", "kimi", "kimi-code"]); + expect(getQuotaProviderRuntimeIds("kimi-code-plan-global")).toEqual(["kimi-code-plan-global"]); + expect(getQuotaProviderRuntimeIds("kimi")).toEqual([ + "kimi-code-plan-cn", + "kimi-for-coding", + "kimi", + "kimi-code", + ]); + expect(getQuotaProviderIdsForRuntimeId("kimi-code-plan-global")).toEqual([ + "kimi-code-plan-global", + ]); + expect(getQuotaProviderIdsForRuntimeId("kimi-code-plan-cn")).toEqual(["kimi-code-plan-cn"]); + expect(getQuotaProviderIdsForRuntimeId("kimi")).toEqual(["kimi-code-plan-cn"]); expect(getQuotaProviderRuntimeIds("deep-seek")).toEqual(["deepseek"]); expect(getQuotaProviderRuntimeIds("opencode-zen")).toEqual(["opencode", "opencode-zen"]); expect(getQuotaProviderRuntimeIds("xai")).toEqual(["xai"]); @@ -498,8 +519,9 @@ describe("provider-metadata", () => { expect(getQuotaProviderDisplayLabel("nano-gpt")).toBe("NanoGPT"); expect(getQuotaProviderDisplayLabel("minimax")).toBe("MiniMax Token Plan"); expect(getQuotaProviderDisplayLabel("minimax-cn-coding-plan")).toBe("MiniMax Token Plan (CN)"); - expect(getQuotaProviderDisplayLabel("kimi-code")).toBe("Kimi Code"); - expect(getQuotaProviderDisplayLabel("kimi")).toBe("Kimi Code"); + expect(getQuotaProviderDisplayLabel("kimi-code-plan-global")).toBe("Kimi Code"); + expect(getQuotaProviderDisplayLabel("kimi-code")).toBe("Kimi Code (CN)"); + expect(getQuotaProviderDisplayLabel("kimi")).toBe("Kimi Code (CN)"); expect(getQuotaProviderDisplayLabel("deep-seek")).toBe("DeepSeek"); expect(getQuotaProviderDisplayLabel("opencode-zen")).toBe("OpenCode Zen"); expect(getQuotaProviderDisplayLabel("kilo")).toBe("Kilo Gateway"); diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index ae588d62..4910dfb8 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -1669,6 +1669,7 @@ cursor: minimax: minimax_china: kimi: +kimi_cn: opencode_go: opencode_zen: xiaomi: diff --git a/tests/providers.cache-policies.test.ts b/tests/providers.cache-policies.test.ts index 34eeb777..79d96d8d 100644 --- a/tests/providers.cache-policies.test.ts +++ b/tests/providers.cache-policies.test.ts @@ -1,10 +1,48 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const cachePolicyMocks = vi.hoisted(() => ({ + resolveGlobal: vi.fn(), + resolveCn: vi.fn(), + deriveIdentity: vi.fn(async (params: unknown) => JSON.stringify(params)), +})); + +vi.mock("../src/lib/kimi-auth.js", () => ({ + DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS: 5_000, + resolveKimiGlobalAuthCached: cachePolicyMocks.resolveGlobal, + resolveKimiCnAuthCached: cachePolicyMocks.resolveCn, + resolveKimiGlobalAuthWithDiagnosticsCached: vi.fn(), + resolveKimiCnAuthWithDiagnosticsCached: vi.fn(), + resolveKimiGlobalAuth: vi.fn(), + resolveKimiCnAuth: vi.fn(), +})); + +vi.mock("../src/lib/resolved-auth-identity.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + deriveResolvedAuthIdentity: cachePolicyMocks.deriveIdentity, + }; +}); import { QUOTA_PROVIDER_REGISTRATION_SOURCE } from "../src/lib/provider-registration.js"; import { PROVIDER_CACHE_POLICIES } from "../src/providers/cache-policies.js"; import { getProviders } from "../src/providers/registry.js"; describe("provider cache policies", () => { + beforeEach(() => { + vi.clearAllMocks(); + cachePolicyMocks.resolveGlobal.mockResolvedValue({ + state: "configured", + apiKey: "shared-kimi-secret", + endpoint: "global", + }); + cachePolicyMocks.resolveCn.mockResolvedValue({ + state: "configured", + apiKey: "shared-kimi-secret", + endpoint: "cn", + }); + }); + it("classifies every canonical provider without an account-neutral fallback", () => { const registeredIds = QUOTA_PROVIDER_REGISTRATION_SOURCE.map(({ id }) => id).sort(); expect(Object.keys(PROVIDER_CACHE_POLICIES).sort()).toEqual(registeredIds); @@ -18,6 +56,10 @@ describe("provider cache policies", () => { .sort(); expect(uncached).toEqual(["alibaba-token-plan", "cursor", "qwen-code"]); + expect(PROVIDER_CACHE_POLICIES["kimi-code-plan-global"].kind).toBe("resolved-auth"); + expect(PROVIDER_CACHE_POLICIES["kimi-code-plan-cn"].kind).toBe("resolved-auth"); + expect(PROVIDER_CACHE_POLICIES).not.toHaveProperty("kimi-for-coding"); + for (const id of [ "anthropic", "copilot", @@ -30,6 +72,45 @@ describe("provider cache policies", () => { } }); + it("keeps Kimi identities isolated through the actual regional policy resolvers", async () => { + const globalPolicy = PROVIDER_CACHE_POLICIES["kimi-code-plan-global"]; + const cnPolicy = PROVIDER_CACHE_POLICIES["kimi-code-plan-cn"]; + if (globalPolicy.kind !== "resolved-auth" || cnPolicy.kind !== "resolved-auth") { + throw new Error("Expected resolved-auth Kimi cache policies"); + } + + const ctx = { config: {} } as never; + const cacheContext = {} as never; + const globalFirst = await globalPolicy.resolveIdentity(ctx, cacheContext); + const cnFirst = await cnPolicy.resolveIdentity(ctx, cacheContext); + + expect(globalFirst).not.toBe(cnFirst); + expect(cachePolicyMocks.deriveIdentity).toHaveBeenCalledWith({ + providerId: "kimi-code-plan-global", + principal: { kind: "credential", value: "shared-kimi-secret" }, + qualifiers: ["global"], + }); + expect(cachePolicyMocks.deriveIdentity).toHaveBeenCalledWith({ + providerId: "kimi-code-plan-cn", + principal: { kind: "credential", value: "shared-kimi-secret" }, + qualifiers: ["cn"], + }); + + cachePolicyMocks.resolveCn.mockResolvedValue({ + state: "configured", + apiKey: "changed-cn-secret", + endpoint: "cn", + }); + + const globalSecond = await globalPolicy.resolveIdentity(ctx, cacheContext); + const cnSecond = await cnPolicy.resolveIdentity(ctx, cacheContext); + + expect(globalSecond).toBe(globalFirst); + expect(cnSecond).not.toBe(cnFirst); + expect(cachePolicyMocks.resolveGlobal).toHaveBeenCalledTimes(2); + expect(cachePolicyMocks.resolveCn).toHaveBeenCalledTimes(2); + }); + it("attaches the exhaustive policy to the stable provider singleton", () => { const first = getProviders(); const second = getProviders(); diff --git a/tests/providers.kimi-code.test.ts b/tests/providers.kimi-code.test.ts index af08bbd3..e6a56bcb 100644 --- a/tests/providers.kimi-code.test.ts +++ b/tests/providers.kimi-code.test.ts @@ -8,209 +8,331 @@ import { import { createProviderAvailabilityContext } from "./helpers/provider-test-harness.js"; const authMocks = vi.hoisted(() => ({ - resolveKimiAuth: vi.fn(), - resolveKimiAuthCached: vi.fn(), + resolveGlobal: vi.fn(), + resolveCn: vi.fn(), + resolveGlobalWithDiagnostics: vi.fn(), + resolveCnWithDiagnostics: vi.fn(), + parseGlobal: vi.fn(), + parseCn: vi.fn(), })); vi.mock("../src/lib/kimi-auth.js", () => ({ - resolveKimiAuthCached: authMocks.resolveKimiAuthCached, - resolveKimiAuth: authMocks.resolveKimiAuth, - getKimiAuthDiagnostics: vi.fn(async () => ({ - state: "none", - source: null, - checkedPaths: [], - credentialDatabasePaths: [], - })), + resolveKimiGlobalAuthCached: authMocks.resolveGlobal, + resolveKimiCnAuthCached: authMocks.resolveCn, + resolveKimiGlobalAuthWithDiagnosticsCached: authMocks.resolveGlobalWithDiagnostics, + resolveKimiCnAuthWithDiagnosticsCached: authMocks.resolveCnWithDiagnostics, + resolveKimiGlobalAuth: authMocks.parseGlobal, + resolveKimiCnAuth: authMocks.parseCn, DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS: 5_000, })); -vi.mock("../src/lib/kimi.js", () => ({ - queryKimiQuota: vi.fn(), -})); - +vi.mock("../src/lib/kimi.js", () => ({ queryKimiQuota: vi.fn() })); vi.mock("../src/lib/provider-availability.js", () => ({ isCanonicalProviderAvailable: vi.fn(), })); - vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ ...(await importOriginal()), readCredentialRows: vi.fn().mockResolvedValue([]), })); -import { kimiCodeProvider } from "../src/providers/kimi-code.js"; +import { queryKimiQuota } from "../src/lib/kimi.js"; +import { readCredentialRows } from "../src/lib/opencode-auth.js"; +import { isCanonicalProviderAvailable } from "../src/lib/provider-availability.js"; +import { kimiCodePlanCnProvider, kimiCodePlanGlobalProvider } from "../src/providers/kimi-code.js"; + +const successfulWindows = [ + { + label: "Weekly limit", + used: 250, + limit: 1000, + percentRemaining: 75, + resetTimeIso: "2026-01-08T00:00:00.000Z", + }, + { + label: "5h limit", + used: 100, + limit: 500, + percentRemaining: 80, + resetTimeIso: "2026-01-01T05:00:00.000Z", + }, +]; -describe("kimi-code provider", () => { +describe("Kimi regional providers", () => { beforeEach(() => { vi.clearAllMocks(); - authMocks.resolveKimiAuthCached.mockResolvedValue({ + authMocks.resolveGlobal.mockResolvedValue({ state: "configured", - apiKey: "test-key", + apiKey: "global-key", + endpoint: "global", + }); + authMocks.resolveCn.mockResolvedValue({ + state: "configured", + apiKey: "cn-key", + endpoint: "cn", + }); + authMocks.resolveGlobalWithDiagnostics.mockResolvedValue({ + auth: { state: "configured", apiKey: "global-key", endpoint: "global" }, + diagnostics: { + state: "configured", + source: "env:KIMI_GLOBAL_API_KEY", + endpoint: "global", + checkedPaths: ["env:KIMI_GLOBAL_API_KEY"], + credentialDatabasePaths: [], + }, }); - authMocks.resolveKimiAuth.mockReturnValue({ state: "configured", apiKey: "row-key" }); + authMocks.resolveCnWithDiagnostics.mockResolvedValue({ + auth: { state: "configured", apiKey: "cn-key", endpoint: "cn" }, + diagnostics: { + state: "configured", + source: "env:KIMI_CN_API_KEY", + endpoint: "cn", + checkedPaths: ["env:KIMI_CN_API_KEY"], + credentialDatabasePaths: [], + }, + }); + vi.mocked(isCanonicalProviderAvailable).mockResolvedValue(true); }); - it("returns attempted:false when no kimi auth is configured", async () => { - authMocks.resolveKimiAuthCached.mockResolvedValueOnce({ state: "none" }); + it.each([ + [kimiCodePlanGlobalProvider, "global", "global-key", "Kimi Code"], + [kimiCodePlanCnProvider, "cn", "cn-key", "Kimi Code (CN)"], + ] as const)("binds $id fetches to the resolved regional credential", async (provider, endpoint, apiKey, label) => { + vi.mocked(queryKimiQuota).mockResolvedValue({ + success: true, + label, + windows: successfulWindows, + }); - const out = await kimiCodeProvider.fetch({ config: {} } as any); - expectNotAttempted(out); - }); + const out = await provider.fetch({ config: { requestTimeoutMs: 1234 } } as any); - it("returns error when kimi auth is invalid", async () => { - authMocks.resolveKimiAuthCached.mockResolvedValueOnce({ - state: "invalid", - error: "Invalid API key", + expectAttemptedWithNoErrors(out); + expect(queryKimiQuota).toHaveBeenCalledWith({ + apiKey, + endpoint, + label, + requestTimeoutMs: 1234, }); - - const out = await kimiCodeProvider.fetch({ config: {} } as any); - expectAttemptedWithErrorLabel(out, "Kimi Code"); - expect(out.errors[0]?.message).toBe("Invalid API key"); + const combinedResolver = + endpoint === "global" + ? authMocks.resolveGlobalWithDiagnostics + : authMocks.resolveCnWithDiagnostics; + expect(combinedResolver).toHaveBeenCalledTimes(1); + expect(authMocks.resolveGlobal).not.toHaveBeenCalled(); + expect(authMocks.resolveCn).not.toHaveBeenCalled(); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "api_endpoint", value: endpoint }, + { + key: "api_base_url", + value: + endpoint === "global" + ? "https://api.kimi.ai/coding/v1" + : "https://api.kimi.com/coding/v1", + }, + ]), + ); }); - it("maps success into canonical grouped-capable entries for all windows", async () => { - const { queryKimiQuota } = await import("../src/lib/kimi.js"); - (queryKimiQuota as any).mockResolvedValueOnce({ + it("maps CN success into the existing accounting and presentation shape", async () => { + vi.mocked(queryKimiQuota).mockResolvedValue({ success: true, - label: "Kimi Code", - windows: [ - { - label: "Weekly limit", - used: 250, - limit: 1000, - percentRemaining: 75, - resetTimeIso: "2026-01-08T00:00:00.000Z", - }, - { - label: "5h limit", - used: 100, - limit: 500, - percentRemaining: 80, - resetTimeIso: "2026-01-01T05:00:00.000Z", - }, - ], + label: "Kimi Code (CN)", + windows: successfulWindows, }); - const out = await kimiCodeProvider.fetch({ config: {} } as any); - expectAttemptedWithNoErrors(out); - expect(visibleEntries(out.entries, "kimi-for-coding")).toEqual([ + const out = await kimiCodePlanCnProvider.fetch({ config: {} } as any); + + expect(visibleEntries(out.entries, "kimi-code-plan-cn")).toEqual([ { - name: "Kimi Code Weekly limit", - group: "Kimi Code", + name: "Kimi Code (CN) Weekly limit", + group: "Kimi Code (CN)", label: "Weekly limit:", right: "250/1000", percentRemaining: 75, resetTimeIso: "2026-01-08T00:00:00.000Z", }, { - name: "Kimi Code 5h limit", - group: "Kimi Code", + name: "Kimi Code (CN) 5h limit", + group: "Kimi Code (CN)", label: "5h limit:", right: "100/500", percentRemaining: 80, resetTimeIso: "2026-01-01T05:00:00.000Z", }, ]); - expect(out.presentation).toEqual({ - singleWindowDisplayName: "Kimi Code", + expect(out.entries[0]?.accounting).toEqual({ + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", }); + expect(out.presentation).toEqual({ singleWindowDisplayName: "Kimi Code (CN)" }); }); - it("keeps a valid inactive duplicate-label database credential when the active row is invalid", async () => { - const { getKimiAuthDiagnostics } = await import("../src/lib/kimi-auth.js"); - const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); - const { queryKimiQuota } = await import("../src/lib/kimi.js"); - (getKimiAuthDiagnostics as any).mockResolvedValueOnce({ - state: "invalid", - source: "opencode.db", - checkedPaths: [], - credentialDatabasePaths: [], - }); - (readCredentialRows as any).mockResolvedValueOnce([ - { id: "bad", integrationId: "kimi", label: "shared", active: true, value: { key: "" } }, - { id: "good", integrationId: "kimi", label: "shared", active: false, value: { key: "ok" } }, - ]); - authMocks.resolveKimiAuth.mockImplementation((auth: any) => - auth.kimi.key - ? { state: "configured", apiKey: "row-key" } - : { state: "invalid", error: "empty key" }, - ); - (queryKimiQuota as any).mockResolvedValueOnce({ - success: true, - label: "Kimi Code", - windows: [{ label: "5h", used: 1, limit: 10, percentRemaining: 90 }], - }); - - const out = await kimiCodeProvider.fetch({ config: {} } as any); - expect(out.errors).toContainEqual({ label: "[Kimi Code shared]*", message: "empty key" }); - expect(out.entries).toContainEqual( - expect.objectContaining({ - group: "[Kimi Code shared 2]", - accounting: expect.objectContaining({ sourceId: "good" }), - }), - ); - expect(queryKimiQuota).toHaveBeenCalledOnce(); + it.each([ + ["k3"], + ["k3-256k"], + ["kimi-for-coding"], + ["kimi-for-coding-highspeed"], + ])("matches the known model %s for each regional canonical id", (modelId) => { + expect( + kimiCodePlanGlobalProvider.matchesCurrentModel?.(`kimi-code-plan-global/${modelId}`), + ).toBe(true); + expect(kimiCodePlanCnProvider.matchesCurrentModel?.(`kimi-code-plan-cn/${modelId}`)).toBe(true); }); - it("maps errors into toast errors", async () => { - const { queryKimiQuota } = await import("../src/lib/kimi.js"); - (queryKimiQuota as any).mockResolvedValueOnce({ - success: false, - error: "Unauthorized", - }); + it("keeps legacy provider ids owned by CN only", () => { + for (const providerId of ["kimi-for-coding", "kimi-code", "kimi", "kimi-for-code"]) { + expect(kimiCodePlanCnProvider.matchesCurrentModel?.(`${providerId}/k3`)).toBe(true); + expect(kimiCodePlanGlobalProvider.matchesCurrentModel?.(`${providerId}/k3`)).toBe(false); + } + }); - const out = await kimiCodeProvider.fetch({} as any); - expectAttemptedWithErrorLabel(out, "Kimi Code"); + it("preserves provider-prefix matching for models outside the known Kimi list", () => { + expect(kimiCodePlanGlobalProvider.matchesCurrentModel?.("kimi-code-plan-global/kimi-k2")).toBe( + true, + ); + expect(kimiCodePlanCnProvider.matchesCurrentModel?.("kimi-code-plan-cn/future-model")).toBe( + true, + ); }); - it("matches kimi model ids", () => { - expect(kimiCodeProvider.matchesCurrentModel?.("kimi-code/kimi-k2")).toBe(true); - expect(kimiCodeProvider.matchesCurrentModel?.("kimi/kimi-k2")).toBe(true); - expect(kimiCodeProvider.matchesCurrentModel?.("openai/gpt-5")).toBe(false); + it("rejects unqualified models without a provider prefix", () => { + expect(kimiCodePlanGlobalProvider.matchesCurrentModel?.("kimi-k2")).toBe(false); + expect(kimiCodePlanCnProvider.matchesCurrentModel?.("future-model")).toBe(false); }); - it("is available when provider ids include kimi and auth is configured", async () => { - const { isCanonicalProviderAvailable } = await import("../src/lib/provider-availability.js"); - (isCanonicalProviderAvailable as any).mockResolvedValue(true); + it("does not resolve auth when the regional runtime provider is unavailable", async () => { + vi.mocked(isCanonicalProviderAvailable).mockResolvedValue(false); - const available = await kimiCodeProvider.isAvailable( - createProviderAvailabilityContext({ providerIds: ["kimi-for-coding"] }), - ); - expect(available).toBe(true); + await expect( + kimiCodePlanGlobalProvider.isAvailable( + createProviderAvailabilityContext({ providerIds: ["kimi-code-plan-cn"] }), + ), + ).resolves.toBe(false); + expect(authMocks.resolveGlobal).not.toHaveBeenCalled(); }); - it("is available when auth is invalid so the provider can surface the error", async () => { - const { isCanonicalProviderAvailable } = await import("../src/lib/provider-availability.js"); - (isCanonicalProviderAvailable as any).mockResolvedValue(true); - authMocks.resolveKimiAuthCached.mockResolvedValueOnce({ + it("keeps availability and auth state independent by region", async () => { + authMocks.resolveGlobal.mockResolvedValue({ state: "none" }); + authMocks.resolveCn.mockResolvedValue({ state: "invalid", error: 'Unsupported Kimi auth type: "oauth"', }); - const available = await kimiCodeProvider.isAvailable( - createProviderAvailabilityContext({ providerIds: ["kimi-for-coding"] }), - ); - expect(available).toBe(true); + await expect( + kimiCodePlanGlobalProvider.isAvailable( + createProviderAvailabilityContext({ providerIds: ["kimi-code-plan-global"] }), + ), + ).resolves.toBe(false); + await expect( + kimiCodePlanCnProvider.isAvailable( + createProviderAvailabilityContext({ providerIds: ["kimi-code-plan-cn"] }), + ), + ).resolves.toBe(true); }); - it("is not available when provider ids exist but auth is missing", async () => { - const { isCanonicalProviderAvailable } = await import("../src/lib/provider-availability.js"); - (isCanonicalProviderAvailable as any).mockResolvedValue(true); - authMocks.resolveKimiAuthCached.mockResolvedValueOnce({ state: "none" }); + it("returns not attempted for absent auth and preserves invalid-auth provenance", async () => { + authMocks.resolveGlobalWithDiagnostics.mockResolvedValueOnce({ + auth: { state: "none" }, + diagnostics: { + state: "none", + source: null, + checkedPaths: ["/trusted/opencode.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], + }, + }); + expectNotAttempted(await kimiCodePlanGlobalProvider.fetch({ config: {} } as any)); - const available = await kimiCodeProvider.isAvailable( - createProviderAvailabilityContext({ providerIds: ["kimi-for-coding"] }), + authMocks.resolveCnWithDiagnostics.mockResolvedValueOnce({ + auth: { state: "invalid", error: "Invalid API key" }, + diagnostics: { + state: "invalid", + source: "opencode.db", + checkedPaths: ["/trusted/opencode.json"], + credentialDatabasePaths: ["/trusted/opencode.db"], + error: "Invalid API key", + }, + }); + const invalid = await kimiCodePlanCnProvider.fetch({ config: {} } as any); + expectAttemptedWithErrorLabel(invalid, "Kimi Code (CN)"); + expect(invalid.errors[0]?.message).toBe("Invalid API key"); + expect(invalid.statusDetails).toEqual( + expect.arrayContaining([ + { key: "auth_state", value: "invalid" }, + { key: "api_key_source", value: "opencode.db" }, + { key: "auth_error", value: "Invalid API key" }, + { key: "api_endpoint", value: "cn" }, + ]), ); - expect(available).toBe(false); + expect(queryKimiQuota).not.toHaveBeenCalled(); + }); + + it("maps request errors to the same regional label", async () => { + vi.mocked(queryKimiQuota).mockResolvedValue({ success: false, error: "Unauthorized" }); + + const out = await kimiCodePlanGlobalProvider.fetch({ config: {} } as any); + expectAttemptedWithErrorLabel(out, "Kimi Code"); + expect(out.statusDetails).toContainEqual({ key: "live_fetch_error", value: "Unauthorized" }); }); - it("is not available when provider lookup throws", async () => { - const { isCanonicalProviderAvailable } = await import("../src/lib/provider-availability.js"); - (isCanonicalProviderAvailable as any).mockRestore(); + it("keeps valid inactive CN credentials and errors alongside invalid rows, without querying Global", async () => { + authMocks.resolveCnWithDiagnostics.mockResolvedValueOnce({ + auth: { state: "invalid", error: "empty key" }, + diagnostics: { + state: "invalid", + source: "opencode.db", + error: "empty key", + checkedPaths: [], + credentialDatabasePaths: [], + }, + }); + vi.mocked(readCredentialRows).mockResolvedValueOnce([ + { + id: "bad", + integrationId: "kimi", + label: "shared", + active: true, + value: { type: "api", key: "" }, + }, + { + id: "good", + integrationId: "kimi-code-plan-cn", + label: "shared", + active: false, + value: { type: "api", key: "cn-key" }, + }, + { + id: "global", + integrationId: "kimi-code-plan-global", + label: "other", + active: false, + value: { type: "api", key: "global-key" }, + }, + ] as never); + authMocks.parseCn.mockImplementation((auth: Record) => { + const key = Object.values(auth)[0]?.key; + return key + ? { state: "configured", apiKey: key, endpoint: "cn" } + : { state: "invalid", error: "empty key" }; + }); + vi.mocked(queryKimiQuota).mockResolvedValueOnce({ + success: true, + label: "Kimi Code (CN)", + windows: successfulWindows, + }); - const ctx = createProviderAvailabilityContext({ providersError: new Error("boom") }); + const out = await kimiCodePlanCnProvider.fetch({ config: {} } as any); - const available = await kimiCodeProvider.isAvailable(ctx); - expect(available).toBe(false); - expect(authMocks.resolveKimiAuthCached).not.toHaveBeenCalled(); + expect(out.attempted).toBe(true); + expect(out.errors).toEqual([ + { label: expect.stringContaining("shared"), message: "empty key" }, + ]); + expect(out.entries).toHaveLength(2); + expect(out.entries[0]?.accounting.sourceId).toBe("good"); + expect(queryKimiQuota).toHaveBeenCalledOnce(); + expect(queryKimiQuota).toHaveBeenCalledWith( + expect.objectContaining({ apiKey: "cn-key", endpoint: "cn" }), + ); }); }); diff --git a/tests/providers.registry.test.ts b/tests/providers.registry.test.ts index c6b1e169..8bc5257f 100644 --- a/tests/providers.registry.test.ts +++ b/tests/providers.registry.test.ts @@ -24,7 +24,8 @@ const EXPECTED_PROVIDER_ORDER = [ "nanogpt", "minimax-coding-plan", "minimax-china-coding-plan", - "kimi-for-coding", + "kimi-code-plan-global", + "kimi-code-plan-cn", "deepseek", "xai", "xiaomi", From dd2afef4ec794066b3b9ab7d81f7159284022f48 Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Thu, 24 Sep 2026 09:26:54 +0200 Subject: [PATCH 22/97] refactor(plugin): migrate entrypoints to V2 Move server and CLI integrations to OpenCode V2 APIs, update the dependency and adapt parity tests to the new contracts. Preserve Cursor compatibility and quota diagnostics. --- package.json | 8 +- pnpm-lock.yaml | 2366 ++++++++++++++++- scripts/prepare-tui-dist.mjs | 18 +- scripts/smoke-packed-package.mjs | 7 +- scripts/verify-typescript-version.mjs | 15 +- src/index.ts | 6 +- src/plugin.ts | 483 +--- src/tui-v2.tsx | 216 +- tests/index.test.ts | 4 +- tests/lib.alibaba-token-plan.test.ts | 5 +- tests/package-manifest.test.ts | 8 +- tests/plugin-v2.test.ts | 49 + tests/plugin.command-handled-boundary.test.ts | 617 +---- tests/plugin.maintainer-announcements.test.ts | 121 +- tests/plugin.quota-command.test.ts | 465 ++-- tests/plugin.quota-status-command.test.ts | 8 +- tests/plugin.qwen-hook.test.ts | 101 +- tests/plugin.tokens-session-all.test.ts | 41 +- ...ovider-surfaces.google-antigravity.test.ts | 146 +- tests/provider-surfaces.opencode-go.test.ts | 201 +- tests/providers.alibaba-token-plan.test.ts | 14 + tests/quota-surface-parity.test.ts | 15 - tests/release-gates.test.ts | 2 +- tests/tui-dist-packaging.test.ts | 27 +- tests/tui-v2-commands.test.ts | 6 + tests/v4-migration-docs.test.ts | 2 +- tests/v4-phase5-cross-surface.test.ts | 266 +- tui.ts | 2 +- 28 files changed, 3340 insertions(+), 1879 deletions(-) create mode 100644 tests/plugin-v2.test.ts diff --git a/package.json b/package.json index 666dd6bd..2a49f2b7 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,6 @@ "default": "./dist/tui.js" } }, - "oc-plugin": [ - "server", - "tui" - ], "files": [ "dist", "README.md", @@ -90,7 +86,7 @@ "@babel/core": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@biomejs/biome": "2.3.11", - "@opencode/plugin": "2.0.2", + "@opencode/plugin": "2.0.15", "@opentelemetry/sdk-metrics": "2.10.0", "@types/node": "^22.0.0", "babel-preset-solid": "^1.9.12", @@ -113,7 +109,7 @@ "better-sqlite3": "^12.10.0" }, "peerDependencies": { - "@opencode/plugin": "2.0.2" + "@opencode/plugin": "2.0.15" }, "engines": { "node": ">=22.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cab8efc..5e7a4c1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,9 +42,9 @@ importers: '@biomejs/biome': specifier: 2.3.11 version: 2.3.11 - '@opencode-ai/plugin': - specifier: 1.18.1 - version: 1.18.1(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10)) + '@opencode/plugin': + specifier: 2.0.15 + version: 2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))(redis@6.2.1(@opentelemetry/api@1.9.1))(solid-js@1.9.12) '@opentelemetry/sdk-metrics': specifier: 2.10.0 version: 2.10.0(@opentelemetry/api@1.9.1) @@ -81,6 +81,99 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-cognito-identity@3.1057.0': + resolution: {integrity: sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.978.1': + resolution: {integrity: sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-cognito-identity@3.972.71': + resolution: {integrity: sha512-rsxsi8tF3vMGdWdaHQNXceHBdb49442yR2Pe08K+Phoc/yICbptdsHwL25Wf0z5+hjhSS5a+hDJHP7Xul4zvHQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.72': + resolution: {integrity: sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.74': + resolution: {integrity: sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.17': + resolution: {integrity: sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.79': + resolution: {integrity: sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.84': + resolution: {integrity: sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.72': + resolution: {integrity: sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.16': + resolution: {integrity: sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.78': + resolution: {integrity: sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-providers@3.1057.0': + resolution: {integrity: sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.46': + resolution: {integrity: sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.47': + resolution: {integrity: sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1138.0': + resolution: {integrity: sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.6': + resolution: {integrity: sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.10': + resolution: {integrity: sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.41': + resolution: {integrity: sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -277,6 +370,51 @@ packages: '@clack/prompts@1.2.0': resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} + '@effect/opentelemetry@4.0.0-rc.112': + resolution: {integrity: sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <2.0.0' + '@opentelemetry/api-logs': '>=0.203.0 <0.300.0' + '@opentelemetry/resources': '>=2.0.0 <3.0.0' + '@opentelemetry/sdk-logs': '>=0.203.0 <0.300.0' + '@opentelemetry/sdk-metrics': '>=2.0.0 <3.0.0' + '@opentelemetry/sdk-trace-base': '>=2.0.0 <3.0.0' + '@opentelemetry/sdk-trace-node': '>=2.0.0 <3.0.0' + '@opentelemetry/sdk-trace-web': '>=2.0.0 <3.0.0' + '@opentelemetry/semantic-conventions': '>=1.33.0 <2.0.0' + effect: ^4.0.0-rc.112 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/api-logs': + optional: true + '@opentelemetry/resources': + optional: true + '@opentelemetry/sdk-logs': + optional: true + '@opentelemetry/sdk-metrics': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/sdk-trace-node': + optional: true + '@opentelemetry/sdk-trace-web': + optional: true + + '@effect/platform-node-shared@4.0.0-rc.112': + resolution: {integrity: sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0-rc.112 + + '@effect/platform-node@4.0.0-rc.112': + resolution: {integrity: sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0-rc.112 + redis: '>=5.0.0 <7.0.0' + '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} @@ -433,6 +571,21 @@ packages: cpu: [x64] os: [win32] + '@gar/promise-retry@1.0.3': + resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@isaacs/string-locale-compare@1.1.0': + resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -476,45 +629,198 @@ packages: cpu: [x64] os: [win32] - '@opencode-ai/plugin@1.18.1': - resolution: {integrity: sha512-qE2Jg4Jh7NU18Wl69zoCz7fOvHQxKmUXkSq4akAqCixYDrGO7heYG/wfvafVSsx0n6jWhCdIN8cToNU6G4lKfw==} + '@npmcli/agent@4.0.2': + resolution: {integrity: sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/arborist@9.4.0': + resolution: {integrity: sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + '@npmcli/config@10.8.1': + resolution: {integrity: sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/git@7.0.2': + resolution: {integrity: sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/installed-package-contents@4.0.0': + resolution: {integrity: sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + '@npmcli/map-workspaces@5.0.3': + resolution: {integrity: sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/metavuln-calculator@9.0.3': + resolution: {integrity: sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/name-from-folder@4.0.0': + resolution: {integrity: sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/node-gyp@5.0.0': + resolution: {integrity: sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/package-json@7.0.5': + resolution: {integrity: sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/promise-spawn@9.0.1': + resolution: {integrity: sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/query@5.0.0': + resolution: {integrity: sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/redact@4.0.0': + resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/run-script@10.0.4': + resolution: {integrity: sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@opencode/ai@2.0.15': + resolution: {integrity: sha512-dY3HG6bp+UMJOi+m1nfzvQaTPpCSmMNDswAOLQq/MJghGrD54WrcEC4IhAQ1hJY7b/hhQYOzPAMeJsvF4aRYYQ==} + + '@opencode/client@2.0.15': + resolution: {integrity: sha512-Lu3Pb6Z/lJ797VryQrsdqEgVBXjtYZWOxLNZKyBUDgXhO/GpzOpXi1H1Uo4g+qWrH8LwUISDBdxHHOWJy5HcqQ==} peerDependencies: - '@opentui/core': '>=0.4.3' - '@opentui/keymap': '>=0.4.3' - '@opentui/solid': '>=0.4.3' + effect: 4.0.0-rc.112 + solid-js: '>=1.9.0' peerDependenciesMeta: - '@opentui/core': + effect: + optional: true + solid-js: + optional: true + + '@opencode/plugin@2.0.15': + resolution: {integrity: sha512-TsSXd2g/433De7ViY+5d/tlLtkZCSmYKo8ScnhtcmPVoM/U5ptQDFXEXx5psTlAfkKcISJh+5vMSOe4u3uCqUQ==} + peerDependencies: + '@opencode/theme': 2.0.15 + '@opentui/core': '>=0.5.10' + '@opentui/solid': '>=0.5.10' + solid-js: '>=1.9.0' + peerDependenciesMeta: + '@opencode/theme': optional: true - '@opentui/keymap': + '@opentui/core': optional: true '@opentui/solid': optional: true + solid-js: + optional: true + + '@opencode/protocol@2.0.15': + resolution: {integrity: sha512-fFi3VYdgNQzT4TswfKGul7DzseOyhA4b/hE/elUe3xxm5iki4tP/pjT8CU3rRWX8sK/75ICr36NAUhSayFtJuw==} + + '@opencode/schema@2.0.15': + resolution: {integrity: sha512-D0JDwF4uTNmFA2MyiSnZ5DlT/t2PF1EEwVbRPulB/Ppcss/z5Fcn6VybHrapki2bKDVQu1Mnn8zBW3G6KP5I6g==} + + '@opencode/util@2.0.15': + resolution: {integrity: sha512-s0Z+2rxb+2Tif+ZV1EvTiGQ0tRiemIZq3WOOjBw2XPBSBt2Sfv9rWXJhTzSpkOZucfTyymuIVXslXPGFF7aaQA==} + + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + engines: {node: '>=8.0.0'} - '@opencode-ai/sdk@1.18.1': - resolution: {integrity: sha512-2mX9SXSDVtZ9i2yCOiXDXKx981EJp/ObUtaThUNn16jkjfwX/1j8r+UR/BKUmBLYq+zle7TtpXreXuO+gtVjKA==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@opentelemetry/context-async-hooks@2.6.1': + resolution: {integrity: sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.10.0': resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.6.1': + resolution: {integrity: sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-http@0.214.0': + resolution: {integrity: sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.214.0': + resolution: {integrity: sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.214.0': + resolution: {integrity: sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/resources@2.10.0': resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.6.1': + resolution: {integrity: sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.214.0': + resolution: {integrity: sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.10.0': resolution: {integrity: sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.6.1': + resolution: {integrity: sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.6.1': + resolution: {integrity: sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.6.1': + resolution: {integrity: sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.43.0': resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} @@ -571,6 +877,73 @@ packages: peerDependencies: solid-js: 1.9.12 + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@redis/bloom@6.2.1': + resolution: {integrity: sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.2.1 + + '@redis/client@6.2.1': + resolution: {integrity: sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@node-rs/xxhash': ^1.1.0 + '@opentelemetry/api': '>=1 <2' + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + '@opentelemetry/api': + optional: true + + '@redis/json@6.2.1': + resolution: {integrity: sha512-AFIUJ8Gj0DaaSBHYuSt8+O0oYWM+50OK1c0OmodB7XERIA8+BbyV3O4v76f9iccWasd1/7qjfZTpuzexUaZtrQ==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.2.1 + + '@redis/search@6.2.1': + resolution: {integrity: sha512-2vfOAOyYFE7UUw3sBBlkqqruBtOUS4HRY5MtW4hp83llrwvtrTE4r22CEqXddlV+54zkLxBE4nmsIJ/dpezQrQ==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.2.1 + + '@redis/time-series@6.2.1': + resolution: {integrity: sha512-kiYniph04dJOole+L359B6C9E+jYS2uDP7hca6Onj0xF38ZIpyxARO0Iq0W4ZRn1e8Q6vqW00QFZVSMRA/2Ijw==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.2.1 + '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -709,9 +1082,93 @@ packages: cpu: [x64] os: [win32] + '@sigstore/bundle@4.0.0': + resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/core@3.2.1': + resolution: {integrity: sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/protobuf-specs@0.5.2': + resolution: {integrity: sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@sigstore/sign@4.1.1': + resolution: {integrity: sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/tuf@4.0.2': + resolution: {integrity: sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/verify@3.1.1': + resolution: {integrity: sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@smithy/core@3.35.0': + resolution: {integrity: sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.14': + resolution: {integrity: sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.8.0': + resolution: {integrity: sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.12.1': + resolution: {integrity: sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.19.0': + resolution: {integrity: sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.5.2': + resolution: {integrity: sha512-nxu3SgmAw9JXT2CtkU0m/XNLWpP9MsaBx1zAGAypCbYj15tIFlmcYwpF+Oh18le83d+IM9PT7ENdXnE4C+d5mA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.5.2': + resolution: {integrity: sha512-iq+cW3mAb7vfcxEEpYi3zXKpDtbrIFyanWjQl4zBq4seWD4OSxXDWSfespZxenX6aEaighn+NR3u1nU1DSvs3w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tufjs/canonical-json@2.0.0': + resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@tufjs/models@4.1.0': + resolution: {integrity: sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==} + engines: {node: ^20.17.0 || >=22.9.0} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -724,6 +1181,9 @@ packages: '@types/node@22.19.6': resolution: {integrity: sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -873,10 +1333,30 @@ packages: '@vitest/utils@4.0.17': resolution: {integrity: sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==} + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -884,6 +1364,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + babel-plugin-jsx-dom-expressions@0.40.6: resolution: {integrity: sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==} peerDependencies: @@ -904,6 +1387,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -916,20 +1403,37 @@ packages: resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bin-links@6.0.2: + resolution: {integrity: sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w==} + engines: {node: ^20.17.0 || >=22.9.0} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + browserslist@4.28.7: resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -938,6 +1442,10 @@ packages: peerDependencies: typescript: ^5 + cacache@20.0.4: + resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==} + engines: {node: ^20.17.0 || >=22.9.0} + caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} @@ -948,10 +1456,41 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + cmd-shim@8.0.0: + resolution: {integrity: sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==} + engines: {node: ^20.17.0 || >=22.9.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comment-json@5.0.0: resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} engines: {node: '>= 6'} + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} + engines: {node: '>= 18'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -959,9 +1498,18 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -987,8 +1535,14 @@ packages: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} - effect@4.0.0-beta.83: - resolution: {integrity: sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + effect@4.0.0-rc.112: + resolution: {integrity: sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A==} electron-to-chromium@1.5.393: resolution: {integrity: sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==} @@ -996,6 +1550,12 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -1007,6 +1567,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -1039,8 +1603,14 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - fast-check@4.8.0: - resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-check@4.10.2: + resolution: {integrity: sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==} engines: {node: '>=12.17.0'} fast-string-truncated-width@1.2.1: @@ -1061,22 +1631,35 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} find-babel-config@2.1.2: resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==} - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1088,6 +1671,18 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@7.1.3: + resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + engines: {node: '>=18'} + + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.4: + resolution: {integrity: sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==} + engines: {node: '>=18'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1099,38 +1694,104 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.5: + resolution: {integrity: sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw==} + engines: {node: 20 || >=22} + glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + google-auth-library@10.5.0: + resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + google-logging-utils@1.2.0: + resolution: {integrity: sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gtoken@8.0.0: + resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} + engines: {node: '>=18'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore-walk@8.0.0: + resolution: {integrity: sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==} + engines: {node: ^20.17.0 || >=22.9.0} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ini@7.0.0: - resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + ip-address@10.7.2: + resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==} + engines: {node: '>= 12'} is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1139,9 +1800,19 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-parse-even-better-errors@5.0.0: + resolution: {integrity: sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==} + engines: {node: ^20.17.0 || >=22.9.0} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stringify-nice@1.1.4: + resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1150,8 +1821,21 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - kubernetes-types@1.30.0: - resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + just-diff-apply@5.5.0: + resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} + + just-diff@6.0.2: + resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} lefthook-darwin-arm64@2.0.15: resolution: {integrity: sha512-ygAqG/NzOgY9bEiqeQtiOmCRTtp9AmOd3eyrpEaSrRB9V9f3RHRgWDrWbde9BiHSsCzcbeY9/X2NuKZ69eUsNA==} @@ -1211,31 +1895,87 @@ packages: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-fetch-happen@15.0.6: + resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==} + engines: {node: ^20.17.0 || >=22.9.0} + marked@17.0.1: resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==} engines: {node: '>= 20'} hasBin: true - mimic-response@3.1.0: + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} + engines: {node: '>=16'} + hasBin: true + + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@8.0.7: resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@5.0.2: + resolution: {integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} @@ -1244,6 +1984,10 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -1254,11 +1998,8 @@ packages: resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@2.0.4: - resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} - - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} + msgpackr@2.1.0: + resolution: {integrity: sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ==} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -1268,18 +2009,69 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + node-abi@3.92.0: resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} engines: {node: '>=10'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true + node-gyp@12.4.0: + resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + npm-bundled@5.0.0: + resolution: {integrity: sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-install-checks@8.0.0: + resolution: {integrity: sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-normalize-package-bin@5.0.0: + resolution: {integrity: sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-package-arg@13.0.2: + resolution: {integrity: sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-packlist@10.0.4: + resolution: {integrity: sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-pick-manifest@11.0.3: + resolution: {integrity: sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-registry-fetch@19.1.1: + resolution: {integrity: sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==} + engines: {node: ^20.17.0 || >=22.9.0} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -1294,10 +2086,26 @@ packages: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} + p-map@7.0.8: + resolution: {integrity: sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==} + engines: {node: '>=18'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pacote@21.5.1: + resolution: {integrity: sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + parse-conflict-json@5.0.1: + resolution: {integrity: sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==} + engines: {node: ^20.17.0 || >=22.9.0} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -1316,6 +2124,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1330,6 +2142,10 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + postcss-selector-parser@7.1.6: + resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} + engines: {node: '>=4'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -1340,6 +2156,24 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + proggy@4.0.0: + resolution: {integrity: sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + promise-all-reject-late@1.0.1: + resolution: {integrity: sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==} + + promise-call-limit@3.0.2: + resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} + + protobufjs@7.6.6: + resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} + engines: {node: '>=12.0.0'} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -1350,10 +2184,18 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + read-cmd-shim@6.0.0: + resolution: {integrity: sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==} + engines: {node: ^20.17.0 || >=22.9.0} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + redis@6.2.1: + resolution: {integrity: sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg==} + engines: {node: '>= 20.0.0'} + reselect@4.1.8: resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} @@ -1362,6 +2204,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1373,6 +2219,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1403,6 +2252,14 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sigstore@4.1.1: + resolution: {integrity: sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==} + engines: {node: ^20.17.0 || >=22.9.0} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1412,6 +2269,18 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.10: + resolution: {integrity: sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + solid-js@1.9.12: resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} @@ -1419,12 +2288,33 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.24: + resolution: {integrity: sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==} + + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -1432,6 +2322,10 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-ansi@7.1.2: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} @@ -1451,6 +2345,10 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1466,9 +2364,16 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} - toml@4.1.1: - resolution: {integrity: sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==} - engines: {node: '>=20'} + treeverse@3.0.0: + resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tuf-js@4.1.0: + resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==} + engines: {node: ^20.17.0 || >=22.9.0} tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -1481,6 +2386,14 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@6.28.1: + resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} + engines: {node: '>=18.17'} + + undici@8.11.0: + resolution: {integrity: sha512-AdzuGcAkzhbha3GgCSoUBx2P0quwym6qdFNCQz/fDGZc8w9XJ741wHUbt2kTmfPz+ZIOMkySFkiY7RXmiDDC8w==} + engines: {node: '>=22.19.0'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1490,9 +2403,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@14.0.1: - resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} - hasBin: true + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} vite@7.3.2: resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} @@ -1568,6 +2481,14 @@ packages: jsdom: optional: true + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + web-tree-sitter@0.25.10: resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==} peerDependencies: @@ -1581,14 +2502,43 @@ packages: engines: {node: '>= 8'} hasBin: true + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@7.0.1: + resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} + engines: {node: ^20.17.0 || >=22.9.0} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -1596,16 +2546,18 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true - zod@4.1.8: resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} @@ -1620,6 +2572,217 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.6 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.6 + '@aws-sdk/util-locate-window': 3.965.10 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.6 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.974.6 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-cognito-identity@3.1057.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.978.1 + '@aws-sdk/credential-provider-node': 3.972.84 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.978.1': + dependencies: + '@aws-sdk/types': 3.974.6 + '@aws-sdk/xml-builder': 3.972.41 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.35.0 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.19.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-cognito-identity@3.972.71': + dependencies: + '@aws-sdk/nested-clients': 3.997.46 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.72': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.74': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.17': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/credential-provider-env': 3.972.72 + '@aws-sdk/credential-provider-http': 3.972.74 + '@aws-sdk/credential-provider-login': 3.972.79 + '@aws-sdk/credential-provider-process': 3.972.72 + '@aws-sdk/credential-provider-sso': 3.973.16 + '@aws-sdk/credential-provider-web-identity': 3.972.78 + '@aws-sdk/nested-clients': 3.997.46 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.79': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/nested-clients': 3.997.46 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.84': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.72 + '@aws-sdk/credential-provider-http': 3.972.74 + '@aws-sdk/credential-provider-ini': 3.973.17 + '@aws-sdk/credential-provider-process': 3.972.72 + '@aws-sdk/credential-provider-sso': 3.973.16 + '@aws-sdk/credential-provider-web-identity': 3.972.78 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.72': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.16': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/nested-clients': 3.997.46 + '@aws-sdk/token-providers': 3.1138.0 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.78': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/nested-clients': 3.997.46 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-providers@3.1057.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.1057.0 + '@aws-sdk/core': 3.978.1 + '@aws-sdk/credential-provider-cognito-identity': 3.972.71 + '@aws-sdk/credential-provider-env': 3.972.72 + '@aws-sdk/credential-provider-http': 3.972.74 + '@aws-sdk/credential-provider-ini': 3.973.17 + '@aws-sdk/credential-provider-login': 3.972.79 + '@aws-sdk/credential-provider-node': 3.972.84 + '@aws-sdk/credential-provider-process': 3.972.72 + '@aws-sdk/credential-provider-sso': 3.973.16 + '@aws-sdk/credential-provider-web-identity': 3.972.78 + '@aws-sdk/nested-clients': 3.997.46 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.46': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/signature-v4-multi-region': 3.996.47 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.47': + dependencies: + '@aws-sdk/types': 3.974.6 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1138.0': + dependencies: + '@aws-sdk/core': 3.978.1 + '@aws-sdk/nested-clients': 3.997.46 + '@aws-sdk/types': 3.974.6 + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.6': + dependencies: + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.10': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.41': + dependencies: + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -1857,6 +3020,39 @@ snapshots: fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 + '@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.214.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112)': + dependencies: + '@opentelemetry/semantic-conventions': 1.43.0 + effect: 4.0.0-rc.112 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.214.0 + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.6.1(@opentelemetry/api@1.9.0) + + '@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-rc.112)': + dependencies: + '@types/ws': 8.18.1 + effect: 4.0.0-rc.112 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform-node@4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112) + effect: 4.0.0-rc.112 + mime: 4.1.0 + redis: 6.2.1(@opentelemetry/api@1.9.1) + undici: 8.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@esbuild/aix-ppc64@0.27.2': optional: true @@ -1935,6 +3131,23 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true + '@gar/promise-retry@1.0.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@isaacs/string-locale-compare@1.1.0': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1967,39 +3180,338 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': optional: true - '@opencode-ai/plugin@1.18.1(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))': + '@npmcli/agent@4.0.2': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.5.3 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/arborist@9.4.0': + dependencies: + '@isaacs/string-locale-compare': 1.1.0 + '@npmcli/fs': 5.0.0 + '@npmcli/installed-package-contents': 4.0.0 + '@npmcli/map-workspaces': 5.0.3 + '@npmcli/metavuln-calculator': 9.0.3 + '@npmcli/name-from-folder': 4.0.0 + '@npmcli/node-gyp': 5.0.0 + '@npmcli/package-json': 7.0.5 + '@npmcli/query': 5.0.0 + '@npmcli/redact': 4.0.0 + '@npmcli/run-script': 10.0.4 + bin-links: 6.0.2 + cacache: 20.0.4 + common-ancestor-path: 2.0.0 + hosted-git-info: 9.0.3 + json-stringify-nice: 1.1.4 + lru-cache: 11.5.3 + minimatch: 10.2.5 + nopt: 9.0.0 + npm-install-checks: 8.0.0 + npm-package-arg: 13.0.2 + npm-pick-manifest: 11.0.3 + npm-registry-fetch: 19.1.1 + pacote: 21.5.1 + parse-conflict-json: 5.0.1 + proc-log: 6.1.0 + proggy: 4.0.0 + promise-all-reject-late: 1.0.1 + promise-call-limit: 3.0.2 + semver: 7.8.4 + ssri: 13.0.1 + treeverse: 3.0.0 + walk-up-path: 4.0.0 + transitivePeerDependencies: + - supports-color + + '@npmcli/config@10.8.1': + dependencies: + '@npmcli/map-workspaces': 5.0.3 + '@npmcli/package-json': 7.0.5 + ci-info: 4.4.0 + ini: 6.0.0 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.8.4 + walk-up-path: 4.0.0 + + '@npmcli/fs@5.0.0': + dependencies: + semver: 7.8.4 + + '@npmcli/git@7.0.2': + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/promise-spawn': 9.0.1 + ini: 6.0.0 + lru-cache: 11.5.3 + npm-pick-manifest: 11.0.3 + proc-log: 6.1.0 + semver: 7.8.4 + which: 6.0.1 + + '@npmcli/installed-package-contents@4.0.0': + dependencies: + npm-bundled: 5.0.0 + npm-normalize-package-bin: 5.0.0 + + '@npmcli/map-workspaces@5.0.3': + dependencies: + '@npmcli/name-from-folder': 4.0.0 + '@npmcli/package-json': 7.0.5 + glob: 13.0.5 + minimatch: 10.2.5 + + '@npmcli/metavuln-calculator@9.0.3': + dependencies: + cacache: 20.0.4 + json-parse-even-better-errors: 5.0.0 + pacote: 21.5.1 + proc-log: 6.1.0 + semver: 7.8.4 + transitivePeerDependencies: + - supports-color + + '@npmcli/name-from-folder@4.0.0': {} + + '@npmcli/node-gyp@5.0.0': {} + + '@npmcli/package-json@7.0.5': + dependencies: + '@npmcli/git': 7.0.2 + glob: 13.0.5 + hosted-git-info: 9.0.3 + json-parse-even-better-errors: 5.0.0 + proc-log: 6.1.0 + semver: 7.8.4 + spdx-expression-parse: 4.0.0 + + '@npmcli/promise-spawn@9.0.1': + dependencies: + which: 6.0.1 + + '@npmcli/query@5.0.0': + dependencies: + postcss-selector-parser: 7.1.6 + + '@npmcli/redact@4.0.0': {} + + '@npmcli/run-script@10.0.4': + dependencies: + '@npmcli/node-gyp': 5.0.0 + '@npmcli/package-json': 7.0.5 + '@npmcli/promise-spawn': 9.0.1 + node-gyp: 12.4.0 + proc-log: 6.1.0 + + '@opencode/ai@2.0.15': + dependencies: + '@aws-sdk/credential-providers': 3.1057.0 + '@opencode/schema': 2.0.15 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/util-utf8': 4.2.2 + aws4fetch: 1.0.20 + effect: 4.0.0-rc.112 + google-auth-library: 10.5.0 + transitivePeerDependencies: + - supports-color + + '@opencode/client@2.0.15(effect@4.0.0-rc.112)(solid-js@1.9.12)': + dependencies: + '@opencode/protocol': 2.0.15 + '@opencode/schema': 2.0.15 + optionalDependencies: + effect: 4.0.0-rc.112 + solid-js: 1.9.12 + + '@opencode/plugin@2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))(redis@6.2.1(@opentelemetry/api@1.9.1))(solid-js@1.9.12)': dependencies: '@ai-sdk/provider': 3.0.8 - '@opencode-ai/sdk': 1.18.1 - effect: 4.0.0-beta.83 + '@opencode/ai': 2.0.15 + '@opencode/client': 2.0.15(effect@4.0.0-rc.112)(solid-js@1.9.12) + '@opencode/protocol': 2.0.15 + '@opencode/schema': 2.0.15 + '@opencode/util': 2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(redis@6.2.1(@opentelemetry/api@1.9.1)) + '@standard-schema/spec': 1.1.0 + effect: 4.0.0-rc.112 zod: 4.1.8 optionalDependencies: '@opentui/core': 0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10) '@opentui/solid': 0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) + solid-js: 1.9.12 + transitivePeerDependencies: + - '@opentelemetry/api-logs' + - '@opentelemetry/resources' + - '@opentelemetry/sdk-logs' + - '@opentelemetry/sdk-metrics' + - '@opentelemetry/sdk-trace-web' + - '@opentelemetry/semantic-conventions' + - bufferutil + - redis + - supports-color + - utf-8-validate + + '@opencode/protocol@2.0.15': + dependencies: + '@opencode/schema': 2.0.15 + effect: 4.0.0-rc.112 + + '@opencode/schema@2.0.15': + dependencies: + '@standard-schema/spec': 1.1.0 + effect: 4.0.0-rc.112 - '@opencode-ai/sdk@1.18.1': + '@opencode/util@2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(redis@6.2.1(@opentelemetry/api@1.9.1))': dependencies: + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.214.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + '@effect/platform-node': 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) + '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112) + '@npmcli/arborist': 9.4.0 + '@npmcli/config': 10.8.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.214.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.6.1(@opentelemetry/api@1.9.0) cross-spawn: 7.0.6 + effect: 4.0.0-rc.112 + glob: 13.0.5 + mime-types: 3.0.2 + minimatch: 10.2.5 + npm-package-arg: 13.0.2 + pacote: 21.5.1 + transitivePeerDependencies: + - '@opentelemetry/api-logs' + - '@opentelemetry/resources' + - '@opentelemetry/sdk-logs' + - '@opentelemetry/sdk-metrics' + - '@opentelemetry/sdk-trace-web' + - '@opentelemetry/semantic-conventions' + - bufferutil + - redis + - supports-color + - utf-8-validate + + '@opentelemetry/api-logs@0.214.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.0': {} '@opentelemetry/api@1.9.1': {} + '@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + optional: true + + '@opentelemetry/exporter-trace-otlp-http@0.214.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.214.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.214.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.214.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.214.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.214.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.214.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.214.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) + protobufjs: 7.6.6 + '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + optional: true + + '@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.214.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.214.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + optional: true + '@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics@2.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions@1.43.0': {} '@opentui/core-darwin-arm64@0.5.10': @@ -2061,6 +3573,51 @@ snapshots: - typescript - web-tree-sitter + '@pkgjs/parseargs@0.11.0': + optional: true + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@redis/bloom@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + + '@redis/client@6.2.1(@opentelemetry/api@1.9.1)': + dependencies: + cluster-key-slot: 1.1.2 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + + '@redis/json@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + + '@redis/search@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + + '@redis/time-series@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + dependencies: + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -2136,8 +3693,116 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true + '@sigstore/bundle@4.0.0': + dependencies: + '@sigstore/protobuf-specs': 0.5.2 + + '@sigstore/core@3.2.1': {} + + '@sigstore/protobuf-specs@0.5.2': {} + + '@sigstore/sign@4.1.1': + dependencies: + '@gar/promise-retry': 1.0.3 + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.2 + make-fetch-happen: 15.0.6 + proc-log: 6.1.0 + transitivePeerDependencies: + - supports-color + + '@sigstore/tuf@4.0.2': + dependencies: + '@sigstore/protobuf-specs': 0.5.2 + tuf-js: 4.1.0 + transitivePeerDependencies: + - supports-color + + '@sigstore/verify@3.1.1': + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.2 + + '@smithy/core@3.35.0': + dependencies: + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.14': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.19.0 + '@smithy/util-hex-encoding': 4.5.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.8.0': + dependencies: + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.12.1': + dependencies: + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.3': + dependencies: + '@smithy/core': 3.35.0 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/types@4.19.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.5.2': + dependencies: + '@smithy/core': 3.35.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.5.2': + dependencies: + '@smithy/core': 3.35.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.5.2 + tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} + '@tufjs/canonical-json@2.0.0': {} + + '@tufjs/models@4.1.0': + dependencies: + '@tufjs/canonical-json': 2.0.0 + minimatch: 10.2.5 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2151,6 +3816,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.6 + '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -2250,12 +3919,26 @@ snapshots: '@vitest/pretty-format': 4.0.17 tinyrainbow: 3.0.3 + abbrev@4.0.0: {} + + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + array-timsort@1.0.3: {} assertion-error@2.0.1: {} + aws4fetch@1.0.20: {} + babel-plugin-jsx-dom-expressions@0.40.6(@babel/core@7.28.0): dependencies: '@babel/core': 7.28.0 @@ -2282,8 +3965,9 @@ snapshots: balanced-match@1.0.2: {} - base64-js@1.5.1: - optional: true + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} baseline-browser-mapping@2.10.44: {} @@ -2293,6 +3977,16 @@ snapshots: prebuild-install: 7.1.3 optional: true + bignumber.js@9.3.1: {} + + bin-links@6.0.2: + dependencies: + cmd-shim: 8.0.0 + npm-normalize-package-bin: 5.0.0 + proc-log: 6.1.0 + read-cmd-shim: 6.0.0 + write-file-atomic: 7.0.1 + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -2305,10 +3999,16 @@ snapshots: readable-stream: 3.6.2 optional: true + bowser@2.14.1: {} + brace-expansion@2.1.4: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + browserslist@4.28.7: dependencies: baseline-browser-mapping: 2.10.44 @@ -2317,6 +4017,8 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.7) + buffer-equal-constant-time@1.0.1: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -2327,6 +4029,19 @@ snapshots: dependencies: typescript: 7.0.2 + cacache@20.0.4: + dependencies: + '@npmcli/fs': 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.5 + lru-cache: 11.5.3 + minipass: 7.1.3 + minipass-collect: 2.0.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + p-map: 7.0.8 + ssri: 13.0.1 + caniuse-lite@1.0.30001806: {} chai@6.2.2: {} @@ -2334,11 +4049,29 @@ snapshots: chownr@1.1.4: optional: true + chownr@3.0.0: {} + + ci-info@4.4.0: {} + + cluster-key-slot@1.1.2: {} + + cmd-shim@8.0.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + comment-json@5.0.0: dependencies: array-timsort: 1.0.3 esprima: 4.0.1 + common-ancestor-path@2.0.0: {} + + content-type@2.1.0: {} + convert-source-map@2.0.0: {} cross-spawn@7.0.6: @@ -2347,8 +4080,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cssesc@3.0.0: {} + csstype@3.2.3: {} + data-uri-to-buffer@4.0.1: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2366,23 +4103,25 @@ snapshots: diff@9.0.0: {} - effect@4.0.0-beta.83: + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 4.8.0 - find-my-way-ts: 0.1.6 - ini: 7.0.0 - kubernetes-types: 1.30.0 - msgpackr: 2.0.4 - multipasta: 0.2.7 - toml: 4.1.1 - uuid: 14.0.1 - yaml: 2.9.0 + safe-buffer: 5.2.1 + + effect@4.0.0-rc.112: + dependencies: + fast-check: 4.10.2 + msgpackr: 2.1.0 electron-to-chromium@1.5.393: {} emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -2392,6 +4131,8 @@ snapshots: entities@7.0.1: {} + env-paths@2.2.1: {} + es-errors@1.3.0: {} es-module-lexer@1.7.0: {} @@ -2438,7 +4179,11 @@ snapshots: expect-type@1.3.0: {} - fast-check@4.8.0: + exponential-backoff@3.1.3: {} + + extend@3.0.2: {} + + fast-check@4.10.2: dependencies: pure-rand: 8.4.0 @@ -2456,6 +4201,11 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + file-uri-to-path@1.0.0: optional: true @@ -2463,15 +4213,26 @@ snapshots: dependencies: json5: 2.2.3 - find-my-way-ts@0.1.6: {} - find-up@3.0.0: dependencies: locate-path: 3.0.0 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + fs-constants@1.0.0: optional: true + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.3 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -2479,6 +4240,31 @@ snapshots: function-bind@1.1.2: {} + gaxios@7.1.3: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + + gaxios@7.3.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.4: + dependencies: + gaxios: 7.1.3 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + gensync@1.0.0-beta.2: {} get-east-asian-width@1.4.0: {} @@ -2486,6 +4272,21 @@ snapshots: github-from-package@0.0.0: optional: true + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.5: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@9.3.5: dependencies: fs.realpath: 1.0.0 @@ -2493,40 +4294,129 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + google-auth-library@10.5.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.1 + gcp-metadata: 8.1.4 + google-logging-utils: 1.2.0 + gtoken: 8.0.0 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + google-logging-utils@1.2.0: {} + + graceful-fs@4.2.11: {} + + gtoken@8.0.0: + dependencies: + gaxios: 7.3.1 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + hasown@2.0.2: dependencies: function-bind: 1.1.2 + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.5.3 + html-entities@2.3.3: {} + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + ieee754@1.2.1: optional: true + ignore-walk@8.0.0: + dependencies: + minimatch: 10.2.5 + inherits@2.0.4: optional: true ini@1.3.8: optional: true - ini@7.0.0: {} + ini@6.0.0: {} + + ip-address@10.7.2: {} is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-fullwidth-code-point@3.0.0: {} + isexe@2.0.0: {} + isexe@4.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + js-tokens@4.0.0: {} jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-parse-even-better-errors@5.0.0: {} + json-schema@0.4.0: {} + json-stringify-nice@1.1.4: {} + json5@2.2.3: {} jsonc-parser@3.3.1: {} - kubernetes-types@1.30.0: {} + jsonparse@1.3.1: {} + + just-diff-apply@5.5.0: {} + + just-diff@6.0.2: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 lefthook-darwin-arm64@2.0.15: optional: true @@ -2576,8 +4466,12 @@ snapshots: p-locate: 3.0.0 path-exists: 3.0.0 + long@5.3.2: {} + lru-cache@10.4.3: {} + lru-cache@11.5.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2586,22 +4480,87 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-fetch-happen@15.0.6: + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/agent': 4.0.2 + '@npmcli/redact': 4.0.0 + cacache: 20.0.4 + http-cache-semantics: 4.2.0 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 1.1.0 + proc-log: 6.1.0 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + marked@17.0.1: {} + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@4.1.0: {} + mimic-response@3.1.0: optional: true + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.12 + minimatch@8.0.7: dependencies: brace-expansion: 2.1.4 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + minimist@1.2.8: optional: true + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.3 + + minipass-fetch@5.0.2: + dependencies: + minipass: 7.1.3 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + iconv-lite: 0.7.3 + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.3 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + minipass@4.2.8: {} minipass@7.1.3: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mkdirp-classic@0.5.3: optional: true @@ -2619,29 +4578,98 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@2.0.4: + msgpackr@2.1.0: optionalDependencies: msgpackr-extract: 3.0.4 - multipasta@0.2.7: {} - nanoid@3.3.11: {} napi-build-utils@2.0.0: optional: true + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + node-abi@3.92.0: dependencies: semver: 7.8.4 optional: true + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 optional: true + node-gyp@12.4.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.8.4 + tar: 7.5.22 + tinyglobby: 0.2.15 + undici: 6.28.1 + which: 6.0.1 + node-releases@2.0.51: {} + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + + npm-bundled@5.0.0: + dependencies: + npm-normalize-package-bin: 5.0.0 + + npm-install-checks@8.0.0: + dependencies: + semver: 7.8.4 + + npm-normalize-package-bin@5.0.0: {} + + npm-package-arg@13.0.2: + dependencies: + hosted-git-info: 9.0.3 + proc-log: 6.1.0 + semver: 7.8.4 + validate-npm-package-name: 7.0.2 + + npm-packlist@10.0.4: + dependencies: + ignore-walk: 8.0.0 + proc-log: 6.1.0 + + npm-pick-manifest@11.0.3: + dependencies: + npm-install-checks: 8.0.0 + npm-normalize-package-bin: 5.0.0 + npm-package-arg: 13.0.2 + semver: 7.8.4 + + npm-registry-fetch@19.1.1: + dependencies: + '@npmcli/redact': 4.0.0 + jsonparse: 1.3.1 + make-fetch-happen: 15.0.6 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minizlib: 3.1.0 + npm-package-arg: 13.0.2 + proc-log: 6.1.0 + transitivePeerDependencies: + - supports-color + obug@2.1.1: {} once@1.4.0: @@ -2657,8 +4685,40 @@ snapshots: dependencies: p-limit: 2.3.0 + p-map@7.0.8: {} + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + + pacote@21.5.1: + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/git': 7.0.2 + '@npmcli/installed-package-contents': 4.0.0 + '@npmcli/package-json': 7.0.5 + '@npmcli/promise-spawn': 9.0.1 + '@npmcli/run-script': 10.0.4 + cacache: 20.0.4 + fs-minipass: 3.0.3 + minipass: 7.1.3 + npm-package-arg: 13.0.2 + npm-packlist: 10.0.4 + npm-pick-manifest: 11.0.3 + npm-registry-fetch: 19.1.1 + proc-log: 6.1.0 + sigstore: 4.1.1 + ssri: 13.0.1 + tar: 7.5.22 + transitivePeerDependencies: + - supports-color + + parse-conflict-json@5.0.1: + dependencies: + json-parse-even-better-errors: 5.0.0 + just-diff: 6.0.2 + just-diff-apply: 5.5.0 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -2674,6 +4734,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -2684,6 +4749,11 @@ snapshots: dependencies: find-up: 3.0.0 + postcss-selector-parser@7.1.6: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -2706,6 +4776,28 @@ snapshots: tunnel-agent: 0.6.0 optional: true + proc-log@6.1.0: {} + + proggy@4.0.0: {} + + promise-all-reject-late@1.0.1: {} + + promise-call-limit@3.0.2: {} + + protobufjs@7.6.6: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 22.19.6 + long: 5.3.2 + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -2722,6 +4814,8 @@ snapshots: strip-json-comments: 2.0.1 optional: true + read-cmd-shim@6.0.0: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -2729,6 +4823,17 @@ snapshots: util-deprecate: 1.0.2 optional: true + redis@6.2.1(@opentelemetry/api@1.9.1): + dependencies: + '@redis/bloom': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + '@redis/json': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + '@redis/search': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + '@redis/time-series': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + transitivePeerDependencies: + - '@node-rs/xxhash' + - '@opentelemetry/api' + reselect@4.1.8: {} resolve@1.22.12: @@ -2738,6 +4843,10 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 @@ -2771,13 +4880,14 @@ snapshots: s-js@0.4.9: {} - safe-buffer@5.2.1: + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: optional: true semver@6.3.1: {} - semver@7.8.4: - optional: true + semver@7.8.4: {} seroval-plugins@1.5.4(seroval@1.5.4): dependencies: @@ -2793,6 +4903,19 @@ snapshots: siginfo@2.0.0: {} + signal-exit@4.1.0: {} + + sigstore@4.1.1: + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.2 + '@sigstore/sign': 4.1.1 + '@sigstore/tuf': 4.0.2 + '@sigstore/verify': 3.1.1 + transitivePeerDependencies: + - supports-color + simple-concat@1.0.1: optional: true @@ -2805,6 +4928,21 @@ snapshots: sisteransi@1.0.5: {} + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.10 + transitivePeerDependencies: + - supports-color + + socks@2.8.10: + dependencies: + ip-address: 10.7.2 + smart-buffer: 4.2.0 + solid-js@1.9.12: dependencies: csstype: 3.2.3 @@ -2813,10 +4951,35 @@ snapshots: source-map-js@1.2.1: {} + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.24 + + spdx-license-ids@3.0.24: {} + + ssri@13.0.1: + dependencies: + minipass: 7.1.3 + stackback@0.0.2: {} std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -2828,6 +4991,10 @@ snapshots: safe-buffer: 5.2.1 optional: true + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-ansi@7.1.2: dependencies: ansi-regex: 6.2.2 @@ -2854,6 +5021,14 @@ snapshots: readable-stream: 3.6.2 optional: true + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + tinybench@2.9.0: {} tinyexec@1.0.2: {} @@ -2865,7 +5040,17 @@ snapshots: tinyrainbow@3.0.3: {} - toml@4.1.1: {} + treeverse@3.0.0: {} + + tslib@2.8.1: {} + + tuf-js@4.1.0: + dependencies: + '@tufjs/models': 4.1.0 + debug: 4.4.3 + make-fetch-happen: 15.0.6 + transitivePeerDependencies: + - supports-color tunnel-agent@0.6.0: dependencies: @@ -2897,16 +5082,19 @@ snapshots: undici-types@6.21.0: {} + undici@6.28.1: {} + + undici@8.11.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: browserslist: 4.28.7 escalade: 3.2.0 picocolors: 1.1.1 - util-deprecate@1.0.2: - optional: true + util-deprecate@1.0.2: {} - uuid@14.0.1: {} + validate-npm-package-name@7.0.2: {} vite@7.3.2(@types/node@22.19.6)(yaml@2.8.3): dependencies: @@ -2959,26 +5147,54 @@ snapshots: - tsx - yaml + walk-up-path@4.0.0: {} + + web-streams-polyfill@3.3.3: {} + web-tree-sitter@0.25.10: {} which@2.0.2: dependencies: isexe: 2.0.0 + which@6.0.1: + dependencies: + isexe: 4.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + wrappy@1.0.2: optional: true + write-file-atomic@7.0.1: + dependencies: + signal-exit: 4.1.0 + + ws@8.21.3: {} + xdg-basedir@5.1.0: {} yallist@3.1.1: {} - yaml@2.8.3: {} + yallist@4.0.0: {} - yaml@2.9.0: {} + yallist@5.0.0: {} + + yaml@2.8.3: {} zod@4.1.8: {} diff --git a/scripts/prepare-tui-dist.mjs b/scripts/prepare-tui-dist.mjs index b7c648d9..67500e7a 100644 --- a/scripts/prepare-tui-dist.mjs +++ b/scripts/prepare-tui-dist.mjs @@ -7,21 +7,13 @@ import solidPreset from "babel-preset-solid"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(__dirname, ".."); -const sourcePath = path.join(rootDir, "src", "tui.tsx"); -const distSourcePath = path.join(rootDir, "dist", "tui.tsx"); +const sourcePath = path.join(rootDir, "src", "tui-v2.tsx"); const distJsPath = path.join(rootDir, "dist", "tui.js"); +const distTypesPath = path.join(rootDir, "dist", "tui.d.ts"); const distJsxPath = path.join(rootDir, "dist", "tui.jsx"); const distJsxMapPath = path.join(rootDir, "dist", "tui.jsx.map"); -const tuiV2SourcePath = path.join(rootDir, "src", "tui-v2.tsx"); -const tuiV2DistJsPath = path.join(rootDir, "dist", "tui-v2.js"); -const tuiV2DistJsxPath = path.join(rootDir, "dist", "tui-v2.jsx"); -const tuiV2DistJsxMapPath = path.join(rootDir, "dist", "tui-v2.jsx.map"); -await fs.copyFile(sourcePath, distSourcePath); -for (const [inputPath, outputPath] of [ - [sourcePath, distJsPath], - [tuiV2SourcePath, tuiV2DistJsPath], -]) { +for (const [inputPath, outputPath] of [[sourcePath, distJsPath]]) { const source = await fs.readFile(inputPath, "utf8"); const transformed = await babel.transformAsync(source, { filename: inputPath, @@ -42,5 +34,5 @@ for (const [inputPath, outputPath] of [ await fs.rm(distJsxPath, { force: true }); await fs.rm(distJsxMapPath, { force: true }); -await fs.rm(tuiV2DistJsxPath, { force: true }); -await fs.rm(tuiV2DistJsxMapPath, { force: true }); +await fs.copyFile(path.join(rootDir, "dist", "tui-v2.d.ts"), distTypesPath); +await fs.copyFile(path.join(rootDir, "dist", "tui-v2.d.ts.map"), `${distTypesPath}.map`); diff --git a/scripts/smoke-packed-package.mjs b/scripts/smoke-packed-package.mjs index 5898aa76..ae2a913d 100644 --- a/scripts/smoke-packed-package.mjs +++ b/scripts/smoke-packed-package.mjs @@ -62,8 +62,9 @@ try { import { fileURLToPath, pathToFileURL } from "node:url"; const rootExportUrl = import.meta.resolve("@slkiser/opencode-quota"); - await import("@slkiser/opencode-quota"); + const { default: server } = await import("@slkiser/opencode-quota"); await import("@slkiser/opencode-quota/server"); + assert.equal(typeof server.setup, "function"); const { metrics } = await import("@opentelemetry/api"); assert.equal(typeof metrics.getMeter, "function"); @@ -72,8 +73,8 @@ try { assert.match(tuiExportPath, /node_modules\\/\\@slkiser\\/opencode-quota\\/dist\\/tui\\.js$/); const tuiSource = await readFile(tuiExportPath, "utf8"); assert.ok(tuiSource.includes("@slkiser/opencode-quota")); - assert.ok(tuiSource.includes("const pluginModule")); - assert.ok(tuiSource.includes("tui")); + assert.ok(tuiSource.includes("Plugin.define")); + assert.ok(tuiSource.includes("prompt.footer")); assert.ok(!tuiSource.includes("jsx-dev-runtime")); const pkg = JSON.parse( diff --git a/scripts/verify-typescript-version.mjs b/scripts/verify-typescript-version.mjs index 49dd5aaf..079300f3 100644 --- a/scripts/verify-typescript-version.mjs +++ b/scripts/verify-typescript-version.mjs @@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url"; import { parse } from "yaml"; const EXPECTED_TYPESCRIPT_VERSION = "7.0.2"; -const EXPECTED_PLUGIN_VERSION = "1.18.1"; +const EXPECTED_PLUGIN_VERSION = "2.0.15"; +const PLUGIN_PACKAGE = "@opencode/plugin"; const EXPECTED_OPENTUI_SPECIFIER = "^0.5.10"; const EXPECTED_OPENTUI_VERSION = "0.5.10"; const EXPECTED_OPENTUI_PACKAGES = ["@opentui/core", "@opentui/solid"]; @@ -54,7 +55,7 @@ function hasTypeScriptPeerOverride(value) { } const configuredTypeScript = packageJson.devDependencies?.typescript; -const configuredPlugin = packageJson.devDependencies?.["@opencode-ai/plugin"]; +const configuredPlugin = packageJson.devDependencies?.[PLUGIN_PACKAGE]; if (configuredTypeScript !== EXPECTED_TYPESCRIPT_VERSION) { fail( `TypeScript must be pinned exactly to ${EXPECTED_TYPESCRIPT_VERSION}; package.json has ${String(configuredTypeScript)}.`, @@ -62,7 +63,7 @@ if (configuredTypeScript !== EXPECTED_TYPESCRIPT_VERSION) { } if (configuredPlugin !== EXPECTED_PLUGIN_VERSION) { fail( - `@opencode-ai/plugin must be pinned exactly to ${EXPECTED_PLUGIN_VERSION}; package.json has ${String(configuredPlugin)}.`, + `${PLUGIN_PACKAGE} must be pinned exactly to ${EXPECTED_PLUGIN_VERSION}; package.json has ${String(configuredPlugin)}.`, ); } for (const name of EXPECTED_OPENTUI_PACKAGES) { @@ -122,21 +123,21 @@ for (const name of EXPECTED_OPENTUI_PACKAGES) { } } -const lockedPlugin = rootImporter.devDependencies?.["@opencode-ai/plugin"]; +const lockedPlugin = rootImporter.devDependencies?.[PLUGIN_PACKAGE]; if ( lockedPlugin?.specifier !== EXPECTED_PLUGIN_VERSION || (lockedPlugin?.version !== EXPECTED_PLUGIN_VERSION && !lockedPlugin?.version?.startsWith(`${EXPECTED_PLUGIN_VERSION}(`)) ) { fail( - `@opencode-ai/plugin lock mismatch: expected ${EXPECTED_PLUGIN_VERSION}, found ${String(lockedPlugin?.specifier)} and ${String(lockedPlugin?.version)}.`, + `${PLUGIN_PACKAGE} lock mismatch: expected ${EXPECTED_PLUGIN_VERSION}, found ${String(lockedPlugin?.specifier)} and ${String(lockedPlugin?.version)}.`, ); } const packageKeys = Object.keys(lockfile.packages ?? {}); const snapshotKeys = Object.keys(lockfile.snapshots ?? {}); const expectedTypeScriptKey = `typescript@${EXPECTED_TYPESCRIPT_VERSION}`; -const expectedPluginKey = `@opencode-ai/plugin@${EXPECTED_PLUGIN_VERSION}`; +const expectedPluginKey = `${PLUGIN_PACKAGE}@${EXPECTED_PLUGIN_VERSION}`; if (!packageKeys.includes(expectedTypeScriptKey) || !snapshotKeys.includes(expectedTypeScriptKey)) { fail(`pnpm-lock.yaml must contain package and snapshot entries for ${expectedTypeScriptKey}.`); @@ -186,5 +187,5 @@ console.warn( `Known unmet peer: ${bunPackageKey} declares typescript ${BUN_FFI_TYPESCRIPT_PEER}, while the root uses ${EXPECTED_TYPESCRIPT_VERSION}. This mismatch is intentionally not suppressed.`, ); console.log( - `TypeScript ${EXPECTED_TYPESCRIPT_VERSION} and @opencode-ai/plugin ${EXPECTED_PLUGIN_VERSION} lock entries verified.`, + `TypeScript ${EXPECTED_TYPESCRIPT_VERSION} and ${PLUGIN_PACKAGE} ${EXPECTED_PLUGIN_VERSION} lock entries verified.`, ); diff --git a/src/index.ts b/src/index.ts index d4315fb4..2157c26a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,3 @@ -import "@opentui/solid/preload"; - export type { MiniMaxResult, MiniMaxResultEntry } from "./lib/types.js"; -const { default: plugin } = await import("./tui-v2.js"); - -export default plugin; +export { default } from "./plugin.js"; diff --git a/src/plugin.ts b/src/plugin.ts index 1895e2dd..dc9cd0b8 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,435 +1,80 @@ -/** - * OpenCode Quota Toast Plugin - * - * Shows a minimal quota status toast without LLM invocation. - * Triggers on session.idle, session.compacted, and question tool completion. - * Supports GitHub Copilot and Google (via opencode-antigravity-auth). - */ - -import { isMainThread } from "node:worker_threads"; -import type { Plugin } from "@opencode-ai/plugin"; -import { tool } from "@opencode-ai/plugin"; -import { handled } from "./lib/command-handled.js"; -import { shouldRegisterServerSlashCommands } from "./lib/command-surfaces.js"; +/** OpenCode V2 server plugin: deterministic quota diagnostics tool. */ +import { Plugin } from "@opencode/plugin"; import { findGitWorktreeRoot, getEffectiveConfigRoot } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { reconcileDetectedProvidersInGlobalConfig } from "./lib/opencode-config-providers.js"; -import { - buildQuotaDialogCommandOutput, - isQuotaDialogCommand, - QUOTA_DIALOG_COMMANDS, - type QuotaDialogCommandId, -} from "./lib/quota-dialog-commands.js"; -import type { SessionModelMeta } from "./lib/quota-render-data.js"; -import type { SessionTokenError } from "./lib/quota-status.js"; -import { disposeQuotaTelemetryOwner } from "./lib/quota-telemetry.js"; -import { createQuotaToastRuntime } from "./lib/quota-toast-runtime.js"; -import type { QuotaToastConfig } from "./lib/types.js"; - -// ============================================================================= -// Types -// ============================================================================= - -/** Minimal client type for SDK compatibility */ -interface OpencodeClient { - config: { - get: () => Promise<{ - data?: { - model?: string; - experimental?: { - quotaToast?: Partial; - }; - }; - }>; - providers: () => Promise<{ - data?: { - providers: Array<{ id: string }>; // minimal shape - }; - }>; - }; - session: { - get: (params: { path: { id: string } }) => Promise<{ - data?: { - parentID?: string; - model?: { - id?: string; - providerID?: string; - }; - }; - }>; - prompt: (params: { - path: { id: string }; - body: { - noReply?: boolean; - parts: Array<{ type: "text"; text: string; ignored?: boolean }>; - }; - }) => Promise; - }; - tui: { - showToast: (params: { - body: { - message: string; - variant: "info" | "success" | "warning" | "error"; - duration?: number; - title?: string; - }; - }) => Promise; - }; - app: { - log: (params: { - body: { - service: string; - level: "debug" | "info" | "warn" | "error"; - message: string; - extra?: Record; - }; - }) => Promise; - }; -} - -/** Event type for plugin hooks */ -interface PluginEvent { - type: string; - properties: { - sessionID?: string; - [key: string]: unknown; - }; -} - -/** Tool execute hook input */ -interface ToolExecuteAfterInput { - tool: string; - sessionID: string; - callID: string; -} - -/** Tool execute hook output */ -interface ToolExecuteAfterOutput { - title: string; - output: string; - metadata: unknown; -} - -/** Config hook shape used to register built-in commands */ -interface PluginConfigInput { - command?: Record; - agent?: Record; - default_agent?: string; -} - -function normalizeDefaultAgent(cfg: PluginConfigInput | null | undefined): void { - if (!cfg?.default_agent || !cfg.agent || cfg.default_agent in cfg.agent) return; - - const stripped = (value: string) => value.replace(/[\u200B\u200C\u200D\uFEFF]/g, ""); - const target = stripped(cfg.default_agent); - const matches = Object.keys(cfg.agent).filter((key) => stripped(key) === target); - if (matches.length === 1) { - cfg.default_agent = matches[0]; - } -} - -/** Server command execution hook input */ -interface CommandExecuteInput { - command: string; - arguments?: string; - sessionID: string; -} - -// ============================================================================= -// Plugin Implementation -// ============================================================================= - -/** - * Main plugin export - */ -export const QuotaToastPlugin: Plugin = async ({ client, directory }) => { - const typedClient = client as unknown as OpencodeClient; - let opencodeConfig: PluginConfigInput | null = null; - - /** - * Inject tool output directly into the session without triggering an LLM response. - * This prevents models from summarizing/rewriting our carefully formatted reports. - */ - async function injectRawOutput( - sessionID: string, - output: string, - options: { rethrow?: boolean } = {}, - ): Promise { - normalizeDefaultAgent(opencodeConfig); - - try { - await typedClient.session.prompt({ - path: { id: sessionID }, - body: { - noReply: true, - // ignored=true keeps this out of future model context while still - // showing it to the user in the transcript. - parts: [{ type: "text", text: sanitizeDisplayText(output), ignored: true }], - }, - }); - } catch (err) { - // Log but don't fail by default - tool output can still be returned. - await typedClient.app.log({ - body: { - service: "quota-toast", - level: "warn", - message: "Failed to inject raw output", - extra: { error: err instanceof Error ? err.message : String(err) }, - }, - }); - if (options.rethrow) { - throw err; - } - } - } - - let providerConfigReconcileQueue: Promise = Promise.resolve(); - - // Track last session token error for /quota_status diagnostics - let lastSessionTokenError: SessionTokenError | undefined; - - function getPluginRuntimeRootHints() { - const cwd = directory || process.cwd(); - const workspaceRoot = findGitWorktreeRoot(cwd) ?? cwd; - const configRoot = getEffectiveConfigRoot(workspaceRoot); - return { +import { buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js"; + +export const QuotaToastPlugin = Plugin.define({ + id: "@slkiser/opencode-quota.server", + async setup(ctx) { + const directory = ctx.location.directory; + const workspaceRoot = findGitWorktreeRoot(directory) ?? directory; + const roots = { workspaceRoot, - configRoot, - fallbackDirectory: cwd, + configRoot: getEffectiveConfigRoot(workspaceRoot), + fallbackDirectory: directory, }; - } - - function registerDeterministicSlashCommands(cfg: PluginConfigInput): void { - cfg.command ??= {}; - - for (const spec of QUOTA_DIALOG_COMMANDS) { - cfg.command[spec.id] = { - template: `/${spec.slashName}`, - description: spec.description, - }; - } - } - - async function handleDeterministicSlashCommand(input: CommandExecuteInput): Promise { - const command = input.command as QuotaDialogCommandId; - const result = await buildQuotaDialogCommandOutput({ - command, - arguments: input.arguments, - client: typedClient, - roots: getPluginRuntimeRootHints(), - sessionID: input.sessionID, - resolveSessionMeta: (sessionID) => getSessionModelMeta(sessionID), - lastSessionTokenError, - setLastSessionTokenError: (error) => { - lastSessionTokenError = error; + // The quota collector accepts a small V1-shaped configuration client. V2 + // does not expose a mutable global config to plugins; quota settings are + // read from the plugin's own config file by the collector. + const client = { + config: { + get: async () => ({ data: {} }), + providers: async () => ({ + data: { + providers: (await ctx.provider.list()).data.map((provider) => ({ id: provider.id })), + }, + }), }, - log, - }); - - if (result.state === "output") { - await injectRawOutput(input.sessionID, result.output, { rethrow: true }); - } - - handled(); - } - - /** - * Log a message (debug level) - */ - async function log(message: string, extra?: Record): Promise { - try { - await typedClient.app.log({ - body: { - service: "quota-toast", - level: "debug", - message, - extra, - }, - }); - } catch { - // Ignore logging errors - } - } - - async function reconcileDetectedProviderConfig(providerIds: readonly string[]): Promise { - if (!directory || providerIds.length === 0) return; - - const reconcile = async () => { - try { - const result = await reconcileDetectedProvidersInGlobalConfig({ - configRootDir: getPluginRuntimeRootHints().configRoot, - detectedProviderIds: providerIds, - }); - if (result.changed) { - await log("Added detected providers to global OpenCode config", { - path: result.path, - format: result.format, - providers: result.addedProviderIds, - }); - } - } catch (error) { - try { - await typedClient.app.log({ - body: { - service: "quota-toast", - level: "warn", - message: "Failed to add detected providers to global OpenCode config", - extra: { error: error instanceof Error ? error.message : String(error) }, - }, - }); - } catch { - // Automatic config repair is best-effort and must not break quota output. - } - } }; - providerConfigReconcileQueue = providerConfigReconcileQueue.then(reconcile, reconcile); - await providerConfigReconcileQueue; - } - - /** - * Check if session is a subagent session - */ - async function isSubagentSession(sessionID: string): Promise { - try { - const response = await typedClient.session.get({ path: { id: sessionID } }); - // Subagent sessions have a parentID - return !!response.data?.parentID; - } catch { - // If we can't determine, assume it's a primary session - return false; - } - } - - /** - * Get the current model metadata from the active session. - * - * Only uses session-scoped model lookup. Does NOT fall back to - * client.config.get() because that returns the global/default model - * which can be stale across sessions. - */ - async function getSessionModelMeta(sessionID?: string): Promise { - if (!sessionID) return {}; - try { - const sessionResp = await typedClient.session.get({ path: { id: sessionID } }); - return { - modelID: sessionResp.data?.model?.id, - providerID: sessionResp.data?.model?.providerID, - }; - } catch { - return {}; - } - } - - const quotaToastRuntime = createQuotaToastRuntime({ - client: typedClient, - roots: getPluginRuntimeRootHints, - resolveSessionMeta: (sessionID) => getSessionModelMeta(sessionID), - isSubagentSession, - reconcileDetectedProviders: reconcileDetectedProviderConfig, - setSessionTokenError: (error) => { - lastSessionTokenError = error; - }, - showToast: (body) => typedClient.tui.showToast({ body }), - log, - onInitialized: (extra) => { - void typedClient.app - .log({ - body: { - service: "quota-toast", - level: "info", - message: "plugin initialized", - extra, + await ctx.tool.transform((editor) => { + editor.add({ + name: "quota_status", + description: "Diagnostics for toast, TUI, pricing and local storage.", + input: { + type: "object", + properties: { + refreshGoogleTokens: { type: "boolean" }, + skewMs: { type: "integer", minimum: 0 }, + force: { type: "boolean" }, }, - }) - .catch(() => {}); - }, - }); - - // Return hook implementations - return { - dispose: async () => { - disposeQuotaTelemetryOwner(typedClient); - }, - - config: async (input: unknown) => { - const cfg = input as PluginConfigInput; - opencodeConfig = cfg; - if (shouldRegisterServerSlashCommands({ isMainThread, argv: process.argv })) { - registerDeterministicSlashCommands(cfg); - } - - // Keep the config-time correction for #39. injectRawOutput repeats the - // same correction after later config hooks have run to handle #169. - normalizeDefaultAgent(cfg); - }, - - "command.execute.before": async (input: CommandExecuteInput) => { - if (!isQuotaDialogCommand(input.command)) return; - await handleDeterministicSlashCommand(input); - }, - - tool: { - quota_status: tool({ - description: - "Diagnostics for toast + TUI + pricing + local storage (includes unknown pricing report).", - args: { - refreshGoogleTokens: tool.schema - .boolean() - .optional() - .describe("If true, refresh Google Antigravity access tokens before reporting"), - skewMs: tool.schema - .number() - .int() - .min(0) - .optional() - .describe("Refresh tokens expiring within this window (ms). Default: 120000"), - force: tool.schema - .boolean() - .optional() - .describe("If true, refresh even if cached token looks valid"), + additionalProperties: false, }, - async execute(args, context) { + async execute(input, context) { + const options = input as { + refreshGoogleTokens?: boolean; + skewMs?: number; + force?: boolean; + }; const result = await buildQuotaDialogCommandOutput({ command: "quota_status", - arguments: JSON.stringify({ - refreshGoogleTokens: args.refreshGoogleTokens, - skewMs: args.skewMs, - force: args.force, - }), - client: typedClient, - roots: getPluginRuntimeRootHints(), + arguments: JSON.stringify(options), + client, + roots, sessionID: context.sessionID, - resolveSessionMeta: (sessionID) => getSessionModelMeta(sessionID), - lastSessionTokenError, - log, - onDetectedProviderIds: reconcileDetectedProviderConfig, + resolveSessionMeta: async (sessionID) => { + const session = await ctx.session.get({ sessionID }); + return { modelID: session.model?.id, providerID: session.model?.providerID }; + }, + onDetectedProviderIds: async (providerIds) => { + if (providerIds.length === 0) return; + try { + await reconcileDetectedProvidersInGlobalConfig({ + configRootDir: roots.configRoot, + detectedProviderIds: providerIds, + }); + } catch (error) { + console.warn("Failed to add detected providers to global OpenCode config", error); + } + }, }); - if (result.state !== "output") return ""; - context.metadata({ title: "Quota Status" }); - await injectRawOutput(context.sessionID, result.output); - return ""; // Empty return - output already injected with noReply + return { content: result.state === "output" ? sanitizeDisplayText(result.output) : "" }; }, - }), - }, - - // Event hook for session.idle and session.compacted - event: async ({ event }: { event: PluginEvent }) => { - const sessionID = event.properties.sessionID; - if (!sessionID) return; - - if (event.type !== "session.idle" && event.type !== "session.compacted") { - return; - } - - await quotaToastRuntime.handleTrigger({ sessionID, trigger: event.type }); - }, - - // Tool execute hook for question tool - "tool.execute.after": async (input: ToolExecuteAfterInput, _output: ToolExecuteAfterOutput) => { - if (input.tool !== "question") return; + }); + }); + }, +}); - await quotaToastRuntime.handleTrigger({ sessionID: input.sessionID, trigger: "question" }); - }, - }; -}; +export default QuotaToastPlugin; diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 0d34453c..18a46058 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -1,11 +1,17 @@ /** @jsxImportSource @opentui/solid */ +import { Plugin } from "@opencode/plugin/tui"; import { RGBA } from "@opentui/core"; import type { JSX } from "@opentui/solid"; import { createSignal, onCleanup, Show } from "solid-js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { formatQuotaRows } from "./lib/format.js"; +import { + BUNDLED_MAINTAINER_ANNOUNCEMENTS, + formatMaintainerAnnouncementHomeCountLine, + getMaintainerAnnouncementsSummary, +} from "./lib/maintainer-announcements.js"; import { buildQuotaDialogCommandOutput, QUOTA_DIALOG_COMMANDS, @@ -18,6 +24,12 @@ import { type QuotaSessionModelContext, resolveQuotaRuntimeContext, } from "./lib/quota-runtime-context.js"; +import { buildCompactQuotaStatusLine } from "./lib/tui-compact-format.js"; +import { + formatPromptBarPercentMeta, + pickPromptBarEntry, + resolvePromptBarLabel, +} from "./lib/tui-prompt-bar-format.js"; import { buildSidebarQuotaPanelLines } from "./lib/tui-sidebar-format.js"; const terminalForeground = RGBA.defaultForeground(); @@ -31,7 +43,14 @@ type Toast = { }; type TuiContext = { client: unknown; - data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void }; + location?: { directory: string }; + data: { + on: (event: string, handler: (event: TuiEvent) => void) => () => void; + location?: { + default: () => { directory: string }; + provider: { list: (location: { directory: string }) => Array<{ id: string }> | undefined }; + }; + }; keymap: { layer: ( build: () => { @@ -42,7 +61,7 @@ type TuiContext = { group: string; palette: true; slash: { name: string }; - run: (input?: unknown) => void; + run: (input?: unknown) => Promise; }>; }, ) => void; @@ -51,7 +70,8 @@ type TuiContext = { slot: ( claim: | { append: "app"; render: () => null } - | { append: "sidebar.content"; render: (props: { sessionID: string }) => JSX.Element }, + | { append: "sidebar.content"; render: (props: { sessionID: string }) => JSX.Element } + | { append: "prompt.footer" | "home.footer.status"; render: () => JSX.Element }, ) => () => void; toast: { show: (toast: Toast) => void }; dialog: { @@ -67,6 +87,22 @@ function getSessionID(event: TuiEvent): string | undefined { return typeof sessionID === "string" && sessionID ? sessionID : undefined; } +function quotaClient(context: TuiContext) { + return { + config: { + get: async () => ({ data: {} }), + providers: async () => ({ + data: { + providers: + context.data.location?.provider.list( + context.location ?? context.data.location.default(), + ) ?? [], + }, + }), + }, + }; +} + async function getSessionModelMeta( client: unknown, sessionID: string, @@ -84,10 +120,18 @@ async function getQuotaMessage( context: TuiContext, sessionID: string, surface: "sidebar" | "idle" | "compacted" | "question", -): Promise<{ message: string; duration: number; activeProviderCount: number } | undefined> { +): Promise< + | { + message: string; + duration: number; + activeProviderCount: number; + announcementCount: number; + } + | undefined +> { const runtime = await resolveQuotaRuntimeContext({ - client: context.client as never, - roots: { fallbackDirectory: process.cwd() }, + client: quotaClient(context), + roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, sessionID, resolveSessionMeta: (id) => getSessionModelMeta(context.client, id), includeSessionMeta: (config) => config.onlyCurrentModel, @@ -153,10 +197,107 @@ async function getQuotaMessage( message: sanitizeDisplayText(message), duration: config.toastDurationMs, activeProviderCount: result.active.length, + announcementCount: + surface !== "sidebar" && + config.maintainerAnnouncements.enabled && + config.maintainerAnnouncements.home + ? getMaintainerAnnouncementsSummary({ + announcements: BUNDLED_MAINTAINER_ANNOUNCEMENTS, + enabledProviders: result.active.map((provider) => provider.id), + }).activeCount + : 0, } : undefined; } +async function getQuotaFooter( + context: TuiContext, + sessionID: string | undefined, + surface: "prompt" | "home", +): Promise { + const runtime = await resolveQuotaRuntimeContext({ + client: quotaClient(context), + roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, + sessionID, + resolveSessionMeta: (id) => getSessionModelMeta(context.client, id), + includeSessionMeta: (config) => config.onlyCurrentModel && surface === "prompt", + }); + const config = runtime.config; + if (!config.enabled) return ""; + if ( + surface === "prompt" && + !config.tuiPromptBar.enabled && + !(config.tuiCompactStatus.enabled && config.tuiCompactStatus.sessionPrompt) + ) + return ""; + if ( + surface === "home" && + !(config.tuiCompactStatus.enabled && config.tuiCompactStatus.homeBottom) + ) + return ""; + const { data } = await collectQuotaRenderData({ + client: runtime.client, + resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, + config, + configMeta: runtime.configMeta, + request: createQuotaRuntimeRequestContext(runtime), + surfaceExplicitProviderIssues: true, + providers: runtime.providers, + includeAllWindowsData: true, + }); + if (!data) return ""; + if (surface === "prompt" && config.tuiPromptBar.enabled) { + const entry = pickPromptBarEntry(data); + if (!entry) return ""; + return sanitizeDisplayText( + [ + resolvePromptBarLabel(entry), + entry.percentRemaining === undefined + ? "" + : formatPromptBarPercentMeta({ + percentRemaining: entry.percentRemaining, + percentDisplayMode: config.percentDisplayMode, + resetTimeIso: entry.resetTimeIso, + resetTimeDecimals: config.resetTimeDecimals, + resetTimeSpaced: config.resetTimeSpaced, + runway: entry.runway, + }), + ] + .filter(Boolean) + .join(" | "), + ); + } + return buildCompactQuotaStatusLine({ + data, + maxWidth: config.tuiCompactStatus.maxWidth, + percentDisplayMode: config.percentDisplayMode, + accountingDetail: config.accountingDetail, + resetTimeSpaced: config.resetTimeSpaced, + }); +} + +function QuotaFooter(props: { + context: TuiContext; + sessionID?: string; + surface: "prompt" | "home"; +}): JSX.Element { + const [line, setLine] = createSignal(""); + const refresh = () => + void getQuotaFooter(props.context, props.sessionID, props.surface) + .then(setLine) + .catch(reportFailure); + refresh(); + const stop = props.context.data.on("session.step.ended", (event) => { + if (props.surface === "home" || getSessionID(event) === props.sessionID) refresh(); + }); + onCleanup(stop); + return ( + + {line()} + + ); +} + function reportFailure(error: unknown): void { const message = error instanceof Error ? error.message : String(error); console.warn(`[opencode-quota] failed to load quota: ${message}`); @@ -193,8 +334,8 @@ async function runQuotaCommand( const result = await buildQuotaDialogCommandOutput({ command, arguments: argumentsText, - client: context.client as never, - roots: { fallbackDirectory: process.cwd() }, + client: quotaClient(context), + roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, sessionID, resolveSessionMeta: (id) => getSessionModelMeta(context.client, id), }); @@ -220,7 +361,7 @@ function registerQuotaCommands(context: TuiContext, getSessionID: () => string | group: "OpenCode Quota", palette: true, slash: { name: spec.slashName }, - run: (input?: unknown) => void runQuotaCommand(context, spec.id, getSessionID(), input), + run: (input?: unknown) => runQuotaCommand(context, spec.id, getSessionID(), input), })), })); } @@ -233,7 +374,8 @@ function SidebarQuotaView(props: { props.setActiveSessionID(props.sessionID); const [open, setOpen] = createSignal(true); const [quota, setQuota] = createSignal< - { message: string; duration: number; activeProviderCount: number } | undefined + | { message: string; duration: number; activeProviderCount: number; announcementCount: number } + | undefined >(undefined); const lines = () => quota()?.message.split("\n") ?? []; const expandable = () => lines().length > 2; @@ -278,50 +420,62 @@ function SidebarQuotaView(props: { ); } -const plugin = { +const plugin = Plugin.define({ id: "@slkiser/opencode-quota", - setup(context: TuiContext) { + setup(context) { + const api = context as unknown as TuiContext; let disposeEvents: (() => void) | undefined; let activeSessionID: string | undefined; + let announcementsNotified = false; const questionToolCalls = new Set(); - const disposeApp = context.ui.slot({ + const disposeApp = api.ui.slot({ append: "app", render: () => { if (disposeEvents) return null; - registerQuotaCommands(context, () => activeSessionID); + registerQuotaCommands(api, () => activeSessionID); const trigger = (event: TuiEvent, reason: "idle" | "compacted" | "question") => { const sessionID = getSessionID(event); if (!sessionID) return; activeSessionID = sessionID; - void getQuotaMessage(context, sessionID, reason) + void getQuotaMessage(api, sessionID, reason) .then((quota) => { if (!quota) return; - context.ui.toast.show({ + api.ui.toast.show({ variant: "info", title: "OpenCode Quota", message: quota.message, duration: quota.duration, }); + if (!announcementsNotified && quota.announcementCount > 0) { + const message = formatMaintainerAnnouncementHomeCountLine(quota.announcementCount); + if (message) { + api.ui.toast.show({ + variant: "info", + title: "OpenCode Quota", + message: sanitizeDisplayText(message), + duration: quota.duration, + }); + announcementsNotified = true; + } + } }) .catch(reportFailure); }; - const onStepEnded = context.data.on("session.step.ended", (event) => - trigger(event, "idle"), - ); - const onCompacted = context.data.on("session.compaction.ended", (event) => + const onStepEnded = api.data.on("session.step.ended", (event) => trigger(event, "idle")); + const onCompacted = api.data.on("session.compaction.ended", (event) => trigger(event, "compacted"), ); - const onQuestionStarted = context.data.on("session.tool.input.started", (event) => { + const onQuestionStarted = api.data.on("session.tool.input.started", (event) => { const id = event.data?.id; if (event.data?.name === "question" && typeof id === "string") { questionToolCalls.add(id); } }); - const onQuestionSucceeded = context.data.on("session.tool.success", (event) => { + const onQuestionSucceeded = api.data.on("session.tool.success", (event) => { const id = event.data?.id; if (typeof id === "string" && questionToolCalls.delete(id)) trigger(event, "question"); }); - const onQuestionFailed = context.data.on("session.tool.failed", (event) => { + const onQuestionFailed = api.data.on("session.tool.failed", (event) => { const id = event.data?.id; if (typeof id === "string") questionToolCalls.delete(id); }); @@ -336,11 +490,11 @@ const plugin = { return null; }, }); - const disposeSidebar = context.ui.slot({ + const disposeSidebar = api.ui.slot({ append: "sidebar.content", render: (props) => ( { activeSessionID = sessionID; @@ -348,12 +502,22 @@ const plugin = { /> ), }); + const disposePrompt = api.ui.slot({ + append: "prompt.footer", + render: () => , + }); + const disposeHome = api.ui.slot({ + append: "home.footer.status", + render: () => , + }); return () => { disposeEvents?.(); disposeApp(); disposeSidebar(); + disposePrompt(); + disposeHome(); }; }, -}; +}); export default plugin; diff --git a/tests/index.test.ts b/tests/index.test.ts index dcdba516..8e839c2d 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -7,14 +7,14 @@ const pluginMocks = vi.hoisted(() => ({ }, })); -vi.mock("../src/tui-v2.js", () => ({ +vi.mock("../src/plugin.js", () => ({ default: pluginMocks.tui, })); vi.mock("@opentui/solid/preload", () => ({})); describe("package entrypoint", () => { - it("exports the V2 TUI plugin on the default export", async () => { + it("exports the V2 server plugin on the default export", async () => { const mod = await import("../src/index.js"); expect(mod.default).toBe(pluginMocks.tui); diff --git a/tests/lib.alibaba-token-plan.test.ts b/tests/lib.alibaba-token-plan.test.ts index e6d6b698..43992e46 100644 --- a/tests/lib.alibaba-token-plan.test.ts +++ b/tests/lib.alibaba-token-plan.test.ts @@ -651,6 +651,7 @@ process.stdin.on("end", () => { async () => { const bin = await makeDir(); const termSeen = path.join(bin, "term.flag"); + const timeoutMs = 1_000; const started = Date.now(); const result = await runAlibabaTokenPlanProcess( spawnRequest(process.execPath, { @@ -668,14 +669,14 @@ spawn(process.execPath, ["-e", "process.on('SIGTERM', () => {}); setInterval(() setInterval(() => {}, 30000); `, ], - timeoutMs: 80, + timeoutMs, cwd: await makeDir(), }), ); const elapsed = Date.now() - started; expect(result.timedOut).toBe(true); expect(result.truncated).toBe(false); - expect(elapsed).toBeGreaterThanOrEqual(ALIBABA_TOKEN_PLAN_KILL_GRACE_MS); + expect(elapsed).toBeGreaterThanOrEqual(timeoutMs + ALIBABA_TOKEN_PLAN_KILL_GRACE_MS); expect(await (await import("node:fs/promises")).readFile(termSeen, "utf8")).toBe("term"); }, ); diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index 69606586..1912e24b 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -153,8 +153,8 @@ describe("package manifest compatibility", () => { }); it("reflects the local OpenCode V2 plugin dependency override", () => { - expect(pkg.peerDependencies?.["@opencode/plugin"]).toBe("2.0.2"); - expect(pkg.devDependencies?.["@opencode/plugin"]).toBe("2.0.2"); + expect(pkg.peerDependencies?.["@opencode/plugin"]).toBe("2.0.15"); + expect(pkg.devDependencies?.["@opencode/plugin"]).toBe("2.0.15"); expect(pkg.dependencies?.["@opentui/core"]).toBe("^0.5.10"); expect(pkg.dependencies?.["@opentui/solid"]).toBe("^0.5.10"); expect(readme).toContain("Node.js `>= 22` is required."); @@ -165,7 +165,7 @@ describe("package manifest compatibility", () => { it("keeps the TypeScript 7 toolchain explicit without suppressing the known peer mismatch", () => { expect(tsconfig.compilerOptions?.types).toEqual(["node"]); expect(typescriptValidator).toContain('const EXPECTED_TYPESCRIPT_VERSION = "7.0.2";'); - expect(typescriptValidator).toContain('const EXPECTED_PLUGIN_VERSION = "1.18.1";'); + expect(typescriptValidator).toContain('const EXPECTED_PLUGIN_VERSION = "2.0.15";'); expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_SPECIFIER = "^0.5.10";'); expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_VERSION = "0.5.10";'); expect(typescriptValidator).toContain('const BUN_FFI_STRUCTS_VERSION = "0.3.1";'); @@ -285,7 +285,7 @@ describe("package manifest compatibility", () => { expect(pkg.bin).toEqual({ "opencode-quota": "./dist/bin/opencode-quota.js", }); - expect(pkg["oc-plugin"]).toEqual(["server", "tui"]); + expect(pkg["oc-plugin"]).toBeUndefined(); expect(pkg.dependencies?.["@clack/prompts"]).toBeTruthy(); expect(pkg.exports?.["."]).toEqual({ default: "./dist/index.js", diff --git a/tests/plugin-v2.test.ts b/tests/plugin-v2.test.ts new file mode 100644 index 00000000..7aa15768 --- /dev/null +++ b/tests/plugin-v2.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from "vitest"; + +const buildOutput = vi.hoisted(() => vi.fn()); +vi.mock("../src/lib/quota-dialog-commands.js", () => ({ + buildQuotaDialogCommandOutput: buildOutput, +})); + +import plugin from "../src/plugin.js"; + +describe("V2 server plugin", () => { + it("registers a structured quota diagnostics tool without a server toast dependency", async () => { + let registered: + | { + name: string; + execute: (input: unknown, context: { sessionID: string }) => Promise<{ content: string }>; + } + | undefined; + const ctx = { + location: { directory: "/tmp/opencode/quota-plugin-v2" }, + provider: { list: vi.fn().mockResolvedValue({ data: [{ id: "openai" }] }) }, + session: { + get: vi.fn().mockResolvedValue({ model: { id: "gpt-5", providerID: "openai" } }), + }, + tool: { + transform: vi.fn(async (callback) => { + callback({ + add: (tool: typeof registered) => { + registered = tool; + }, + }); + }), + }, + }; + buildOutput.mockResolvedValue({ state: "output", output: "Quota ready" }); + + await plugin.setup(ctx as never); + expect(registered?.name).toBe("quota_status"); + const output = await registered?.execute({ force: true }, { sessionID: "session-test" }); + expect(output).toEqual({ content: "Quota ready" }); + expect(buildOutput).toHaveBeenCalledWith( + expect.objectContaining({ + command: "quota_status", + sessionID: "session-test", + arguments: '{"force":true}', + }), + ); + expect(ctx.provider.list).toHaveBeenCalledTimes(0); + }); +}); diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index edc42306..6953aa17 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -1,519 +1,158 @@ -import { rm } from "fs/promises"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -import { isCommandHandledError } from "../src/lib/command-handled.js"; -import { - createPluginTestClient as createClient, - createConfigModuleMock, - createPluginToolMockModule, - createPricingModuleMock, - createProvidersRegistryModuleMock, - getPromptText, - makeQuotaToastTestConfig, - seedDefaultPluginBootstrapMocks, -} from "./helpers/plugin-test-harness.js"; +import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-commands.js"; +import tuiPlugin from "../src/tui-v2.js"; -const TEST_RUNTIME_ROOT = "/tmp/opencode-quota-plugin-command-boundary-tests"; - -const mocks = vi.hoisted(() => ({ - loadConfig: vi.fn(), - getProviders: vi.fn(), - getPricingSnapshotMeta: vi.fn(), - getPricingSnapshotSource: vi.fn(), - getRuntimePricingRefreshStatePath: vi.fn(), - getRuntimePricingSnapshotPath: vi.fn(), - maybeRefreshPricingSnapshot: vi.fn(), - setPricingSnapshotAutoRefresh: vi.fn(), - setPricingSnapshotSelection: vi.fn(), -})); - -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); - -vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); - -vi.mock("../src/providers/registry.js", () => - createProvidersRegistryModuleMock(mocks.getProviders), -); - -vi.mock("../src/lib/modelsdev-pricing.js", () => createPricingModuleMock(mocks)); - -vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirs: () => ({ - dataDir: `${TEST_RUNTIME_ROOT}/data`, - configDir: `${TEST_RUNTIME_ROOT}/config`, - cacheDir: `${TEST_RUNTIME_ROOT}/cache`, - stateDir: `${TEST_RUNTIME_ROOT}/state`, - }), +const mocks = vi.hoisted(() => ({ build: vi.fn() })); +vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ + ...(await importOriginal()), + buildQuotaDialogCommandOutput: mocks.build, })); -type PluginHooks = { - config?: (input: unknown) => Promise | void; - "command.execute.before"?: (input: { - command: string; - arguments?: string; - sessionID: string; - }) => Promise | void; -}; - -type PluginConfigFixture = { - command?: Record; - agent?: Record; - default_agent?: string; -}; - -const AGENT_NORMALIZATION_CASES = [ - { - name: "exact", - agent: { "\u200Bplanner": {} }, - defaultAgent: "\u200Bplanner", - expectedDefaultAgent: "\u200Bplanner", - }, - { - name: "unique", - agent: { "\u200Bplanner": {}, coder: {} }, - defaultAgent: "planner", - expectedDefaultAgent: "\u200Bplanner", - }, - { - name: "ambiguous", - agent: { "\u200Bplanner": {}, "\u200Cplanner": {} }, - defaultAgent: "planner", - expectedDefaultAgent: "planner", - }, -] as const; - -const PLUGIN_ORDERS = ["quota-first", "remapper-first"] as const; - -async function loadPluginHooks(client: ReturnType): Promise { - const { QuotaToastPlugin } = await import("../src/plugin.js"); - return (await QuotaToastPlugin({ client } as any)) as PluginHooks; -} - -async function expectHandled(promise: Promise | unknown): Promise { - try { - await promise; - } catch (err) { - expect(isCommandHandledError(err)).toBe(true); - return; - } - throw new Error("expected handled sentinel"); -} - -async function runServerCommand(params: { - command: string; - arguments?: string; - client?: ReturnType; - sessionID?: string; -}) { - const client = params.client ?? createClient(); - const hooks = await loadPluginHooks(client); - const commandHook = hooks["command.execute.before"]; - expect(commandHook).toBeDefined(); - - await expectHandled( - commandHook?.({ - command: params.command, - arguments: params.arguments, - sessionID: params.sessionID ?? "session-command", - }), - ); - - return { client, hooks }; -} - -async function buildDialogOutput(params: { - command: "quota" | "pricing_refresh" | "tokens_daily" | "tokens_session_all"; - client: ReturnType; - sessionID?: string; -}) { - const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); - return buildQuotaDialogCommandOutput({ - command: params.command, - client: params.client, - roots: { - workspaceRoot: process.cwd(), - configRoot: process.cwd(), - fallbackDirectory: process.cwd(), +function startTui() { + const commands = new Map< + string, + { run: (input?: unknown) => Promise; slash: { name: string } } + >(); + const listeners = new Map }) => void>(); + const alert = vi.fn().mockResolvedValue(undefined); + const prompt = vi.fn().mockResolvedValue(undefined); + const toast = vi.fn(); + const slots = new Map unknown }>(); + const context = { + location: { directory: process.cwd() }, + client: { session: { get: vi.fn() } }, + data: { + on: vi.fn((name: string, callback: (event: { data?: Record }) => void) => { + listeners.set(name, callback); + return () => listeners.delete(name); + }), }, - sessionID: params.sessionID, - }); + keymap: { + layer: vi.fn( + ( + build: () => { + commands: Array<{ + id: string; + run: (input?: unknown) => Promise; + slash: { name: string }; + }>; + }, + ) => { + for (const command of build().commands) commands.set(command.id, command); + }, + ), + }, + ui: { + slot: vi.fn( + (claim: { append: string; render: (props?: { sessionID: string }) => unknown }) => { + slots.set(claim.append, claim); + if (claim.append === "app") claim.render(); + return vi.fn(); + }, + ), + toast: { show: toast }, + dialog: { alert, prompt, set: vi.fn() }, + }, + }; + const dispose = tuiPlugin.setup(context as never); + return { commands, listeners, alert, prompt, toast, slots, context, dispose }; } -describe("plugin command handled boundary", () => { - beforeEach(async () => { - seedDefaultPluginBootstrapMocks(mocks, { - configOverrides: { enabled: true }, - resetPluginState: true, +describe("V2 CLI command boundary", () => { + beforeEach(() => { + mocks.build.mockReset().mockResolvedValue({ + state: "output", + title: "Quota", + output: "Quota ready", + dialogSize: "large", }); - await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); - const { __resetQuotaStateForTests } = await import("../src/lib/quota-state.js"); - __resetQuotaStateForTests(); }); - afterEach(async () => { - const { __resetQuotaStateForTests } = await import("../src/lib/quota-state.js"); - __resetQuotaStateForTests(); - await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); - }); - - it("registers deterministic slash commands for the server/web command surface", async () => { - const client = createClient(); - const hooks = await loadPluginHooks(client); - const cfg: { command?: Record } = {}; - const { QUOTA_DIALOG_COMMANDS } = await import("../src/lib/quota-dialog-commands.js"); - - await hooks.config?.(cfg as any); - - expect(hooks["command.execute.before"]).toBeDefined(); - expect(cfg.command).toBeDefined(); - expect(QUOTA_DIALOG_COMMANDS).toHaveLength(12); - expect(new Set(QUOTA_DIALOG_COMMANDS.map((spec) => spec.id)).size).toBe(12); - expect(new Set(QUOTA_DIALOG_COMMANDS.map((spec) => spec.slashName)).size).toBe(12); - expect(Object.keys(cfg.command ?? {})).toHaveLength(12); - for (const spec of QUOTA_DIALOG_COMMANDS) { - expect(cfg.command?.[spec.id]).toEqual({ - template: `/${spec.slashName}`, - description: spec.description, - }); - } - expect(client.session.prompt).not.toHaveBeenCalled(); - }); - - for (const order of PLUGIN_ORDERS) { - for (const scenario of AGENT_NORMALIZATION_CASES) { - it(`normalizes the default agent for ${order} ordering with an ${scenario.name} match`, async () => { - const client = createClient(); - const hooks = await loadPluginHooks(client); - const cfg: PluginConfigFixture = {}; - let configAtPrompt: - | { defaultAgent: string | undefined; agentKeys: string[]; namesExistingAgent: boolean } - | undefined; - - if (order === "quota-first" && scenario.name === "unique") { - client.session.prompt.mockImplementationOnce(async () => { - const agentKeys = Object.keys(cfg.agent ?? {}); - configAtPrompt = { - defaultAgent: cfg.default_agent, - agentKeys, - namesExistingAgent: - cfg.default_agent !== undefined && agentKeys.includes(cfg.default_agent), - }; - return {}; - }); - } - - const applyAgentRemap = () => { - cfg.agent = { ...scenario.agent }; - cfg.default_agent = scenario.defaultAgent; - }; - - if (order === "remapper-first") applyAgentRemap(); - await hooks.config?.(cfg); - if (order === "quota-first") applyAgentRemap(); - - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota", - sessionID: `session-agent-order-${order}-${scenario.name}`, - }), - ); - - expect(cfg.default_agent).toBe(scenario.expectedDefaultAgent); - expect(client.session.prompt).toHaveBeenCalledTimes(1); - if (order === "quota-first" && scenario.name === "unique") { - expect(configAtPrompt).toEqual({ - defaultAgent: "\u200Bplanner", - agentKeys: ["\u200Bplanner", "coder"], - namesExistingAgent: true, - }); - } - }); - } - } - - it("leaves non-quota server commands untouched", async () => { - const client = createClient(); - const hooks = await loadPluginHooks(client); - - await expect( - hooks["command.execute.before"]?.({ command: "project_notes", sessionID: "session-other" }), - ).resolves.toBeUndefined(); - - expect(client.session.prompt).not.toHaveBeenCalled(); + it("registers the 12 local slash commands, not V1 server command hooks", () => { + const tui = startTui(); + expect(tui.commands.size).toBe(12); + expect([...tui.commands.values()].map((item) => item.slash.name)).toEqual( + QUOTA_DIALOG_COMMANDS.map((item) => item.slashName), + ); + expect(new Set(QUOTA_DIALOG_COMMANDS.map((item) => item.id)).size).toBe(12); + expect(tui.context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "app" })); + tui.dispose?.(); }); - it("handles server /quota by injecting deterministic output and aborting continuation", async () => { - mocks.getProviders.mockReturnValue([ - { - id: "boom-provider", - isAvailable: vi.fn().mockRejectedValue(new Error("boom")), - fetch: vi.fn(), - }, - ]); - - const { client } = await runServerCommand({ command: "quota", sessionID: "session-2" }); - - expect(client.session.prompt).toHaveBeenCalledTimes(1); - expect(client.session.prompt).toHaveBeenCalledWith( + it("isolates commands from the model transcript and presents quota output locally", async () => { + const tui = startTui(); + await tui.commands.get("quota.quota")?.run(); + expect(mocks.build).toHaveBeenCalledWith( expect.objectContaining({ - path: { id: "session-2" }, - body: expect.objectContaining({ - noReply: true, - parts: [ - expect.objectContaining({ - type: "text", - ignored: true, - }), - ], - }), + command: "quota", + sessionID: undefined, + roots: { fallbackDirectory: process.cwd() }, }), ); - expect(getPromptText(client)).toContain("Quota unavailable"); - expect(getPromptText(client)).toContain("No provider data available"); + expect(tui.alert).toHaveBeenCalledWith({ title: "Quota", message: "Quota ready" }); + expect(tui.context.client.session.get).not.toHaveBeenCalled(); + tui.dispose?.(); }); - it("injects clean plain text with noReply/ignored when /quota has no current model", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeQuotaToastTestConfig({ - enabled: true, - onlyCurrentModel: false, - showSessionTokens: false, - tuiCommandDisplay: "dialog", + it("passes explicit /tokens_between arguments to the deterministic output builder", async () => { + const tui = startTui(); + await tui.commands.get("quota.tokens_between")?.run({ arguments: "not-a-date-range" }); + expect(mocks.build).toHaveBeenCalledWith( + expect.objectContaining({ + command: "tokens_between", + arguments: "not-a-date-range", }), ); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: { - resultType: "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", - }, - name: "OpenAI Weekly", - group: "OpenAI", - label: "Weekly:", - percentRemaining: 80, - }, - ], - errors: [], - }), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient(); - - await runServerCommand({ - command: "quota", - sessionID: "session-zero-model", - client, - }); - - expect(provider.fetch).toHaveBeenCalledTimes(1); - expect(client.session.prompt).toHaveBeenCalledWith({ - path: { id: "session-zero-model" }, - body: { - noReply: true, - parts: [ - { - type: "text", - ignored: true, - text: expect.stringContaining(" Week quota"), - }, - ], - }, - }); - expect(getPromptText(client)).toMatch(/\n {2}Week quota\s+[█░]{10}\s+80% left/u); - expect(getPromptText(client)).toMatch(/^Quota \(\/quota\)/u); - expect(getPromptText(client)).not.toContain("```"); - expect(getPromptText(client)).not.toMatch(/^#{1,6} /mu); - expect(getPromptText(client)).not.toContain("No enabled quota providers matched"); + expect(tui.prompt).not.toHaveBeenCalled(); + tui.dispose?.(); }); - it("handles /tokens_between arguments through one inline injection", async () => { - const { client } = await runServerCommand({ - command: "tokens_between", - arguments: "not-a-date-range", - sessionID: "session-between", - }); - - expect(client.session.prompt).toHaveBeenCalledTimes(1); - expect(getPromptText(client)).toContain("Invalid arguments for /tokens_between"); + it("prompts for missing date ranges and does not invoke the command when cancelled", async () => { + const tui = startTui(); + await tui.commands.get("quota.tokens_between")?.run(); + expect(tui.prompt).toHaveBeenCalledWith( + expect.objectContaining({ placeholder: "YYYY-MM-DD YYYY-MM-DD" }), + ); + expect(mocks.build).not.toHaveBeenCalled(); + tui.dispose?.(); }); - it("injects inline usage output when /tokens_between arguments are missing", async () => { - const { client } = await runServerCommand({ - command: "tokens_between", - sessionID: "session-between-missing", - }); - - expect(client.session.prompt).toHaveBeenCalledTimes(1); - expect(getPromptText(client)).toContain("Invalid arguments for /tokens_between"); - expect(getPromptText(client)).toContain("Expected: /tokens_between YYYY-MM-DD YYYY-MM-DD"); + it("returns without a dialog when quota is disabled", async () => { + mocks.build.mockResolvedValue({ state: "noop", command: "quota", reason: "disabled" }); + const tui = startTui(); + await tui.commands.get("quota.quota")?.run(); + expect(tui.alert).not.toHaveBeenCalled(); + expect(tui.toast).not.toHaveBeenCalled(); + tui.dispose?.(); }); - it("propagates server slash command injection failures instead of throwing handled", async () => { - mocks.getProviders.mockReturnValue([ - { - id: "boom-provider", - isAvailable: vi.fn().mockRejectedValue(new Error("boom")), - fetch: vi.fn(), - }, - ]); - const injectionError = new Error("prompt unavailable"); - const client = createClient(); - client.session.prompt.mockRejectedValueOnce(injectionError); - const hooks = await loadPluginHooks(client); - - await expect( - hooks["command.execute.before"]?.({ command: "quota", sessionID: "session-inject-fails" }), - ).rejects.toBe(injectionError); - - expect(isCommandHandledError(injectionError)).toBe(false); - expect(client.app.log).toHaveBeenCalledWith( + it("shows command failures as sanitized TUI error toasts rather than injecting a message", async () => { + mocks.build.mockRejectedValue(new Error("quota unavailable")); + const tui = startTui(); + await tui.commands.get("quota.quota")?.run(); + expect(tui.toast).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - level: "warn", - message: "Failed to inject raw output", - }), + variant: "error", + message: "quota unavailable", }), ); - }); - - it("still builds deterministic quota dialog output without session.prompt injection", async () => { - const isAvailable = vi.fn().mockRejectedValue(new Error("boom")); - mocks.getProviders.mockReturnValue([ - { - id: "boom-provider", - isAvailable, - fetch: vi.fn(), - }, - ]); - const client = createClient(); - - const result = await buildDialogOutput({ command: "quota", client, sessionID: "session-2" }); - - expect(result.state).toBe("output"); - expect(result.state === "output" ? result.output : "").toContain("Quota unavailable"); - expect(result.state === "output" ? result.output : "").toContain("No provider data available"); - expect(isAvailable).toHaveBeenCalledOnce(); - expect(client.session.prompt).not.toHaveBeenCalled(); - }); - - it("handles disabled deterministic server commands without injecting output", async () => { - mocks.loadConfig.mockResolvedValue(makeQuotaToastTestConfig({ enabled: false })); - const client = createClient(); - - await runServerCommand({ command: "tokens_daily", client, sessionID: "session-disabled" }); - await runServerCommand({ - command: "tokens_session_all", - client, - sessionID: "session-disabled-tree", - }); - - expect(mocks.maybeRefreshPricingSnapshot).not.toHaveBeenCalled(); - expect(client.session.prompt).not.toHaveBeenCalled(); - }); - - it("returns no-op dialog result for disabled deterministic commands", async () => { - mocks.loadConfig.mockResolvedValue(makeQuotaToastTestConfig({ enabled: false })); - const client = createClient(); - - const daily = await buildDialogOutput({ - command: "tokens_daily", - client, - sessionID: "session-disabled", - }); - const tree = await buildDialogOutput({ - command: "tokens_session_all", - client, - sessionID: "session-disabled-tree", - }); - - expect(daily).toEqual({ state: "noop", command: "tokens_daily", reason: "disabled" }); - expect(tree).toEqual({ state: "noop", command: "tokens_session_all", reason: "disabled" }); - expect(mocks.maybeRefreshPricingSnapshot).not.toHaveBeenCalled(); - expect(client.session.prompt).not.toHaveBeenCalled(); - }); - - it("handles server /pricing_refresh by refreshing pricing, injecting output, and aborting continuation", async () => { - mocks.maybeRefreshPricingSnapshot.mockResolvedValue({ - attempted: true, - updated: true, - state: { version: 1, updatedAt: Date.now(), lastResult: "success" }, - }); - - const { client } = await runServerCommand({ - command: "pricing_refresh", - sessionID: "session-pricing-refresh", - }); - - expect(mocks.maybeRefreshPricingSnapshot).toHaveBeenCalledWith({ - reason: "manual", - force: true, - snapshotSelection: "auto", - allowRefreshWhenSelectionBundled: true, - }); - expect(client.session.prompt).toHaveBeenCalledTimes(1); - expect(getPromptText(client)).toContain("Pricing Refresh (/pricing_refresh)"); - }); - - it("still builds /pricing_refresh dialog output without throwing a handled sentinel", async () => { - mocks.maybeRefreshPricingSnapshot.mockResolvedValue({ - attempted: true, - updated: true, - state: { version: 1, updatedAt: Date.now(), lastResult: "success" }, - }); - - const client = createClient(); - - const result = await buildDialogOutput({ - command: "pricing_refresh", - client, - sessionID: "session-pricing-refresh", - }); - - expect(mocks.maybeRefreshPricingSnapshot).toHaveBeenCalledWith({ - reason: "manual", - force: true, - snapshotSelection: "auto", - allowRefreshWhenSelectionBundled: true, - }); - expect(result.state === "output" ? result.output : "").toContain( - "Pricing Refresh (/pricing_refresh)", - ); - expect(client.session.prompt).not.toHaveBeenCalled(); - }); - - it("handles disabled server /pricing_refresh as a no-op", async () => { - mocks.loadConfig.mockResolvedValue(makeQuotaToastTestConfig({ enabled: false })); - const client = createClient(); - - await runServerCommand({ - command: "pricing_refresh", - client, - sessionID: "session-disabled-refresh", - }); - - expect(mocks.maybeRefreshPricingSnapshot).not.toHaveBeenCalled(); - expect(client.session.prompt).not.toHaveBeenCalled(); - }); - - it("treats /pricing_refresh as a dialog no-op when disabled", async () => { - mocks.loadConfig.mockResolvedValue(makeQuotaToastTestConfig({ enabled: false })); - const client = createClient(); - - const result = await buildDialogOutput({ - command: "pricing_refresh", - client, - sessionID: "session-disabled-refresh", - }); - - expect(result).toEqual({ state: "noop", command: "pricing_refresh", reason: "disabled" }); - expect(mocks.maybeRefreshPricingSnapshot).not.toHaveBeenCalled(); - expect(client.session.prompt).not.toHaveBeenCalled(); + expect(tui.alert).not.toHaveBeenCalled(); + tui.dispose?.(); + }); + + it("does not offer V1 web/server slash interception or agent-config normalization", async () => { + // V2 CLI commands are local keymap registrations. The server plugin only provides a tool; + // there is no V2 server command.execute.before hook to inject output into web sessions. + const server = (await import("../src/plugin.js")).default; + const transform = vi.fn(); + await server.setup({ + location: { directory: process.cwd() }, + tool: { transform }, + provider: { list: vi.fn() }, + } as never); + expect(transform).toHaveBeenCalledOnce(); + expect((server as Record)["command.execute.before"]).toBeUndefined(); + expect((server as Record).config).toBeUndefined(); }); }); diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 4c425bd9..bbe7ccc7 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -11,7 +11,6 @@ import { createPricingModuleMock, createProvidersRegistryModuleMock, createQwenAuthModuleMock, - getToastMessage, makeQuotaToastTestConfig, seedDefaultPluginBootstrapMocks, } from "./helpers/plugin-test-harness.js"; @@ -115,7 +114,7 @@ function makeAnnouncementSummary(overrides: Record = {}) { function configureQuestionQuotaToast( overrides: Parameters[0] = {}, ): void { - mocks.loadConfig.mockResolvedValueOnce( + mocks.loadConfig.mockResolvedValue( makeQuotaToastTestConfig({ enabled: true, enableToast: true, @@ -144,14 +143,31 @@ function configureQuestionQuotaToast( ]); } -async function runSuccessfulQuestion( - hooks: Record, - sessionID = "session-question", -): Promise { - await hooks["tool.execute.after"]?.( - { tool: "question", sessionID, callID: `call-${sessionID}` }, - { title: "Question", output: "ok", metadata: { status: "success" } }, - ); +async function startCli() { + const { default: plugin } = await import("../src/tui-v2.js"); + const listeners = new Map }) => void>(); + const toast = vi.fn(); + const context = { + client: { session: { get: vi.fn().mockResolvedValue({ data: {} }) } }, + location: { directory: process.cwd() }, + data: { + on: vi.fn((name: string, listener: (event: { data: Record }) => void) => { + listeners.set(name, listener); + return () => listeners.delete(name); + }), + location: { provider: { list: () => [{ id: "copilot" }] } }, + }, + keymap: { layer: vi.fn() }, + ui: { + slot: vi.fn((claim: { append: string; render: () => unknown }) => { + if (claim.append === "app") claim.render(); + return vi.fn(); + }), + toast: { show: toast }, + }, + }; + const dispose = plugin.setup(context as never); + return { listeners, toast, dispose }; } async function buildAnnouncementsDialogOutput(params: { @@ -210,7 +226,7 @@ describe("maintainer announcement plugin integration", () => { await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); - it("registers and builds the no-arg /quota_announcements deterministic output", async () => { + it("builds the CLI /quota_announcements output from available providers", async () => { const provider = { id: "copilot", isAvailable: vi.fn().mockResolvedValue(true), @@ -218,20 +234,14 @@ describe("maintainer announcement plugin integration", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const { QUOTA_DIALOG_COMMANDS } = await import("../src/lib/quota-dialog-commands.js"); const announcementCommand = QUOTA_DIALOG_COMMANDS.find( (command) => command.id === "quota_announcements", ); const client = createClient(); - const hooks = await QuotaToastPlugin({ client } as any); - const cfg: any = {}; - - await hooks.config?.(cfg); - expect(cfg.command?.quota_announcements).toEqual({ - template: `/${announcementCommand?.slashName}`, - description: announcementCommand?.description, - }); + expect(announcementCommand).toEqual( + expect.objectContaining({ slashName: "quota_announcements" }), + ); const output = await buildAnnouncementsDialogOutput({ client }); @@ -263,9 +273,7 @@ describe("maintainer announcement plugin integration", () => { : makeAnnouncementSummary({ activeCount: 0, activeAnnouncements: [] }); }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); await expect(buildAnnouncementsDialogOutput({ client })).resolves.toBe( "Maintainer announcements\n\nNo current announcements.", @@ -284,9 +292,7 @@ describe("maintainer announcement plugin integration", () => { }), ); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); await expect(buildAnnouncementsDialogOutput({ client })).resolves.toBe( "Maintainer announcements\n\nNo current announcements.", @@ -294,9 +300,7 @@ describe("maintainer announcement plugin integration", () => { }); it("rejects /quota_announcements arguments", async () => { - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); await expect( buildAnnouncementsDialogOutput({ @@ -308,27 +312,38 @@ describe("maintainer announcement plugin integration", () => { ); }); - it("shows one count-only fallback toast after the first visible quota toast without TUI", async () => { + it("shows one count-only announcement toast after the first visible V2 CLI quota toast", async () => { configureQuestionQuotaToast(); - - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const client = createClient(); - const hooks = await QuotaToastPlugin({ client } as any); - - await runSuccessfulQuestion(hooks); - - await flushMaintainerFallbackWork(); - expect(getToastMessage(client, 0)).toContain("Copilot"); - expect(getToastMessage(client, 1)).toBe(ANNOUNCEMENT_TOAST_MESSAGE); - expect(getToastMessage(client, 1)).not.toContain(TEST_ANNOUNCEMENT.message); - expect(getToastMessage(client, 1)).not.toContain(TEST_ANNOUNCEMENT.id); + const cli = await startCli(); + cli.listeners.get("session.tool.input.started")?.({ + data: { id: "call-1", name: "question", sessionID: "session-question" }, + }); + cli.listeners.get("session.tool.success")?.({ + data: { id: "call-1", sessionID: "session-question" }, + }); + await vi.waitFor(() => expect(cli.toast).toHaveBeenCalledTimes(2)); + expect(cli.toast.mock.calls[0]?.[0].message).toContain("Copilot"); + expect(cli.toast.mock.calls[1]?.[0].message).toBe(ANNOUNCEMENT_TOAST_MESSAGE); + expect(cli.toast.mock.calls[1]?.[0].message).not.toContain(TEST_ANNOUNCEMENT.message); + expect(cli.toast.mock.calls[1]?.[0].message).not.toContain(TEST_ANNOUNCEMENT.id); expect(announcementMocks.getMaintainerAnnouncementsSummary).toHaveBeenCalledWith( expect.objectContaining({ enabledProviders: ["copilot"] }), ); + cli.listeners.get("session.tool.input.started")?.({ + data: { id: "call-2", name: "question", sessionID: "session-question" }, + }); + cli.listeners.get("session.tool.success")?.({ + data: { id: "call-2", sessionID: "session-question" }, + }); + await vi.waitFor(() => expect(cli.toast).toHaveBeenCalledTimes(3)); + expect( + cli.toast.mock.calls.filter(([notice]) => notice.message === ANNOUNCEMENT_TOAST_MESSAGE), + ).toHaveLength(1); + cli.dispose?.(); }); - it("does not attempt fallback before or without a visible quota toast", async () => { - mocks.loadConfig.mockResolvedValueOnce( + it("does not announce before or without a visible V2 CLI quota toast", async () => { + mocks.loadConfig.mockResolvedValue( makeQuotaToastTestConfig({ enabled: true, enableToast: true, @@ -341,20 +356,18 @@ describe("maintainer announcement plugin integration", () => { }), ); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const client = createClient(); - const hooks = await QuotaToastPlugin({ client } as any); - - await hooks.event?.({ - event: { type: "session.idle", properties: { sessionID: "session-idle" } }, - } as any); - await hooks["tool.execute.after"]?.( - { tool: "question", sessionID: "session-question", callID: "call-1" }, - { title: "Error", output: "failed", metadata: { status: "error" } }, - ); + const cli = await startCli(); + cli.listeners.get("session.step.ended")?.({ data: { sessionID: "session-idle" } }); + cli.listeners.get("session.tool.input.started")?.({ + data: { id: "call-1", name: "question", sessionID: "session-question" }, + }); + cli.listeners.get("session.tool.failed")?.({ + data: { id: "call-1", sessionID: "session-question" }, + }); + await flushMaintainerFallbackWork(); expect(announcementMocks.getMaintainerAnnouncementsSummary).not.toHaveBeenCalled(); - expect(tuiDiagnosticsMocks.inspectTuiConfig).not.toHaveBeenCalled(); - expect(client.tui.showToast).not.toHaveBeenCalled(); + expect(cli.toast).not.toHaveBeenCalled(); + cli.dispose?.(); }); }); diff --git a/tests/plugin.quota-command.test.ts b/tests/plugin.quota-command.test.ts index dadd3977..faeec624 100644 --- a/tests/plugin.quota-command.test.ts +++ b/tests/plugin.quota-command.test.ts @@ -13,7 +13,6 @@ import { createProvidersRegistryModuleMock, createQwenAuthModuleMock, createSessionTokensModuleMock, - getToastMessage, seedDefaultPluginBootstrapMocks, } from "./helpers/plugin-test-harness.js"; @@ -25,7 +24,7 @@ const TEST_ACCOUNTING = { authority: "provider_reported", } as const; -type DialogCommand = "quota" | "pricing_refresh"; +type DialogCommand = "quota" | "pricing_refresh" | "tokens_between"; async function buildDialogOutput(params: { command?: DialogCommand; @@ -34,6 +33,12 @@ async function buildDialogOutput(params: { arguments?: string; }) { const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); + // V2 CLI obtains host provider IDs from its location cache, not V1 plugin bootstrap. + params.client.config.providers.mockResolvedValue({ + data: { + providers: mocks.getProviders().map((provider: { id: string }) => ({ id: provider.id })), + }, + }); const result = await buildQuotaDialogCommandOutput({ command: params.command ?? "quota", arguments: params.arguments, @@ -57,6 +62,30 @@ async function buildDialogOutput(params: { return result.state === "output" ? result.output : ""; } +async function createV2StatusTool(directory: string) { + const { default: server } = await import("../src/plugin.js"); + let execute: + | ((input: unknown, context: { sessionID: string }) => Promise<{ content: string }>) + | undefined; + await server.setup({ + location: { directory }, + provider: { list: vi.fn().mockResolvedValue({ data: [] }) }, + session: { get: vi.fn().mockResolvedValue({}) }, + tool: { + transform: async ( + register: (editor: { add: (tool: { execute: typeof execute }) => void }) => void, + ) => + register({ + add: (tool) => { + execute = tool.execute; + }, + }), + }, + } as never); + if (!execute) throw new Error("quota_status tool was not registered"); + return execute; +} + const mocks = vi.hoisted(() => ({ loadConfig: vi.fn(), getProviders: vi.fn(), @@ -122,6 +151,7 @@ describe("/quota command behavior", () => { beforeEach(async () => { savedConfigDir = process.env.OPENCODE_CONFIG_DIR; delete process.env.OPENCODE_CONFIG_DIR; + mocks.loadConfig.mockReset(); seedDefaultPluginBootstrapMocks(mocks, { configOverrides: { enabled: true, @@ -145,6 +175,7 @@ describe("/quota command behavior", () => { }); afterEach(async () => { + vi.restoreAllMocks(); if (savedConfigDir !== undefined) process.env.OPENCODE_CONFIG_DIR = savedConfigDir; else delete process.env.OPENCODE_CONFIG_DIR; const { __resetQuotaStateForTests } = await import("../src/lib/quota-state.js"); @@ -152,137 +183,89 @@ describe("/quota command behavior", () => { await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); - it("maps host hooks while keeping commands, shared callbacks, and dispose adapter-owned", async () => { - const handleTrigger = vi.fn().mockResolvedValue(undefined); - const runtimeModule = await import("../src/lib/quota-toast-runtime.js"); - let runtimeDependencies: - | Parameters[0] - | undefined; - vi.spyOn(runtimeModule, "createQuotaToastRuntime").mockImplementation((dependencies) => { - runtimeDependencies = dependencies; - return { handleTrigger }; - }); + it("registers the V2 server diagnostics tool with provider repair and no command hooks", async () => { const dialogModule = await import("../src/lib/quota-dialog-commands.js"); - const buildDialogOutput = vi + const output = vi .spyOn(dialogModule, "buildQuotaDialogCommandOutput") - .mockResolvedValue({ state: "output", output: "adapter output" }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const client = createClient(); - const hooks = await QuotaToastPlugin({ client, directory: process.cwd() } as any); - if (!runtimeDependencies) throw new Error("toast runtime dependencies were not captured"); - - await hooks.config?.({}); - await hooks["command.execute.before"]?.({ - command: "not-quota", - sessionID: "session-command", - arguments: "", - } as any); - await hooks.event?.({ - event: { type: "message.updated", properties: { sessionID: "session-event" } }, - } as any); - await hooks.event?.({ - event: { type: "session.idle", properties: {} }, - } as any); - await hooks["tool.execute.after"]?.( - { tool: "bash", sessionID: "session-tool", callID: "call-bash" }, - {} as any, - ); - expect(handleTrigger).not.toHaveBeenCalled(); - - await hooks.event?.({ - event: { type: "session.idle", properties: { sessionID: "session-idle" } }, - } as any); - await hooks.event?.({ - event: { type: "session.compacted", properties: { sessionID: "session-compact" } }, - } as any); - await hooks["tool.execute.after"]?.( - { tool: "question", sessionID: "session-question", callID: "call-question" }, - {} as any, - ); - expect(handleTrigger.mock.calls).toEqual([ - [{ sessionID: "session-idle", trigger: "session.idle" }], - [{ sessionID: "session-compact", trigger: "session.compacted" }], - [{ sessionID: "session-question", trigger: "question" }], - ]); - - await runtimeDependencies.reconcileDetectedProviders(["openai"]); - expect(mocks.reconcileDetectedProvidersInGlobalConfig).toHaveBeenCalledWith({ - configRootDir: process.cwd(), - detectedProviderIds: ["openai"], + .mockImplementation(async (params) => { + expect(await params.resolveSessionMeta?.("session-status")).toEqual({ + modelID: "gpt-5", + providerID: "openai", + }); + await params.onDetectedProviderIds?.(["openai"]); + return { state: "output", output: "adapter output" }; + }); + const { default: server } = await import("../src/plugin.js"); + let tool: + | { + name: string; + execute: (input: unknown, context: { sessionID: string }) => Promise<{ content: string }>; + } + | undefined; + const ctx = { + location: { directory: process.cwd() }, + tool: { + transform: vi.fn(async (register) => + register({ + add: (value: typeof tool) => { + tool = value; + }, + }), + ), + }, + provider: { list: vi.fn().mockResolvedValue({ data: [] }) }, + session: { get: vi.fn().mockResolvedValue({ model: { providerID: "openai", id: "gpt-5" } }) }, + }; + await server.setup(ctx as never); + expect(tool?.name).toBe("quota_status"); + expect(await tool?.execute({ force: true }, { sessionID: "session-status" })).toEqual({ + content: "adapter output", }); - const tokenError = { sessionID: "session-status", error: "token storage unavailable" }; - runtimeDependencies.setSessionTokenError(tokenError); - await hooks.tool?.quota_status.execute({}, { sessionID: "session-status", metadata: vi.fn() }); - expect(buildDialogOutput).toHaveBeenCalledWith( + expect(output).toHaveBeenCalledWith( expect.objectContaining({ command: "quota_status", - lastSessionTokenError: tokenError, + arguments: '{"force":true}', + sessionID: "session-status", }), ); - - try { - await hooks["command.execute.before"]?.({ - command: "quota", - sessionID: "session-command", - arguments: "", - } as any); - } catch { - // Deterministic commands signal host handling after raw output injection. - } - expect(buildDialogOutput).toHaveBeenCalledWith( - expect.objectContaining({ command: "quota", sessionID: "session-command" }), - ); - expect(handleTrigger).toHaveBeenCalledTimes(3); - - await hooks.dispose?.(); - expect(mocks.disposeQuotaTelemetryOwner).toHaveBeenCalledWith(client); - expect(handleTrigger).toHaveBeenCalledTimes(3); + expect(mocks.reconcileDetectedProvidersInGlobalConfig).toHaveBeenCalledWith({ + configRootDir: process.cwd(), + detectedProviderIds: ["openai"], + }); + expect(ctx.session.get).toHaveBeenCalledWith({ sessionID: "session-status" }); + expect((server as Record)["command.execute.before"]).toBeUndefined(); }); - it("keeps pending runtime work alive when adapter dispose releases only telemetry", async () => { - vi.useFakeTimers(); - try { - mocks.loadConfig.mockResolvedValueOnce({ - ...DEFAULT_CONFIG, - enabled: true, - enabledProviders: ["openai"], - showOnIdle: true, - showOnQuestion: false, - showSessionTokens: false, - }); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi - .fn() - .mockRejectedValueOnce(new Error("temporary failure")) - .mockResolvedValueOnce({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "After dispose", percentRemaining: 69 }], - errors: [], - }), - }; - mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const hooks = await QuotaToastPlugin({ client } as any); - - await hooks.event?.({ - event: { type: "session.idle", properties: { sessionID: "session-dispose" } }, - } as any); - await hooks.dispose?.(); - expect(mocks.disposeQuotaTelemetryOwner).toHaveBeenCalledWith(client); - - await vi.advanceTimersByTimeAsync(3_000); - expect(provider.fetch).toHaveBeenCalledTimes(2); - expect(getToastMessage(client, 1)).toContain("After dispose"); - } finally { - vi.useRealTimers(); - } + it("recovers from a provider failure on the next CLI quota request", async () => { + mocks.loadConfig.mockResolvedValue({ + ...DEFAULT_CONFIG, + enabled: true, + enabledProviders: ["openai"], + minIntervalMs: 0, + showSessionTokens: false, + }); + const provider = { + id: "openai", + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi + .fn() + .mockRejectedValueOnce(new Error("temporary failure")) + .mockResolvedValueOnce({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "After failure", percentRemaining: 69 }], + errors: [], + }), + }; + mocks.getProviders.mockReturnValue([provider]); + const client = createClient(); + await buildDialogOutput({ client, sessionID: "session-retry" }); + const recovered = await buildDialogOutput({ client, sessionID: "session-retry" }); + expect(provider.fetch).toHaveBeenCalledTimes(2); + expect(recovered).toContain("After failure"); }); it("applies pricing snapshot selection from config on first use", async () => { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, pricingSnapshot: { source: "bundled", autoRefresh: 7 }, @@ -291,11 +274,7 @@ describe("/quota command behavior", () => { minIntervalMs: 60_000, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - - await QuotaToastPlugin({ client } as any); - await buildDialogOutput({ client, sessionID: "session-init" }); expect(mocks.loadConfig).toHaveBeenCalledWith( @@ -308,82 +287,47 @@ describe("/quota command behavior", () => { expect(mocks.maybeRefreshPricingSnapshot).not.toHaveBeenCalled(); }); - it("reconciles auth-detected providers through the global config writer in auto mode", async () => { - mocks.loadConfig.mockResolvedValueOnce({ - ...DEFAULT_CONFIG, - enabled: true, - enabledProviders: "auto", - showOnIdle: true, - showOnCompact: false, - showOnQuestion: false, - showSessionTokens: false, - minIntervalMs: 60_000, + it("reconciles auth-detected providers using the V2 server status tool", async () => { + const dialogModule = await import("../src/lib/quota-dialog-commands.js"); + vi.spyOn(dialogModule, "buildQuotaDialogCommandOutput").mockImplementation(async (params) => { + await params.onDetectedProviderIds?.(["openai"]); + return { state: "output", output: "OpenAI quota" }; }); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - entries: [{ accounting: TEST_ACCOUNTING, name: "OpenAI", percentRemaining: 75 }], - errors: [], - }), - }; - mocks.getProviders.mockReturnValue([provider]); - - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const client = createClient(); const projectDirectory = `${TEST_RUNTIME_ROOT}/project`; - const hooks = await QuotaToastPlugin({ client, directory: projectDirectory } as any); - - await hooks.event?.({ - event: { type: "session.idle", properties: { sessionID: "session-auto-provider" } }, - } as any); - + const execute = await createV2StatusTool(projectDirectory); + expect(await execute({}, { sessionID: "session-auto-provider" })).toEqual({ + content: "OpenAI quota", + }); expect(mocks.reconcileDetectedProvidersInGlobalConfig).toHaveBeenCalledWith({ configRootDir: projectDirectory, detectedProviderIds: ["openai"], }); }); - it("keeps quota output working when automatic global config repair and logging fail", async () => { - mocks.loadConfig.mockResolvedValueOnce({ - ...DEFAULT_CONFIG, - enabled: true, - enabledProviders: "auto", - showOnIdle: true, - showOnCompact: false, - showOnQuestion: false, - showSessionTokens: false, - minIntervalMs: 60_000, - }); + it("keeps the V2 status tool working when automatic global config repair fails", async () => { mocks.reconcileDetectedProvidersInGlobalConfig.mockRejectedValueOnce(new Error("disk full")); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - entries: [{ accounting: TEST_ACCOUNTING, name: "OpenAI", percentRemaining: 75 }], - errors: [], - }), - }; - mocks.getProviders.mockReturnValue([provider]); - - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const client = createClient(); - client.app.log.mockRejectedValue(new Error("logging unavailable")); - const hooks = await QuotaToastPlugin({ - client, - directory: `${TEST_RUNTIME_ROOT}/project`, - } as any); - - await hooks.event?.({ - event: { type: "session.idle", properties: { sessionID: "session-repair-failure" } }, - } as any); - - expect(client.tui.showToast).toHaveBeenCalledTimes(1); - expect(getToastMessage(client)).toContain("OpenAI"); + const dialogModule = await import("../src/lib/quota-dialog-commands.js"); + vi.spyOn(dialogModule, "buildQuotaDialogCommandOutput").mockImplementation(async (params) => { + await params.onDetectedProviderIds?.(["openai"]); + return { state: "output", output: "OpenAI quota" }; + }); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + try { + const execute = await createV2StatusTool(`${TEST_RUNTIME_ROOT}/project`); + expect(await execute({}, { sessionID: "session-repair-failure" })).toEqual({ + content: "OpenAI quota", + }); + expect(warn).toHaveBeenCalledWith( + "Failed to add detected providers to global OpenCode config", + expect.any(Error), + ); + } finally { + warn.mockRestore(); + } }); it("honors percentDisplayMode for /quota output", async () => { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, enabledProviders: ["openai"], @@ -404,9 +348,7 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); const injected = await buildDialogOutput({ client, @@ -426,7 +368,7 @@ describe("/quota command behavior", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-15T10:00:00.000Z")); try { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, enabledProviders: ["openai"], @@ -456,9 +398,7 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); const injected = await buildDialogOutput({ client, @@ -473,31 +413,18 @@ describe("/quota command behavior", () => { } }); - it("rewrites default_agent only when one zero-width-normalized key matches", async () => { - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const hooks = await QuotaToastPlugin({ client: createClient() } as any); - - const uniqueMatch = { - agent: { - "\u200Bplanner": {}, - coder: {}, - }, - default_agent: "planner", - }; - - await hooks.config?.(uniqueMatch as any); - expect(uniqueMatch.default_agent).toBe("\u200Bplanner"); - - const ambiguousMatch = { - agent: { - "\u200Bplanner": {}, - "\u200Cplanner": {}, - }, - default_agent: "planner", - }; - - await hooks.config?.(ambiguousMatch as any); - expect(ambiguousMatch.default_agent).toBe("planner"); + it("does not mutate server agent selection from the V2 diagnostics tool", async () => { + const dialogModule = await import("../src/lib/quota-dialog-commands.js"); + vi.spyOn(dialogModule, "buildQuotaDialogCommandOutput").mockResolvedValue({ + state: "output", + output: "Quota ready", + }); + const execute = await createV2StatusTool(process.cwd()); + expect(await execute({}, { sessionID: "session-status" })).toEqual({ content: "Quota ready" }); + // V2 has no server config mutation hook; CLI slash commands do not touch agents. + expect(dialogModule.buildQuotaDialogCommandOutput).toHaveBeenCalledWith( + expect.not.objectContaining({ agent: expect.anything(), default_agent: expect.anything() }), + ); }); it("renders provider errors even when no quota entries are returned", async () => { @@ -514,9 +441,7 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); const injected = await buildDialogOutput({ client, sessionID: "session-errors" }); expect(injected).toContain("Alibaba Coding Plan: Unsupported Alibaba Coding Plan tier: max"); @@ -531,9 +456,7 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "auto", providerID: "cursor" }); - await QuotaToastPlugin({ client } as any); const injected = await buildDialogOutput({ client, sessionID: "session-fetch-failure" }); expect(injected).toContain("Cursor: Failed to read quota data"); @@ -541,7 +464,7 @@ describe("/quota command behavior", () => { }); it("reports explicit cursor providers with no local history as no local usage yet", async () => { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, enabledProviders: ["cursor"], @@ -561,9 +484,7 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "auto", providerID: "cursor" }); - await QuotaToastPlugin({ client } as any); const injected = await buildDialogOutput({ client, sessionID: "session-cursor-empty" }); expect(injected).toContain("Cursor: No local usage yet"); @@ -571,7 +492,7 @@ describe("/quota command behavior", () => { }); it("reports explicit Anthropic providers with local auth but no exposed quota windows", async () => { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, enabledProviders: ["anthropic"], @@ -591,12 +512,10 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "anthropic/claude-sonnet-4-5", providerID: "anthropic", }); - await QuotaToastPlugin({ client } as any); const injected = await buildDialogOutput({ client, sessionID: "session-anthropic-empty" }); expect(injected).toContain( @@ -606,7 +525,7 @@ describe("/quota command behavior", () => { }); it("reports Anthropic no-data guidance in auto mode when it is the only active provider", async () => { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, enabledProviders: "auto", @@ -626,12 +545,10 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "anthropic/claude-sonnet-4-5", providerID: "anthropic", }); - await QuotaToastPlugin({ client } as any); const injected = await buildDialogOutput({ client, sessionID: "session-anthropic-auto-empty" }); expect(injected).toContain( @@ -641,7 +558,7 @@ describe("/quota command behavior", () => { }); it("does not diagnose filtered providers as detected-but-empty when onlyCurrentModel excludes them", async () => { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, onlyCurrentModel: true, @@ -658,9 +575,7 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "openai/gpt-5" }); - await QuotaToastPlugin({ client } as any); const injected = await buildDialogOutput({ client, sessionID: "session-filtered-out" }); @@ -721,15 +636,12 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "model-a", providerID: "shared-provider" }); let currentSession = { data: { model: { id: "model-a", providerID: "shared-provider" } }, }; client.session.get = vi.fn().mockImplementation(async () => currentSession); - await QuotaToastPlugin({ client } as any); - const firstInjected = await buildDialogOutput({ client, sessionID: "session-model-switch", @@ -751,7 +663,7 @@ describe("/quota command behavior", () => { }); it("reuses shared quota-state across /quota sessions when render context matches", async () => { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, onlyCurrentModel: false, @@ -772,9 +684,7 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); const firstOutput = await buildDialogOutput({ client, sessionID: "session-a" }); const secondOutput = await buildDialogOutput({ client, sessionID: "session-b" }); @@ -821,9 +731,7 @@ describe("/quota command behavior", () => { }), ); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - await QuotaToastPlugin({ client } as any); const firstRun = buildDialogOutput({ client, sessionID: "session-a" }); const secondRun = buildDialogOutput({ client, sessionID: "session-b" }); @@ -892,9 +800,7 @@ describe("/quota command behavior", () => { accessToken: "token", }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "qwen-code/qwen3-coder-plus" }); - await QuotaToastPlugin({ client } as any); await buildDialogOutput({ client, sessionID: "session-qwen" }); const latest = await buildDialogOutput({ client, sessionID: "session-qwen" }); @@ -939,9 +845,7 @@ describe("/quota command behavior", () => { tier: "lite", }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "alibaba/qwen3-coder-plus" }); - await QuotaToastPlugin({ client } as any); await buildDialogOutput({ client, sessionID: "session-alibaba" }); const latest = await buildDialogOutput({ client, sessionID: "session-alibaba" }); @@ -973,9 +877,7 @@ describe("/quota command behavior", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "auto", providerID: "cursor" }); - await QuotaToastPlugin({ client } as any); await buildDialogOutput({ client, sessionID: "session-cursor" }); const latest = await buildDialogOutput({ client, sessionID: "session-cursor" }); @@ -985,7 +887,7 @@ describe("/quota command behavior", () => { }); it("runs /pricing_refresh with force=true by default and reports bundled pinning", async () => { - mocks.loadConfig.mockResolvedValueOnce({ + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: true, pricingSnapshot: { source: "bundled", autoRefresh: 7 }, @@ -1004,12 +906,7 @@ describe("/quota command behavior", () => { }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); - await Promise.resolve(); - await Promise.resolve(); - mocks.maybeRefreshPricingSnapshot.mockClear(); const injected = await buildDialogOutput({ command: "pricing_refresh", @@ -1031,14 +928,7 @@ describe("/quota command behavior", () => { }); it("rejects /pricing_refresh arguments", async () => { - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); - - // Force first config load so deferred init completes before our assertion. - await buildDialogOutput({ client, sessionID: "session-warmup" }); - await Promise.resolve(); - mocks.maybeRefreshPricingSnapshot.mockClear(); const injected = await buildDialogOutput({ command: "pricing_refresh", @@ -1051,4 +941,55 @@ describe("/quota command behavior", () => { expect(injected).toContain("Invalid arguments for /pricing_refresh"); expect(injected).toContain("This command does not accept arguments."); }); + + it("reports invalid /tokens_between dates without admitting a session prompt", async () => { + const client = createClient(); + const output = await buildDialogOutput({ + command: "tokens_between", + arguments: "not-a-date-range", + client, + sessionID: "session-between-invalid", + }); + expect(output).toContain("Invalid arguments for /tokens_between"); + expect(client.session.prompt).not.toHaveBeenCalled(); + }); + + it("explains the expected /tokens_between range when arguments are missing", async () => { + const client = createClient(); + const output = await buildDialogOutput({ + command: "tokens_between", + client, + sessionID: "session-between-missing", + }); + expect(output).toContain("Expected: /tokens_between YYYY-MM-DD YYYY-MM-DD"); + expect(client.session.prompt).not.toHaveBeenCalled(); + }); + + it("returns a no-op for disabled token commands without refreshing pricing", async () => { + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: false }); + const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); + const client = createClient(); + const result = await buildQuotaDialogCommandOutput({ + command: "tokens_daily", + client, + roots: { fallbackDirectory: process.cwd() }, + sessionID: "session-disabled", + }); + expect(result).toEqual({ state: "noop", command: "tokens_daily", reason: "disabled" }); + expect(mocks.maybeRefreshPricingSnapshot).not.toHaveBeenCalled(); + expect(client.session.prompt).not.toHaveBeenCalled(); + }); + + it("does not refresh pricing when /pricing_refresh is disabled", async () => { + mocks.loadConfig.mockResolvedValue({ ...DEFAULT_CONFIG, enabled: false }); + const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); + const result = await buildQuotaDialogCommandOutput({ + command: "pricing_refresh", + client: createClient(), + roots: { fallbackDirectory: process.cwd() }, + sessionID: "session-disabled-refresh", + }); + expect(result).toEqual({ state: "noop", command: "pricing_refresh", reason: "disabled" }); + expect(mocks.maybeRefreshPricingSnapshot).not.toHaveBeenCalled(); + }); }); diff --git a/tests/plugin.quota-status-command.test.ts b/tests/plugin.quota-status-command.test.ts index 58711839..e7b738e0 100644 --- a/tests/plugin.quota-status-command.test.ts +++ b/tests/plugin.quota-status-command.test.ts @@ -243,7 +243,7 @@ describe("/quota_status command behavior", () => { ); }); - it("passes every registered provider through the shared fetch-once status flow", async () => { + it("passes every registered provider through the shared fetch-once CLI status flow", async () => { const openai = { id: "openai", isAvailable: vi.fn().mockResolvedValue(true), @@ -266,9 +266,7 @@ describe("/quota_status command behavior", () => { }; mocks.getProviders.mockReturnValue([openai, synthetic, copilot, cursor]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - await QuotaToastPlugin({ client } as any); const output = await buildQuotaStatusDialogOutput({ client, @@ -325,12 +323,10 @@ describe("/quota_status command behavior", () => { expect(output).toBe("Injected quota status"); }); - it("reports no_session diagnostics when no active TUI session is available", async () => { + it("reports no_session diagnostics when the CLI has no active session", async () => { mocks.getProviders.mockReturnValue([]); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - await QuotaToastPlugin({ client } as any); const output = await buildQuotaStatusDialogOutput({ client, diff --git a/tests/plugin.qwen-hook.test.ts b/tests/plugin.qwen-hook.test.ts index 07b8c74c..0b866e33 100644 --- a/tests/plugin.qwen-hook.test.ts +++ b/tests/plugin.qwen-hook.test.ts @@ -1,78 +1,57 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - createAlibabaAuthModuleMock, - createPluginTestClient as createClient, - createConfigModuleMock, - createPluginToolMockModule, - createPricingModuleMock, - createQwenAuthModuleMock, - seedDefaultPluginBootstrapMocks, -} from "./helpers/plugin-test-harness.js"; - -const mocks = vi.hoisted(() => ({ - loadConfig: vi.fn(), - resolveQwenLocalPlanCached: vi.fn(), - resolveAlibabaCodingPlanAuthCached: vi.fn(), - getPricingSnapshotMeta: vi.fn(), - getPricingSnapshotSource: vi.fn(), - getRuntimePricingRefreshStatePath: vi.fn(), - getRuntimePricingSnapshotPath: vi.fn(), - maybeRefreshPricingSnapshot: vi.fn(), - setPricingSnapshotAutoRefresh: vi.fn(), - setPricingSnapshotSelection: vi.fn(), +const resolveQuotaRuntimeContext = vi.hoisted(() => vi.fn()); +vi.mock("../src/lib/quota-runtime-context.js", async (importOriginal) => ({ + ...(await importOriginal()), + resolveQuotaRuntimeContext, })); -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); -vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); -vi.mock("../src/lib/opencode-auth.js", () => ({ - readAuthFileCached: vi.fn(), - readAuthFile: vi.fn(), - getCredentialDatabasePath: vi.fn(() => "/tmp/opencode.db"), - getCredentialDatabasePaths: vi.fn(() => ["/tmp/opencode.db"]), - clearReadAuthFileCacheForTests: vi.fn(), -})); -vi.mock("../src/lib/qwen-auth.js", () => - createQwenAuthModuleMock(mocks.resolveQwenLocalPlanCached), -); -vi.mock("../src/lib/alibaba-auth.js", () => - createAlibabaAuthModuleMock(mocks.resolveAlibabaCodingPlanAuthCached), -); -vi.mock("../src/lib/modelsdev-pricing.js", () => createPricingModuleMock(mocks)); +import plugin from "../src/tui-v2.tsx"; -const { QuotaToastPlugin } = await import("../src/plugin.js"); +describe("V2 CLI question-tool accounting boundary", () => { + const handlers = new Map }) => void>(); + const client = { session: { get: vi.fn() } }; + const toast = vi.fn(); -describe("plugin question hook accounting boundary", () => { beforeEach(() => { - seedDefaultPluginBootstrapMocks(mocks, { - configOverrides: { showOnQuestion: false }, + handlers.clear(); + client.session.get.mockReset(); + toast.mockReset(); + resolveQuotaRuntimeContext.mockReset().mockResolvedValue({ + config: { enabled: true, enableToast: true, showOnQuestion: false }, }); + plugin.setup({ + client, + data: { + on: (name: string, handler: (event: { data: Record }) => void) => { + handlers.set(name, handler); + return () => handlers.delete(name); + }, + }, + keymap: { layer: vi.fn() }, + ui: { + slot: (claim: { append: string; render: () => unknown }) => { + if (claim.append === "app") claim.render(); + return vi.fn(); + }, + toast: { show: toast }, + }, + } as never); }); it("does not treat a successful question-tool execution as a completed model request", async () => { - const client = createClient({ modelID: "qwen3-coder-plus", providerID: "qwen-code" }); - const hooks = await QuotaToastPlugin({ client } as any); - - await hooks["tool.execute.after"]?.( - { tool: "question", sessionID: "session-1", callID: "call-1" }, - { title: "Question", output: "ok", metadata: { status: "success" } }, - ); - + handlers.get("session.tool.input.started")?.({ data: { name: "question", id: "call-1" } }); + handlers.get("session.tool.success")?.({ data: { sessionID: "session-1", id: "call-1" } }); + await vi.waitFor(() => expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1)); expect(client.session.get).not.toHaveBeenCalled(); - expect(mocks.resolveQwenLocalPlanCached).not.toHaveBeenCalled(); - expect(mocks.resolveAlibabaCodingPlanAuthCached).not.toHaveBeenCalled(); + expect(toast).not.toHaveBeenCalled(); }); - it("does not use question-tool failure metadata as accounting authority", async () => { - const client = createClient({ modelID: "qwen3-coder-plus", providerID: "qwen-code" }); - const hooks = await QuotaToastPlugin({ client } as any); - - await hooks["tool.execute.after"]?.( - { tool: "question", sessionID: "session-1", callID: "call-2" }, - { title: "Error", output: "failed", metadata: { status: "error", error: "boom" } }, - ); - + it("does not use question-tool failure metadata as accounting authority", () => { + handlers.get("session.tool.input.started")?.({ data: { name: "question", id: "call-2" } }); + handlers.get("session.tool.failed")?.({ data: { sessionID: "session-1", id: "call-2" } }); + expect(resolveQuotaRuntimeContext).not.toHaveBeenCalled(); expect(client.session.get).not.toHaveBeenCalled(); - expect(mocks.resolveQwenLocalPlanCached).not.toHaveBeenCalled(); + expect(toast).not.toHaveBeenCalled(); }); }); diff --git a/tests/plugin.tokens-session-all.test.ts b/tests/plugin.tokens-session-all.test.ts index dba314f3..88b82cda 100644 --- a/tests/plugin.tokens-session-all.test.ts +++ b/tests/plugin.tokens-session-all.test.ts @@ -4,7 +4,6 @@ import { createAlibabaAuthModuleMock, createPluginTestClient as createClient, createConfigModuleMock, - createPluginToolMockModule, createPricingModuleMock, createProvidersRegistryModuleMock, createQwenAuthModuleMock, @@ -41,8 +40,6 @@ const mocks = vi.hoisted(() => ({ }, })); -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); - vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); vi.mock("../src/providers/registry.js", () => @@ -121,27 +118,35 @@ describe("/tokens_session_all command", () => { ]); }); - it("registers /tokens_session_all in server plugin config", async () => { - const { QuotaToastPlugin } = await import("../src/plugin.js"); + it("registers /tokens_session_all as a V2 CLI slash command", async () => { + const { default: plugin } = await import("../src/tui-v2.js"); const { QUOTA_DIALOG_COMMANDS } = await import("../src/lib/quota-dialog-commands.js"); const tokensSessionAllCommand = QUOTA_DIALOG_COMMANDS.find( (command) => command.id === "tokens_session_all", ); - const hooks = await QuotaToastPlugin({ client: createClient() } as any); - const cfg: { command?: Record } = {}; - - await hooks.config?.(cfg as any); + let commands: Array<{ slash: { name: string }; title: string }> = []; + plugin.setup({ + keymap: { + layer: (build: () => { commands: typeof commands }) => { + commands = build().commands; + }, + }, + data: { on: () => () => {} }, + ui: { + slot: (claim: { append: string; render: () => void }) => { + if (claim.append === "app") claim.render(); + return () => {}; + }, + }, + } as never); - expect(cfg.command?.tokens_session_all).toEqual({ - template: `/${tokensSessionAllCommand?.slashName}`, - description: tokensSessionAllCommand?.description, - }); + expect( + commands.find((command) => command.slash.name === tokensSessionAllCommand?.slashName), + ).toEqual(expect.objectContaining({ title: tokensSessionAllCommand?.title })); }); it("aggregates the current session tree for /tokens_session_all", async () => { - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); const output = await buildTokenDialogOutput({ command: "tokens_session_all", @@ -183,9 +188,7 @@ describe("/tokens_session_all command", () => { }); it("keeps /tokens_session scoped to the selected session only", async () => { - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); await buildTokenDialogOutput({ command: "tokens_session", @@ -219,9 +222,7 @@ describe("/tokens_session_all command", () => { new mocks.SessionNotFoundError("ses_missing", "/tmp/opencode.db"), ); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); const injected = await buildTokenDialogOutput({ command: "tokens_session_all", @@ -239,9 +240,7 @@ describe("/tokens_session_all command", () => { new mocks.SessionNotFoundError("ses_parent", "/tmp/opencode.db"), ); - const { QuotaToastPlugin } = await import("../src/plugin.js"); const client = createClient(); - await QuotaToastPlugin({ client } as any); const injected = await buildTokenDialogOutput({ command: "tokens_session", diff --git a/tests/provider-surfaces.google-antigravity.test.ts b/tests/provider-surfaces.google-antigravity.test.ts index 4579a3b4..6210fa53 100644 --- a/tests/provider-surfaces.google-antigravity.test.ts +++ b/tests/provider-surfaces.google-antigravity.test.ts @@ -1,16 +1,16 @@ import { rm } from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { isCommandHandledError } from "../src/lib/command-handled.js"; +import { formatQuotaRows } from "../src/lib/format.js"; +import { formatQuotaCommand } from "../src/lib/quota-command-format.js"; +import { formatQuotaRowsGrouped } from "../src/lib/toast-format-grouped.js"; +import { buildCompactQuotaStatusLine } from "../src/lib/tui-compact-format.js"; +import { buildSidebarQuotaPanelLines } from "../src/lib/tui-sidebar-format.js"; import { createConfigModuleMock, createPluginRuntimePathsMockModule, - createPluginTestClient, - createPluginToolMockModule, createPricingModuleMock, createProvidersRegistryModuleMock, - getPromptText, - getToastMessage, makeQuotaToastTestConfig, seedDefaultPluginBootstrapMocks, } from "./helpers/plugin-test-harness.js"; @@ -32,7 +32,6 @@ const mocks = vi.hoisted(() => ({ queryGoogleQuota: vi.fn(), })); -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); vi.mock("../src/lib/config.js", async (importOriginal) => ({ ...(await importOriginal()), ...createConfigModuleMock(mocks.loadConfig), @@ -66,15 +65,6 @@ vi.mock("../src/lib/google-antigravity-companion.js", () => ({ })), })); -type PluginHooks = { - dispose?: () => Promise | void; - event?: (input: unknown) => Promise | void; - "command.execute.before"?: (input: { - command: string; - sessionID: string; - }) => Promise | void; -}; - function createConfig() { return makeQuotaToastTestConfig({ enabled: true, @@ -106,68 +96,29 @@ function createConfig() { }); } -async function expectHandled(value: unknown): Promise { - try { - await Promise.resolve(value); - } catch (error) { - expect(isCommandHandledError(error)).toBe(true); - return; - } - throw new Error("Expected the handled command sentinel"); -} - function expectNoProviderMisattribution(output: string): void { expect(output).toContain("Antigravity"); expect(output).not.toContain("Google Antigravity"); - expect(output).not.toMatch(/\bClaude\b/u); expect(output).not.toMatch(/Anthropic|subscription/iu); } async function collectSurfaceOutputs() { - const client = createPluginTestClient({ - modelID: "google/antigravity-claude", - providerID: "google", - }); - client.config.providers.mockResolvedValue({ - data: { providers: [{ id: "google" }] }, - }); - - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const hooks = (await QuotaToastPlugin({ client } as never)) as PluginHooks; - - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota", - sessionID: "antigravity-session", - }), - ); - const command = getPromptText(client); - - await hooks.event?.({ - event: { - type: "session.idle", - properties: { sessionID: "antigravity-session" }, - }, - }); - const toast = getToastMessage(client); - - const tuiApi = { - state: { - provider: [{ id: "google" }], - path: { worktree: process.cwd(), directory: process.cwd() }, - session: { messages: () => [] }, - }, - client, - } as never; - const { loadTuiSessionQuotaSurfaces } = await import("../src/lib/tui-runtime.js"); - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: tuiApi, - sessionID: "antigravity-session", - }); - const sidebar = [...surfaces.sidebar.lines, ...(surfaces.sidebar.linesExpanded ?? [])].join("\n"); - const compact = surfaces.compact.status === "ready" ? surfaces.compact.text : ""; - - return { command, toast, sidebar, compact, surfaces, hooks }; + const result = await provider.fetch({ config: createConfig() } as never); + const data = { entries: result.entries, errors: result.errors }; + const formatConfig = { + formatStyle: "singleWindow" as const, + percentDisplayMode: "remaining" as const, + }; + return { + command: formatQuotaCommand({ ...data, generatedAtMs: 0 }), + cli: formatQuotaRows({ ...data, version: "test", style: "allWindows" }), + toast: formatQuotaRowsGrouped(data), + sidebar: buildSidebarQuotaPanelLines({ + data, + config: { ...formatConfig, formatStyle: "allWindows" }, + }).join("\n"), + compact: buildCompactQuotaStatusLine({ data, maxWidth: 240 }), + }; } describe("Google Antigravity provider surfaces", () => { @@ -213,23 +164,18 @@ describe("Google Antigravity provider surfaces", () => { await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); - it("keeps two same-family accounts distinct without redundant family wording", async () => { - const { command, toast, sidebar, compact, surfaces, hooks } = await collectSurfaceOutputs(); + it("keeps same-family accounts distinct and verifies the compact line", async () => { + const outputs = await collectSurfaceOutputs(); - for (const output of [command, toast, sidebar, compact]) { + for (const output of Object.values(outputs)) { expectNoProviderMisattribution(output); expect(output).toContain("Antigravity (ali…)"); expect(output).toContain("Antigravity (bob…)"); } - expect(command.match(/\n {2}Quota\s/gu)).toHaveLength(2); - expect(toast).not.toMatch(/\n(?:5h|7d|Weekly)\s/u); - expect(surfaces.sidebar.status).toBe("ready"); - expect(sidebar.match(/\nQuota\s/gu)).toHaveLength(2); - expect(surfaces.compact.status).toBe("ready"); - expect(compact).toBe("Antigravity (ali…) 0% reset | Antigravity (bob…) 0% reset"); - expect(mocks.queryGoogleQuota).toHaveBeenCalledTimes(1); - - await hooks.dispose?.(); + expect(outputs.compact).toBe( + "Antigravity (ali…): Claude 0% reset | Antigravity (bob…): Claude 0% reset", + ); + expect(mocks.queryGoogleQuota).toHaveBeenCalled(); }); it("keeps family names when one account returns multiple families", async () => { @@ -252,13 +198,13 @@ describe("Google Antigravity provider surfaces", () => { errors: [], }); - const { command, toast, sidebar, compact, hooks } = await collectSurfaceOutputs(); - for (const output of [command, toast, sidebar, compact]) { + const outputs = await collectSurfaceOutputs(); + for (const output of [outputs.command, outputs.cli, outputs.toast, outputs.compact]) { expect(output).toMatch(/\bClaude\b/u); expect(output).toMatch(/\bG3Pro\b/u); } - - await hooks.dispose?.(); + expect(outputs.sidebar).toMatch(/\bClaude\b/u); + expect(outputs.sidebar).toMatch(/\bG3Pro\b/u); }); it("keeps family names when accounts return different singleton families", async () => { @@ -281,16 +227,20 @@ describe("Google Antigravity provider surfaces", () => { errors: [], }); - const { command, toast, sidebar, compact, hooks } = await collectSurfaceOutputs(); - for (const output of [command, toast, sidebar, compact]) { - expect(output).toMatch(/Antigravity \(ali…\).*Claude/su); - expect(output).toMatch(/Antigravity \(bob…\).*G3Pro/su); + const outputs = await collectSurfaceOutputs(); + for (const output of Object.values(outputs)) { + expect(output).toContain("Antigravity (ali…)"); + expect(output).toContain("Antigravity (bob…)"); } - - await hooks.dispose?.(); + for (const output of [outputs.command, outputs.cli, outputs.toast, outputs.compact]) { + expect(output).toMatch(/Antigravity \(ali…\)[\s\S]*Claude/u); + expect(output).toMatch(/Antigravity \(bob…\)[\s\S]*G3Pro/u); + } + expect(outputs.sidebar).toMatch(/Antigravity \(ali…\)[\s\S]*Claude/u); + expect(outputs.sidebar).toMatch(/Antigravity \(bob…\)[\s\S]*G3Pro/u); }); - it("preserves collision-safe account labels while hiding a shared family", async () => { + it("preserves collision-safe account labels for a shared family", async () => { mocks.queryGoogleQuota.mockResolvedValue({ success: true, models: [ @@ -310,14 +260,14 @@ describe("Google Antigravity provider surfaces", () => { errors: [], }); - const { command, toast, sidebar, compact, hooks } = await collectSurfaceOutputs(); - for (const output of [command, toast, sidebar, compact]) { + const outputs = await collectSurfaceOutputs(); + for (const output of Object.values(outputs)) { expectNoProviderMisattribution(output); expect(output).toContain("Antigravity (alice… 1)"); expect(output).toContain("Antigravity (alice… 2)"); } - expect(compact).toBe("Antigravity (alice… 1) 64% | Antigravity (alice… 2) 37%"); - - await hooks.dispose?.(); + expect(outputs.compact).toBe( + "Antigravity (alice… 1): Claude 64% | Antigravity (alice… 2): Claude 37%", + ); }); }); diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 84e7d2cb..4969411d 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -1,16 +1,11 @@ import { rm } from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { isCommandHandledError } from "../src/lib/command-handled.js"; import { createConfigModuleMock, createPluginRuntimePathsMockModule, - createPluginTestClient, - createPluginToolMockModule, createPricingModuleMock, createProvidersRegistryModuleMock, - getPromptText, - getToastMessage, makeQuotaToastTestConfig, seedDefaultPluginBootstrapMocks, } from "./helpers/plugin-test-harness.js"; @@ -36,7 +31,6 @@ const mocks = vi.hoisted(() => ({ queryOpenCodeGoQuota: vi.fn(), })); -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); vi.mock("../src/lib/config.js", async (importOriginal) => ({ ...(await importOriginal()), ...createConfigModuleMock(mocks.loadConfig), @@ -61,15 +55,6 @@ vi.mock("../src/lib/opencode-go.js", () => ({ queryOpenCodeGoQuota: mocks.queryOpenCodeGoQuota, })); -type PluginHooks = { - dispose?: () => Promise | void; - event?: (input: unknown) => Promise | void; - "command.execute.before"?: (input: { - command: string; - sessionID: string; - }) => Promise | void; -}; - function createConfig() { return makeQuotaToastTestConfig({ enabled: true, @@ -125,22 +110,50 @@ function successfulResult() { }; } -async function expectHandled(value: unknown): Promise { - try { - await Promise.resolve(value); - } catch (error) { - expect(isCommandHandledError(error)).toBe(true); - return; - } - throw new Error("Expected the handled command sentinel"); +async function runQuotaStatus(sessionID: string): Promise { + let quotaTool: + | { execute: (input: unknown, context: { sessionID: string }) => Promise<{ content: string }> } + | undefined; + const { default: plugin } = await import("../src/plugin.js"); + await plugin.setup({ + location: { directory: process.cwd() }, + provider: { list: vi.fn().mockResolvedValue({ data: [{ id: "opencode-go" }] }) }, + session: { + get: vi.fn().mockResolvedValue({ model: { id: "model", providerID: "opencode-go" } }), + }, + tool: { + transform: async (callback: (editor: { add: (tool: typeof quotaTool) => void }) => void) => + callback({ + add: (tool) => { + quotaTool = tool; + }, + }), + }, + } as never); + if (!quotaTool) throw new Error("V2 quota_status tool was not registered"); + return (await quotaTool.execute({}, { sessionID })).content; +} + +async function collectQuotaProjection(): Promise { + const { collectQuotaRenderData } = await import("../src/lib/quota-render-data.js"); + const result = await collectQuotaRenderData({ + client: {} as never, + config: createConfig(), + providers: [provider], + formatStyle: "allWindows", + surfaceExplicitProviderIssues: true, + bypassProviderCache: true, + includeAllWindowsData: true, + }); + return JSON.stringify(result.allWindowsData ?? result.data); } function expectCanonicalPercentOrder(output: string): void { expect(output).toContain("OpenCode Go"); const positions = [ - output.lastIndexOf("88%"), - output.lastIndexOf("55%"), - output.lastIndexOf("20%"), + output.lastIndexOf('"percentRemaining":88'), + output.lastIndexOf('"percentRemaining":55'), + output.lastIndexOf('"percentRemaining":20'), ]; expect( positions.every((position) => position >= 0), @@ -191,67 +204,12 @@ describe("OpenCode Go shared projections", () => { }); it("keeps canonical all-window values and the five-hour prompt entry on every surface", async () => { - const client = createPluginTestClient({ - modelID: "opencode-go/model", - providerID: "opencode-go", - }); - client.config.providers.mockResolvedValue({ - data: { providers: [{ id: "opencode-go" }] }, - }); - - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const hooks = (await QuotaToastPlugin({ client } as never)) as PluginHooks; - - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota", - sessionID: "opencode-go-session", - }), - ); - const command = getPromptText(client); - - await hooks.event?.({ - event: { - type: "session.idle", - properties: { sessionID: "opencode-go-session" }, - }, - }); - const toast = getToastMessage(client); - - const { loadTuiSessionQuotaSurfaces } = await import("../src/lib/tui-runtime.js"); - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [{ id: "opencode-go" }], - path: { worktree: process.cwd(), directory: process.cwd() }, - session: { messages: () => [] }, - }, - client, - } as never, - sessionID: "opencode-go-session", - }); - const sidebar = [...surfaces.sidebar.lines, ...(surfaces.sidebar.linesExpanded ?? [])].join( - "\n", - ); - const compact = surfaces.compact.status === "ready" ? surfaces.compact.text : ""; - - for (const output of [command, toast, sidebar, compact]) { - expectCanonicalPercentOrder(output); - expect(output).not.toContain(TEST_TOKEN); - } - expect(surfaces.promptBar).toMatchObject({ - status: "ready", - entry: { - name: "OpenCode Go 5h", - percentRemaining: 88, - resetTimeIso: "2026-08-12T12:30:00.000Z", - accounting: { acquisitionMethod: "remote_api" }, - }, - percentDisplayMode: "remaining", - }); - expect(JSON.stringify(surfaces)).not.toContain(TEST_TOKEN); - - await hooks.dispose?.(); + const status = await runQuotaStatus("opencode-go-session"); + const output = await collectQuotaProjection(); + expectCanonicalPercentOrder(output); + expect(output).not.toContain(TEST_TOKEN); + expect(status).toContain("live_entry_1: 5h: percent_remaining=88"); + expect(status).not.toContain(TEST_TOKEN); }); it("hides a not-subscribed result on every display and keeps it visible in safe diagnostics", async () => { @@ -261,83 +219,26 @@ describe("OpenCode Go shared projections", () => { notSubscribed: true, retryable: false, }); - const client = createPluginTestClient({ - modelID: "opencode-go/model", - providerID: "opencode-go", - }); - client.config.providers.mockResolvedValue({ - data: { providers: [{ id: "opencode-go" }] }, - }); - - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const hooks = (await QuotaToastPlugin({ client } as never)) as PluginHooks; - - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota", - sessionID: "opencode-go-no-subscription", - }), - ); - const command = getPromptText(client); - - await hooks.event?.({ - event: { - type: "session.idle", - properties: { sessionID: "opencode-go-no-subscription" }, - }, - }); - - const { loadTuiSessionQuotaSurfaces } = await import("../src/lib/tui-runtime.js"); - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [{ id: "opencode-go" }], - path: { worktree: process.cwd(), directory: process.cwd() }, - session: { messages: () => [] }, - }, - client, - } as never, - sessionID: "opencode-go-no-subscription", - }); - const sidebar = [...surfaces.sidebar.lines, ...(surfaces.sidebar.linesExpanded ?? [])].join( - "\n", - ); - const compact = surfaces.compact.status === "ready" ? surfaces.compact.text : ""; - - for (const output of [command, getToastMessage(client), sidebar, compact]) { - expect(output).not.toContain("EntitlementError"); - expect(output).not.toContain("not subscribed"); - expect(output).not.toContain(TEST_TOKEN); - } - expect(client.tui.showToast).not.toHaveBeenCalled(); + const output = await runQuotaStatus("opencode-go-no-subscription"); + expect(output).not.toContain("EntitlementError"); + expect(output).not.toContain(TEST_TOKEN); + expect(output).toContain("opencode_go:"); const { buildQuotaExport } = await import("../src/lib/quota-export.js"); const { createRuntimeProviderIdResolver } = await import("../src/lib/runtime-provider-ids.js"); const exportData = await buildQuotaExport({ providers: [provider], ctx: { - client, + client: {} as never, config: createConfig(), - resolveRuntimeProviderIds: createRuntimeProviderIdResolver(client), + resolveRuntimeProviderIds: createRuntimeProviderIdResolver({} as never), } as never, ttlMs: 60_000, fromCache: true, }); expect(exportData.providers["opencode-go"]).toEqual({ status: "unavailable" }); - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota_status", - sessionID: "opencode-go-no-subscription", - }), - ); - const status = getPromptText(client, 1); - expect(status).toContain("opencode_go_state"); - expect(status).toContain("not_subscribed"); - expect(status).not.toContain(TEST_TOKEN); expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledTimes(1); - - await hooks.dispose?.(); }); it("selects the most constrained window and preserves accounting through projection", async () => { diff --git a/tests/providers.alibaba-token-plan.test.ts b/tests/providers.alibaba-token-plan.test.ts index e21d8d34..ca92644a 100644 --- a/tests/providers.alibaba-token-plan.test.ts +++ b/tests/providers.alibaba-token-plan.test.ts @@ -25,6 +25,14 @@ vi.mock("../src/lib/alibaba-token-plan.js", async () => { }; }); +vi.mock("../src/lib/qwen-local-quota.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readAlibabaCodingPlanQuotaState: vi.fn(actual.readAlibabaCodingPlanQuotaState), + }; +}); + const accounting = { resultType: "quota" as const, acquisitionMethod: "local_cli" as const, @@ -167,6 +175,12 @@ describe("alibaba-token-plan provider", () => { const tokenOut = await alibabaTokenPlanProvider.fetch({ config: {} } as any); expectAttemptedWithErrorLabel(tokenOut, "Alibaba Personal Token Plan"); + const { readAlibabaCodingPlanQuotaState } = await import("../src/lib/qwen-local-quota.js"); + vi.mocked(readAlibabaCodingPlanQuotaState).mockResolvedValue({ + version: 1, + recent: [], + updatedAt: Date.now(), + }); const codingOut = await alibabaCodingPlanProvider.fetch({ config: { quotaProviders: [] }, } as any); diff --git a/tests/quota-surface-parity.test.ts b/tests/quota-surface-parity.test.ts index a6c1addf..2e5fd470 100644 --- a/tests/quota-surface-parity.test.ts +++ b/tests/quota-surface-parity.test.ts @@ -225,9 +225,6 @@ describe("quota surface parity regressions", () => { sessionMeta: { modelID: "synthetic/default", providerID: "synthetic" }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - await QuotaToastPlugin({ client } as any); - const quotaOutput = await buildQuotaDialogOutputText({ client, sessionID: "session-worktree-root-parity", @@ -329,9 +326,6 @@ describe("quota surface parity regressions", () => { sessionMeta: { modelID: "synthetic/default", providerID: "synthetic" }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - await QuotaToastPlugin({ client } as any); - await expect( buildQuotaDialogOutputText({ client, @@ -434,9 +428,6 @@ describe("quota surface parity regressions", () => { sessionMeta: { modelID: "openai/gpt-5", providerID: "openai" }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - await QuotaToastPlugin({ client } as any); - const quotaOutput = await buildQuotaDialogOutputText({ client, sessionID: "session-layered-provider-override", @@ -525,9 +516,6 @@ describe("quota surface parity regressions", () => { sessionMeta: { modelID: "synthetic/default", providerID: "synthetic" }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - await QuotaToastPlugin({ client } as any); - const quotaOutput = await buildQuotaDialogOutputText({ client, sessionID: "session-synthetic-parity", @@ -615,9 +603,6 @@ describe("quota surface parity regressions", () => { sessionMeta: { modelID: "openai/gpt-5", providerID: "openai" }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - await QuotaToastPlugin({ client } as any); - const quotaOutput = await buildQuotaDialogOutputText({ client, sessionID: "session-openai-style-divergence", diff --git a/tests/release-gates.test.ts b/tests/release-gates.test.ts index eee65d7b..e73d50c0 100644 --- a/tests/release-gates.test.ts +++ b/tests/release-gates.test.ts @@ -121,7 +121,7 @@ describe("v4 release gates", () => { const typescript = run(typescriptScript); expect(typescript.status).toBe(0); expect(typescript.stdout).toContain( - "TypeScript 7.0.2 and @opencode-ai/plugin 1.18.1 lock entries verified", + "TypeScript 7.0.2 and @opencode/plugin 2.0.15 lock entries verified", ); const historyRepo = path.join(tempDir, "clean-history"); diff --git a/tests/tui-dist-packaging.test.ts b/tests/tui-dist-packaging.test.ts index ac17945a..eeea3778 100644 --- a/tests/tui-dist-packaging.test.ts +++ b/tests/tui-dist-packaging.test.ts @@ -36,7 +36,7 @@ const packagedTui = await import("../dist/tui.js"); describe("tui dist packaging", () => { it("loads the conventional root TUI entrypoint as the existing local plugin", async () => { - const [local, source] = await Promise.all([import("../tui.js"), import("../src/index.js")]); + const [local, source] = await Promise.all([import("../tui.js"), import("../src/tui-v2.js")]); expect(local.default).toBe(source.default); expect(local.default).toMatchObject({ id: "@slkiser/opencode-quota" }); @@ -45,32 +45,21 @@ describe("tui dist packaging", () => { it("ships the precompiled TUI entry and removes stale jsx artifacts", async () => { const distTui = new URL("../dist/tui.js", import.meta.url); - const distTuiV2 = new URL("../dist/tui-v2.js", import.meta.url); const distJsx = new URL("../dist/tui.jsx", import.meta.url); const distJsxMap = new URL("../dist/tui.jsx.map", import.meta.url); - const distTuiV2Jsx = new URL("../dist/tui-v2.jsx", import.meta.url); - const distTuiV2JsxMap = new URL("../dist/tui-v2.jsx.map", import.meta.url); expect(await exists(distTui)).toBe(true); - expect(await exists(distTuiV2)).toBe(true); expect(await exists(distJsx)).toBe(false); expect(await exists(distJsxMap)).toBe(false); - expect(await exists(distTuiV2Jsx)).toBe(false); - expect(await exists(distTuiV2JsxMap)).toBe(false); const source = await readFile(distTui, "utf8"); expect(source).toContain("createComponent"); - expect(source).toContain("sidebar_content"); - expect(source).toContain("buildSidebarContentRows"); - expect(source).toContain("Index"); - expect(source).not.toContain("displayLines().map"); - expect(source).toContain("loadTuiSessionQuotaSurfaces"); - expect(source).toContain("resolveTuiSurfaceRegistration"); - expect(source).toContain("const pluginModule"); - expect(source).toContain("registerQuotaDialogCommands"); - expect(source).toContain("CommandOutputDialog"); + expect(source).toContain("sidebar.content"); + expect(source).toContain("prompt.footer"); + expect(source).toContain("home.footer.status"); + expect(source).toContain("buildCompactQuotaStatusLine"); expect(source).toContain("buildQuotaDialogCommandOutput"); - expect(source).toContain("registerLayer"); + expect(source).toContain("registerQuotaCommands"); expect(source).not.toContain("jsx-dev-runtime"); }); @@ -78,14 +67,14 @@ describe("tui dist packaging", () => { expect(packagedTui.default).toMatchObject({ id: "@slkiser/opencode-quota", }); - expect(typeof packagedTui.default.tui).toBe("function"); + expect(typeof packagedTui.default.setup).toBe("function"); }); it("can load the packaged root module", async () => { const mod = await import("../dist/index.js"); expect(mod.default).toMatchObject({ - id: "@slkiser/opencode-quota", + id: "@slkiser/opencode-quota.server", }); expect(typeof mod.default.setup).toBe("function"); }); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 039c83c3..d551a31a 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -46,6 +46,12 @@ describe("V2 quota TUI commands", () => { expect(context.ui.slot).toHaveBeenCalledWith( expect.objectContaining({ append: "sidebar.content" }), ); + expect(context.ui.slot).toHaveBeenCalledWith( + expect.objectContaining({ append: "prompt.footer" }), + ); + expect(context.ui.slot).toHaveBeenCalledWith( + expect.objectContaining({ append: "home.footer.status" }), + ); expect(context.data.on.mock.calls.map(([event]) => event)).toEqual([ "session.step.ended", "session.compaction.ended", diff --git a/tests/v4-migration-docs.test.ts b/tests/v4-migration-docs.test.ts index f9f12e4d..8e35347f 100644 --- a/tests/v4-migration-docs.test.ts +++ b/tests/v4-migration-docs.test.ts @@ -24,7 +24,7 @@ const configuration = await readFile( describe("v4 migration documentation contract", () => { it("keeps requirements and navigation aligned", () => { expect(packageJson.engines?.node).toBe(">=22.0.0"); - expect(packageJson.peerDependencies?.["@opencode/plugin"]).toBe("2.0.2"); + expect(packageJson.peerDependencies?.["@opencode/plugin"]).toBe("2.0.15"); expect(packageJson.engines).not.toHaveProperty("opencode"); expect(migration).toContain("[← Back to README](../../README.md)"); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 4f4d9ad5..19f02cd5 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -1,7 +1,6 @@ import { rm } from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { isCommandHandledError } from "../src/lib/command-handled.js"; import { assertPhase5CanariesRedacted, assertPhase5FixtureOrder, @@ -22,8 +21,6 @@ import { createProvidersRegistryModuleMock, createQwenAuthModuleMock, createSessionTokensModuleMock, - getPromptText, - getToastMessage, makeQuotaToastTestConfig, seedDefaultPluginBootstrapMocks, } from "./helpers/plugin-test-harness.js"; @@ -119,24 +116,86 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT, { includeCandidates: true }), ); -type PluginHooks = { - config?: (input: unknown) => Promise | void; - dispose?: () => Promise | void; - event?: (input: unknown) => Promise | void; - "command.execute.before"?: (input: { - command: string; - sessionID: string; - }) => Promise | void; - tool?: { - quota_status?: { - execute( - args: Record, - context: { sessionID: string; metadata(value: { title: string }): void }, - ): Promise; - }; - }; +type RegisteredTool = { + name: string; + execute(input: object, context: { sessionID: string }): Promise<{ content: string }>; }; +async function setupV2Surfaces(client: ReturnType, providerIds: string[]) { + let tool: RegisteredTool | undefined; + const { default: serverPlugin } = await import("../src/plugin.js"); + await serverPlugin.setup({ + location: { directory: process.cwd() }, + provider: { list: vi.fn(async () => ({ data: providerIds.map((id) => ({ id })) })) }, + session: { + get: vi.fn(async () => ({ model: { id: "model-one", providerID: providerIds[0] } })), + }, + tool: { + transform: vi.fn(async (callback: (editor: { add(value: RegisteredTool): void }) => void) => { + callback({ + add: (value) => { + tool = value; + }, + }); + }), + }, + } as never); + expect(tool?.name).toBe("quota_status"); + + const events = new Map void>(); + let commands: Array<{ slash: { name: string }; run: (input?: unknown) => Promise }> = []; + const alert = vi.fn(async (_input: { title: string; message: string }) => {}); + const toast = vi.fn(); + const slots: string[] = []; + const { default: tuiPlugin } = await import("../src/tui-v2.js"); + const dispose = tuiPlugin.setup({ + client, + location: { directory: process.cwd() }, + data: { + on: (event: string, callback: (event: { data: { sessionID: string } }) => void) => { + events.set(event, callback); + return () => events.delete(event); + }, + location: { + default: () => ({ directory: process.cwd() }), + provider: { list: () => providerIds.map((id) => ({ id })) }, + }, + }, + keymap: { + layer: (build: () => { commands: typeof commands }) => { + commands = build().commands; + }, + }, + ui: { + slot: (claim: { append: string; render: () => unknown }) => { + slots.push(claim.append); + if (claim.append === "app") claim.render(); + return () => {}; + }, + toast: { show: toast }, + dialog: { alert, prompt: vi.fn(), set: vi.fn() }, + }, + } as never); + expect(slots).toEqual(["app", "sidebar.content", "prompt.footer", "home.footer.status"]); + const quota = commands.find((command) => command.slash.name === "quota"); + expect(quota).toBeDefined(); + expect(commands.some((command) => command.slash.name === "quota_status")).toBe(true); + return { + tool: tool!, + alert, + toast, + events, + quota: quota!, + dispose: dispose as () => void, + }; +} + +function getV2ToastMessage(toast: ReturnType, index = 0): string { + const message = toast.mock.calls[index]?.[0]?.message; + if (typeof message !== "string") throw new Error("Expected V2 CLI quota toast"); + return message; +} + function configFor(formatStyle: "allWindows" | "singleWindow") { return makeQuotaToastTestConfig({ enabled: true, @@ -220,16 +279,6 @@ function createClient() { return client; } -async function expectHandled(value: unknown): Promise { - try { - await Promise.resolve(value); - } catch (error) { - expect(isCommandHandledError(error)).toBe(true); - return; - } - throw new Error("Expected the ADR 0002 handled sentinel"); -} - function assertTreeSessionTokenTotals(output: string): void { expect(output).toMatch(/1\.2K[^\n]*300[^\n]*45/u); } @@ -391,33 +440,15 @@ describe("v4 Phase 5 cross-surface release evidence", () => { await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); - it("proves server command, toast lifecycle, TUI placement, projections, order, partial failure, and redaction", async () => { + it("proves V2 CLI command and toast, server diagnostic tool, TUI placement, export, and redaction", async () => { const client = createClient(); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const hooks = (await QuotaToastPlugin({ client } as never)) as PluginHooks; - - const serverConfig: { command?: Record } = {}; - await hooks.config?.(serverConfig); - expect(serverConfig.command).toHaveProperty("quota"); - - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota", - sessionID: "phase5-session", - }), - ); - - expect(client.session.prompt).toHaveBeenCalledTimes(1); - expect(client.session.prompt).toHaveBeenCalledWith( - expect.objectContaining({ - path: { id: "phase5-session" }, - body: expect.objectContaining({ - noReply: true, - parts: [expect.objectContaining({ type: "text", ignored: true })], - }), - }), - ); - const serverOutput = getPromptText(client); + const v2 = await setupV2Surfaces(client, PHASE5_RUNTIME_PROVIDER_IDS); + v2.events.get("session.step.ended")?.({ data: { sessionID: "phase5-session" } }); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(1)); + await v2.quota.run(); + expect(v2.alert).toHaveBeenCalledOnce(); + expect(client.session.prompt).not.toHaveBeenCalled(); + const serverOutput = v2.alert.mock.calls[0][0].message; expect(serverOutput).toMatch(/^Quota \(\/quota\)/); expect(serverOutput).not.toContain("```"); expect(serverOutput).not.toMatch(/^#{1,6} /mu); @@ -431,40 +462,19 @@ describe("v4 Phase 5 cross-surface release evidence", () => { assertTreeSessionTokenTotals(serverOutput); expect(serverOutput).toContain("tree-model"); - await hooks.event?.({ - event: { - type: "session.idle", - properties: { sessionID: "phase5-session" }, - }, - }); - expect(client.tui.showToast).toHaveBeenCalledTimes(1); - const toastOutput = getToastMessage(client); + const toastOutput = getV2ToastMessage(v2.toast); assertFixtureContent(toastOutput); assertTreeSessionTokenTotals(toastOutput); expect(toastOutput).toContain("tree-model"); const callsAfterFirstToast = vi.mocked(globalThis.fetch).mock.calls.length; - await hooks.event?.({ - event: { - type: "session.idle", - properties: { sessionID: "phase5-session" }, - }, - }); - expect(client.tui.showToast).toHaveBeenCalledTimes(2); - expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(callsAfterFirstToast); - assertFixtureContent(getToastMessage(client, 1)); - - const statusMetadata = vi.fn(); - expect(hooks.tool?.quota_status).toBeDefined(); - await hooks.tool?.quota_status?.execute( - {}, - { - sessionID: "phase5-session", - metadata: statusMetadata, - }, - ); - expect(statusMetadata).toHaveBeenCalledWith({ title: "Quota Status" }); - const statusOutput = getPromptText(client, 1); + v2.events.get("session.step.ended")?.({ data: { sessionID: "phase5-session" } }); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(2)); + // The failed accounting source is retried on the next V2 CLI event. + expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(callsAfterFirstToast + 1); + assertFixtureContent(getV2ToastMessage(v2.toast, 1)); + + const statusOutput = (await v2.tool.execute({}, { sessionID: "phase5-session" })).content; expect(statusOutput).toMatch(/^# Quota Status .*\(\/quota_status\)/u); expect(statusOutput).toContain("provider_team-accounting:"); expect(statusOutput).toContain("provider_openrouter-primary:"); @@ -489,6 +499,17 @@ describe("v4 Phase 5 cross-surface release evidence", () => { "../src/lib/quota-export.js" ); const exportContext = createExportProviderContext(runtime); + const { collectQuotaRenderData } = await import("../src/lib/quota-render-data.js"); + // The aggregate's process-local cache is scoped to the client object; V2 CLI + // constructs its own client adapter, so prime the export reader's context. + await collectQuotaRenderData({ + client: runtime.client, + resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, + config: runtime.config, + configMeta: runtime.configMeta, + request: {}, + providers: runtime.providers, + }); const fetchCallsBeforeExport = vi.mocked(globalThis.fetch).mock.calls.length; const exportData = await buildQuotaExport({ providers: [quotaProvidersProvider], @@ -700,7 +721,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(telemetryOutput).not.toContain(source.url); } expect(allOutput).not.toMatch(/telemetryToken|opencode\.quota\./); - await hooks.dispose?.(); + v2.dispose(); }); it("keeps over-quota MiniMax results in cache, export, and all four displays", async () => { @@ -715,16 +736,9 @@ describe("v4 Phase 5 cross-surface release evidence", () => { data: { providers: [{ id: "minimax-coding-plan" }] }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const hooks = (await QuotaToastPlugin({ client } as never)) as PluginHooks; - - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota", - sessionID: "minimax-session", - }), - ); - const serverOutput = getPromptText(client); + const v2 = await setupV2Surfaces(client, ["minimax-coding-plan"]); + await v2.quota.run(); + const serverOutput = v2.alert.mock.calls[0][0].message; expect(serverOutput).toContain("MiniMax Token Plan"); expect(serverOutput).toContain("5h quota"); expect(serverOutput).toContain("Weekly quota"); @@ -763,13 +777,9 @@ describe("v4 Phase 5 cross-surface release evidence", () => { ), ).toEqual([-5, -10]); - await hooks.event?.({ - event: { - type: "session.idle", - properties: { sessionID: "minimax-session" }, - }, - }); - const toastOutput = getToastMessage(client); + v2.events.get("session.step.ended")?.({ data: { sessionID: "minimax-session" } }); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); + const toastOutput = getV2ToastMessage(v2.toast); expect(toastOutput).toContain("MiniMax Token Plan"); expect(toastOutput).toContain("5h"); expect(toastOutput).toContain("Weekly"); @@ -808,7 +818,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(compactOutput.match(/0%/gu)).toHaveLength(2); expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(1); - await hooks.dispose?.(); + v2.dispose(); }); it("shows the optional Anthropic Fable weekly row on all four displays", async () => { @@ -845,27 +855,16 @@ describe("v4 Phase 5 cross-surface release evidence", () => { data: { providers: [{ id: "anthropic" }] }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const hooks = (await QuotaToastPlugin({ client } as never)) as PluginHooks; - - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota", - sessionID: "anthropic-fable-session", - }), - ); - const serverOutput = getPromptText(client); + const v2 = await setupV2Surfaces(client, ["anthropic"]); + await v2.quota.run(); + const serverOutput = v2.alert.mock.calls[0][0].message; expect(serverOutput).toContain("Claude"); expect(serverOutput).toContain("Fable"); expect(serverOutput).toContain("98% left"); - await hooks.event?.({ - event: { - type: "session.idle", - properties: { sessionID: "anthropic-fable-session" }, - }, - }); - const toastOutput = getToastMessage(client); + v2.events.get("session.step.ended")?.({ data: { sessionID: "anthropic-fable-session" } }); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); + const toastOutput = getV2ToastMessage(v2.toast); expect(toastOutput).toContain("Fable"); expect(toastOutput).toContain("98%"); @@ -896,7 +895,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(compactOutput).toContain("Fable"); expect(compactOutput).toContain("98%"); - await hooks.dispose?.(); + v2.dispose(); }); it("renders CN general percentage quota and excludes video on all four surfaces", async () => { @@ -925,16 +924,9 @@ describe("v4 Phase 5 cross-surface release evidence", () => { data: { providers: [{ id: "minimax-china-coding-plan" }] }, }); - const { QuotaToastPlugin } = await import("../src/plugin.js"); - const hooks = (await QuotaToastPlugin({ client } as never)) as PluginHooks; - - await expectHandled( - hooks["command.execute.before"]?.({ - command: "quota", - sessionID: "minimax-china-session", - }), - ); - const serverOutput = getPromptText(client); + const v2 = await setupV2Surfaces(client, ["minimax-china-coding-plan"]); + await v2.quota.run(); + const serverOutput = v2.alert.mock.calls[0][0].message; expect(serverOutput).toContain("MiniMax Token Plan"); expect(serverOutput).toContain("(CN)"); expect(serverOutput).toContain("5h quota"); @@ -944,13 +936,9 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(serverOutput).not.toContain("video"); expect(serverOutput).not.toContain("Invalid normalized provider result"); - await hooks.event?.({ - event: { - type: "session.idle", - properties: { sessionID: "minimax-china-session" }, - }, - }); - const toastOutput = getToastMessage(client); + v2.events.get("session.step.ended")?.({ data: { sessionID: "minimax-china-session" } }); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); + const toastOutput = getV2ToastMessage(v2.toast); expect(toastOutput).toContain("MiniMax Token Plan"); expect(toastOutput).toContain("(CN)"); expect(toastOutput).toContain("5h"); @@ -993,6 +981,6 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(compactOutput).not.toContain("video"); expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(1); - await hooks.dispose?.(); + v2.dispose(); }); }); diff --git a/tui.ts b/tui.ts index 0fdc642d..8c5d23bd 100644 --- a/tui.ts +++ b/tui.ts @@ -1 +1 @@ -export { default } from "./src/index.js"; +export { default } from "./src/tui-v2.js"; From 1c86c6648f13014d0477ce04d0153a2be89d3abc Mon Sep 17 00:00:00 2001 From: Ran Sagy Date: Thu, 24 Sep 2026 12:26:52 +0300 Subject: [PATCH 23/97] fix(quota): drop the auth.json fallback from the V2 branch Per review: the V2 branch must not read OpenCode`s auth.json anywhere. Removes readLegacyAuthEntries/readLegacyAuthRows and their use in the Go provider`s legacy usage API fallback, which now authenticates only from credential database rows (opencode-go/opencode key integrations). Also treats console /api/go/status 404 as "not subscribed" while keeping 403 ambiguous (it may indicate an access issue rather than a subscription state), so callers fall back and surface the console error instead of hiding behind a not-subscribed state. --- src/lib/opencode-auth.ts | 35 ---------------------------- src/lib/opencode-go.ts | 8 +++++-- src/providers/opencode-go.ts | 8 ++----- tests/lib.opencode-auth.test.ts | 19 --------------- tests/providers.opencode-go.test.ts | 36 ----------------------------- 5 files changed, 8 insertions(+), 98 deletions(-) diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 9444b9e8..6734c2a0 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -118,41 +118,6 @@ export async function readAuthFile(): Promise { return Object.keys(auth).length > 0 ? (auth as AuthData) : null; } -async function readLegacyAuthEntries(): Promise { - for (const path of getAuthPaths()) { - if (!existsSync(path)) continue; - try { - const { readFile } = await import("fs/promises"); - const raw = await readFile(path, "utf8"); - const parsed: unknown = JSON.parse(raw); - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { - return parsed as AuthData; - } - } catch { - // Unreadable legacy files are ignored; the database remains the source. - } - } - return null; -} - -/** Credential rows synthesized from the legacy auth.json file. */ -export async function readLegacyAuthRows(): Promise { - const legacy = await readLegacyAuthEntries(); - if (!legacy) return []; - const rows: CredentialRow[] = []; - for (const [integrationId, entry] of Object.entries(legacy)) { - if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue; - rows.push({ - id: `auth-json:${integrationId}`, - integrationId, - label: "legacy", - active: true, - value: entry as Record, - }); - } - return rows; -} - export async function readCredentialRows(): Promise { return readCredentialRowsFromDatabases(getCredentialDatabasePaths()); } diff --git a/src/lib/opencode-go.ts b/src/lib/opencode-go.ts index 6102574d..645929db 100644 --- a/src/lib/opencode-go.ts +++ b/src/lib/opencode-go.ts @@ -256,10 +256,14 @@ export async function queryOpenCodeGoConsoleStatus( timeoutMs: options.requestTimeoutMs, consume: async (response) => { if (!response.ok) { - if (response.status === 404 || response.status === 403) { + // 404 on this member-scoped route is read as "no Go subscription". + // 403 is kept ambiguous (it could be an access/permission issue on + // the console side rather than a subscription state), so callers + // fall back instead of showing a not-subscribed state. + if (response.status === 404) { return { success: false, - error: `OpenCode Go subscription not found for this console account (${response.status})`, + error: "OpenCode Go subscription not found for this console account (404)", notSubscribed: true, }; } diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index 80c452da..0d0f0632 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -10,7 +10,6 @@ import type { import { formatCredentialDisplayNames, readCredentialRows, - readLegacyAuthRows, selectConnectionCredentialRows, } from "../lib/opencode-auth.js"; import { @@ -209,12 +208,9 @@ async function fetchOpenCodeGoLegacy( // credential database (see resolveOpenCodeGoAuth). Alias rows must not // become additional connections: prefer native rows and collapse rows // holding the same credential (e.g. Zen + Go sharing a workspace key). - // OpenCode's console migration removed workspace API keys from the - // credential database while the key written by pre-2.0 `opencode auth - // login` (auth.json) can remain valid for the Go usage API. Console - // OAuth credentials cannot authorize this API and are skipped. + // Console OAuth credentials cannot authorize this API and are skipped. const credentialRows = selectConnectionCredentialRows( - [...(await readCredentialRows()), ...(await readLegacyAuthRows())] + (await readCredentialRows()) .filter((row) => OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS.includes(row.integrationId), ) diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index b25ebf18..d927cbd5 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -11,7 +11,6 @@ import { getCredentialDatabasePaths, readAuthFile, readCredentialRows, - readLegacyAuthRows, selectConnectionCredentialRows, } from "../src/lib/opencode-auth.js"; @@ -187,24 +186,6 @@ describe("OpenCode auth reader", () => { openai: { access: "openai-access" }, }); }); - - it("synthesizes legacy auth.json rows for providers that lost keys in the console migration", async () => { - const { dataDir } = await createCredentialDatabase(); - vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); - await writeFile( - join(dataDir, "auth.json"), - JSON.stringify({ "opencode-go": { type: "key", key: "workspace-key" } }), - ); - - const rows = await readLegacyAuthRows(); - expect(rows).toEqual([ - expect.objectContaining({ - id: "auth-json:opencode-go", - integrationId: "opencode-go", - value: { type: "key", key: "workspace-key" }, - }), - ]); - }); }); describe("selectConnectionCredentialRows", () => { diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index cb41588d..c5dfa58a 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -14,7 +14,6 @@ const mocks = vi.hoisted(() => ({ getOpenCodeGoAuthDiagnostics: vi.fn(), queryOpenCodeGoQuota: vi.fn(), queryOpenCodeGoConsoleStatus: vi.fn(), - readLegacyAuthRows: vi.fn(async () => []), resolveOpenCodeConsoleAuth: vi.fn(), })); @@ -43,7 +42,6 @@ vi.mock("../src/lib/opencode-console-auth.js", () => ({ vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ ...(await importOriginal()), readCredentialRows: vi.fn().mockResolvedValue([]), - readLegacyAuthRows: mocks.readLegacyAuthRows, })); import { @@ -109,7 +107,6 @@ describe("opencode-go provider", () => { }); mocks.queryOpenCodeGoQuota.mockResolvedValue(successfulResult()); mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "row-token" }); - mocks.readLegacyAuthRows.mockResolvedValue([]); mocks.resolveOpenCodeConsoleAuth.mockResolvedValue({ state: "none" }); }); @@ -419,39 +416,6 @@ describe("opencode-go provider", () => { ]); }); - it("uses the pre-2.0 auth.json workspace key after the console migration removes database rows", async () => { - mocks.readLegacyAuthRows.mockResolvedValueOnce([ - { - id: "auth-json:opencode-go", - integrationId: "opencode-go", - label: "legacy", - active: true, - value: { type: "key", key: "legacy-workspace-key" }, - }, - { - id: "auth-json:opencode", - integrationId: "opencode", - label: "legacy", - active: true, - value: { - type: "oauth", - methodID: "server", - access: "console-token", - metadata: { orgID: "wrk_x" }, - }, - }, - ]); - mocks.resolveOpenCodeGoAuth.mockReturnValue({ state: "configured", apiKey: "legacy-workspace-key" }); - - const out = await runFetch(["rolling", "weekly"]); - - expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); - expect(visibleEntries(out.entries, "opencode-go").map((entry) => entry.group)).toEqual([ - "[OpenCode Go legacy]*", - "[OpenCode Go legacy]*", - ]); - }); - it("falls back to legacy alias rows when no native opencode-go row exists", async () => { const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); (readCredentialRows as any).mockResolvedValueOnce([ { From b3706b16a11c8fab70c8732755b00b14e3da485a Mon Sep 17 00:00:00 2001 From: dmmop <> Date: Thu, 24 Sep 2026 12:23:28 +0200 Subject: [PATCH 24/97] chore(release): sync package version to 4.10.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a49f2b7..883de720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slkiser/opencode-quota", - "version": "4.10.1", + "version": "4.10.2", "packageManager": "pnpm@11.0.0", "description": "OpenCode quota & tokens usage with zero context window pollution. Supports GitHub Copilot, OpenAI (Plus/Pro), Qwen Code, Chutes AI, Synthetic, Google Antigravity, Z.ai coding plan and more.", "type": "module", From 521dabf629e7855f2ce2f47b07afd3e040c40e12 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:27:33 +0200 Subject: [PATCH 25/97] refactor!: delete the OpenCode 1 TUI and its tests OpenCode 2 loads the TUI from src/tui-v2.tsx (packaged as dist/tui.js), so the OpenCode 1 TUI and everything only it used are removed. Deleted source: src/tui.tsx, src/lib/tui-runtime.ts, src/lib/tui-line-style.ts, src/types/tui-runtime-shims.d.ts, and the V1-only helpers src/lib/tui-native-provider-quota.ts, src/lib/tui-refresh-lifecycle.ts, src/lib/tui-sidebar-content.ts. Deleted scripts: scripts/test-stabilization-connected.mjs, scripts/test-stabilization-fixtures.mjs and the test:stabilization:* package scripts (plus their CONTRIBUTING section). Deleted tests: tui-smoke, tui-runtime, tui-line-style, tui-sidebar-collapse, tui-startup-registration, tui-startup-runtime-resolution, tui-provider-availability-timing, quota-surface-parity, test-stabilization-connected, test-stabilization-fixtures, lib.tui-native-provider-quota, tui-first-quota-lifecycle, tui-sidebar-content, tui-sidebar-expansion-ghost, and their now-unused helpers. Also: drop createPluginToolMockModule and every vi.mock("@opencode-ai/plugin"); biome override targets only src/tui-v2.tsx; the TUI plugin-spec check matches dist/tui.js; prepare-tui-dist no longer removes the old tui.jsx output. The four-surface test now reads sidebar and compact output from the OpenCode 2 TUI slots instead of the deleted V1 runtime. tui-config-diagnostics tests stay because that module is still live. --- CONTRIBUTING.md | 22 - biome.json | 2 +- package.json | 4 - scripts/prepare-tui-dist.mjs | 4 - scripts/test-stabilization-connected.mjs | 1778 ---------- scripts/test-stabilization-fixtures.mjs | 445 --- src/lib/config-file-utils.ts | 2 +- src/lib/tui-line-style.ts | 12 - src/lib/tui-native-provider-quota.ts | 25 - src/lib/tui-refresh-lifecycle.ts | 102 - src/lib/tui-runtime.ts | 721 ---- src/lib/tui-sidebar-content.ts | 95 - src/tui.tsx | 1030 ------ src/types/tui-runtime-shims.d.ts | 228 -- tests/helpers/plugin-test-harness.ts | 19 - tests/helpers/tui-first-quota-timing.ts | 367 -- .../tui-provider-availability-timing.ts | 177 - .../helpers/tui-startup-runtime-resolution.ts | 251 -- tests/lib.init-installer.test.ts | 6 +- tests/lib.tui-native-provider-quota.test.ts | 41 - tests/plugin.maintainer-announcements.test.ts | 2 - tests/plugin.quota-command.test.ts | 3 - tests/plugin.quota-status-command.test.ts | 3 - tests/quota-surface-parity.test.ts | 648 ---- tests/test-stabilization-connected.test.ts | 1546 -------- tests/test-stabilization-fixtures.test.ts | 495 --- tests/tui-config-diagnostics.test.ts | 4 +- tests/tui-dist-packaging.test.ts | 2 +- tests/tui-first-quota-lifecycle.test.ts | 333 -- tests/tui-line-style.test.ts | 25 - .../tui-provider-availability-timing.test.ts | 596 ---- tests/tui-runtime.test.ts | 3153 ----------------- tests/tui-sidebar-collapse.test.ts | 425 --- tests/tui-sidebar-content.test.ts | 97 - tests/tui-sidebar-expansion-ghost.test.ts | 68 - tests/tui-smoke.test.ts | 2150 ----------- tests/tui-startup-registration.test.ts | 462 --- tests/tui-startup-runtime-resolution.test.ts | 693 ---- tests/v4-phase5-cross-surface.test.ts | 180 +- tsconfig.json | 2 +- 40 files changed, 72 insertions(+), 16146 deletions(-) delete mode 100644 scripts/test-stabilization-connected.mjs delete mode 100644 scripts/test-stabilization-fixtures.mjs delete mode 100644 src/lib/tui-line-style.ts delete mode 100644 src/lib/tui-native-provider-quota.ts delete mode 100644 src/lib/tui-refresh-lifecycle.ts delete mode 100644 src/lib/tui-runtime.ts delete mode 100644 src/lib/tui-sidebar-content.ts delete mode 100644 src/tui.tsx delete mode 100644 src/types/tui-runtime-shims.d.ts delete mode 100644 tests/helpers/tui-first-quota-timing.ts delete mode 100644 tests/helpers/tui-provider-availability-timing.ts delete mode 100644 tests/helpers/tui-startup-runtime-resolution.ts delete mode 100644 tests/lib.tui-native-provider-quota.test.ts delete mode 100644 tests/quota-surface-parity.test.ts delete mode 100644 tests/test-stabilization-connected.test.ts delete mode 100644 tests/test-stabilization-fixtures.test.ts delete mode 100644 tests/tui-first-quota-lifecycle.test.ts delete mode 100644 tests/tui-line-style.test.ts delete mode 100644 tests/tui-provider-availability-timing.test.ts delete mode 100644 tests/tui-runtime.test.ts delete mode 100644 tests/tui-sidebar-collapse.test.ts delete mode 100644 tests/tui-sidebar-content.test.ts delete mode 100644 tests/tui-sidebar-expansion-ghost.test.ts delete mode 100644 tests/tui-smoke.test.ts delete mode 100644 tests/tui-startup-registration.test.ts delete mode 100644 tests/tui-startup-runtime-resolution.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc718771..21838c6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,28 +52,6 @@ It checks Biome linting and formatting, the pinned TypeScript toolchain, reposit Use `pnpm run test:watch` for local iteration. Use `pnpm run build:check` when you need the build plus package dry-run check. -## Maintainer stabilization checks - -These commands are for maintainers. They do not replace `pnpm verify`. - -### Connected OpenCode - -`pnpm run test:stabilization:tui` builds this worktree, copies `$OPENCODE_CONFIG_DIR` or the default OpenCode config directory into a `0700` temp directory, points only the OpenCode Quota plugin entries at this worktree's `dist/index.js` and `dist/tui.js`, enables sidebar/toast/compact with the prompt bar off, and launches real `opencode`. Companion plugin order and other copied settings stay as they were. The real config is never edited. OpenCode is launched only with the temp copy. That copy is deleted on exit, on prepare/copy/transform failure, and after forwarded `SIGINT`/`SIGTERM`/`SIGHUP`. After those signals, the runner waits a bounded grace period, then force-terminates the child (process group where safe) so cleanup cannot wait forever. - -This uses real credentials and makes real quota/API calls. - -After the first TUI session exits, the script offers a second session with the prompt bar on. Use `pnpm run test:stabilization:tui:prompt-bar` to skip the first stage. Use `pnpm run test:stabilization:web` to launch `opencode web` instead. - -Web mode runs the fixed `opencode models` command directly, without a shell or model request, in the same isolated environment used for `opencode web`. Plain catalog lines count as usable models. Structured catalog entries must explicitly report a usable state; aliases and disabled, hidden, unavailable, or unknown entries are rejected. If the copied default is stale, the runner prefers the first usable same-provider model using locale-independent ordering and rewrites only the temp JSONC copy. Comments and plugin order stay intact. An empty, failed, or unusable catalog stops the Web launch and prints click/select steps. The real config is not edited. After choosing a model, run `/quota`, then `/quota_status`. - -Web diagnostics live outside the temp config in a `0700` directory with a `0600` file. Runner output, server output, each log read, the number of log files, and the final file all have fixed limits and use a truncation marker. Header and cookie values, credentials, request and response bodies, serialized config, and marked private content are replaced with redaction markers. Log capture starts at the pre-launch offsets rather than copying full historical logs. Diagnostic creation, reads, and writes are best-effort; a failure cannot replace the Web child exit result, and the path prints only after a successful write. Classification requires markers in order. A generic send failure, or a busy marker without earlier successful output evidence, is `unknown-unclassified`. #272 requires hook entry, successful output evidence, and an affirmative session-busy marker. A model error is `invalid-model-before-hook` only when no hook-entry marker exists. Signals stop preflight or Web, escalate after the grace period when needed, clean the temp config, and retain the original signal exit code. - -The prompt bar is a fixed 12-cell bar. The label is the provider plus window, for example `OpenAI 5h`. When you resize the terminal, check placement and clipping. The bar does not grow. - -### Fake-data fixtures - -`pnpm run test:stabilization:fixtures` runs a frozen Vitest set for config symlinks, config write targets, atomic JSON, OpenCode Go, Synthetic empty responses/surfaces, OpenRouter diagnostics/surfaces, Alibaba Token Plan process/provider, quota status, prompt selection, prompt-bar identity, TUI runtime, quota export, API-key query/config, and contribution guidance. It does not use real credentials, network provider calls, or the real `bl` executable. Before Vitest starts, the runner creates a `0700` temp HOME/XDG sandbox, strips known provider credential/session env vars and `OPENCODE_CONFIG`/`OPENCODE_CONFIG_DIR`, then deletes the sandbox on success, error, or signal. - ## CI Checks (Automated) PR and `main` pushes trigger `.github/workflows/ci.yml` (`CI` workflow): diff --git a/biome.json b/biome.json index efeb97a7..68e695fe 100644 --- a/biome.json +++ b/biome.json @@ -83,7 +83,7 @@ } }, { - "includes": ["src/tui.tsx", "src/tui-v2.tsx"], + "includes": ["src/tui-v2.tsx"], "linter": { "rules": { "a11y": { diff --git a/package.json b/package.json index 80046ebd..b1a1355f 100644 --- a/package.json +++ b/package.json @@ -52,10 +52,6 @@ "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", - "test:stabilization:tui": "node scripts/test-stabilization-connected.mjs --tui", - "test:stabilization:tui:prompt-bar": "node scripts/test-stabilization-connected.mjs --tui --prompt-bar", - "test:stabilization:web": "node scripts/test-stabilization-connected.mjs --web", - "test:stabilization:fixtures": "node scripts/test-stabilization-fixtures.mjs", "verify": "pnpm run check && pnpm run verify:typescript-version && pnpm run verify:v4-history && pnpm run typecheck && pnpm run build && pnpm test && pnpm run test:four-surfaces && pnpm run verify:package-contents", "prepublishOnly": "pnpm run build", "prepare": "lefthook install" diff --git a/scripts/prepare-tui-dist.mjs b/scripts/prepare-tui-dist.mjs index 67500e7a..907372d9 100644 --- a/scripts/prepare-tui-dist.mjs +++ b/scripts/prepare-tui-dist.mjs @@ -10,8 +10,6 @@ const rootDir = path.resolve(__dirname, ".."); const sourcePath = path.join(rootDir, "src", "tui-v2.tsx"); const distJsPath = path.join(rootDir, "dist", "tui.js"); const distTypesPath = path.join(rootDir, "dist", "tui.d.ts"); -const distJsxPath = path.join(rootDir, "dist", "tui.jsx"); -const distJsxMapPath = path.join(rootDir, "dist", "tui.jsx.map"); for (const [inputPath, outputPath] of [[sourcePath, distJsPath]]) { const source = await fs.readFile(inputPath, "utf8"); @@ -32,7 +30,5 @@ for (const [inputPath, outputPath] of [[sourcePath, distJsPath]]) { await fs.writeFile(outputPath, `${transformed.code}\n`); } -await fs.rm(distJsxPath, { force: true }); -await fs.rm(distJsxMapPath, { force: true }); await fs.copyFile(path.join(rootDir, "dist", "tui-v2.d.ts"), distTypesPath); await fs.copyFile(path.join(rootDir, "dist", "tui-v2.d.ts.map"), `${distTypesPath}.map`); diff --git a/scripts/test-stabilization-connected.mjs b/scripts/test-stabilization-connected.mjs deleted file mode 100644 index 3be8b190..00000000 --- a/scripts/test-stabilization-connected.mjs +++ /dev/null @@ -1,1778 +0,0 @@ -#!/usr/bin/env node -/** - * Maintainer connected TUI/Web runner. - * - * Copies the real OpenCode config into a 0700 temp directory, rewrites only - * OpenCode Quota plugin entries to this worktree's dist file URLs, and launches - * real `opencode`. The real config is never edited. The temp copy is deleted - * on exit, on prepare/copy/transform failure, and after forwarded signals. - * Web mode keeps a separate sanitized diagnostics file for that run only. - * After SIGINT/SIGTERM/SIGHUP, the child gets a bounded grace period and is - * then force-terminated (process group where safe) so cleanup cannot wait forever. - */ -import { spawn } from "node:child_process"; -import { existsSync } from "node:fs"; -import { - chmod, - cp, - mkdir, - mkdtemp, - open, - opendir, - readdir, - readFile, - realpath, - rm, - stat, - writeFile, -} from "node:fs/promises"; -import { homedir, constants as osConstants, tmpdir } from "node:os"; -import path from "node:path"; -import { createInterface } from "node:readline/promises"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { parse, stringify } from "comment-json"; -import { xdgConfig } from "xdg-basedir"; - -export const CONNECTED_TEMP_PREFIX = "opencode-quota-stabilization-"; -export const FORWARD_SIGNALS = Object.freeze(["SIGINT", "SIGTERM", "SIGHUP"]); -export const CONNECTED_SIGNAL_GRACE_MS = 2_000; -export const CONNECTED_CREDENTIAL_WARNING = - "WARNING: This connected run uses your real OpenCode credentials, quota, and provider API calls. The real config is not modified. A temporary copy is used and deleted on exit."; - -const scriptPath = fileURLToPath(import.meta.url); -export const repoRoot = path.resolve(path.dirname(scriptPath), ".."); - -const TUI_SCHEMA_URL = "https://opencode.ai/tui.json"; - -export function getConnectedUsage() { - return `Usage: - node scripts/test-stabilization-connected.mjs --tui [--prompt-bar] [--dry-run] - node scripts/test-stabilization-connected.mjs --web [--dry-run] - node scripts/test-stabilization-connected.mjs --help - -Copies $OPENCODE_CONFIG_DIR (or the default OpenCode config dir) into a 0700 temp -directory, points only OpenCode Quota plugin entries at this worktree's dist files, -enables sidebar/toast/compact, and launches real opencode. Never edits the real config. - -TUI launches with the prompt bar off. After the first TUI exits, the script offers a -second session with the prompt bar on. Use --prompt-bar to skip the first stage. -The prompt bar is a fixed 12-cell bar. Check provider identity and placement/clipping -on terminal resize; the bar does not grow with the terminal. -Web launches opencode web after checking the temp copy's default model against -the fixed opencode models command in that same temp environment. It never sends a -model request. If the model is stale, only the temp copy is rewritten. If a safe -model cannot be chosen, the runner exits before launch with click/select guidance. -Web writes bounded, sanitized diagnostics for this run only. Diagnostic capture is -best-effort and cannot change the Web child exit result. - -This uses real credentials and makes real quota/API calls.`; -} - -export function parseConnectedArgs(argv) { - const args = { mode: null, promptBar: false, dryRun: false, help: false }; - for (const arg of argv) { - if (arg === "--help" || arg === "-h") { - args.help = true; - continue; - } - if (arg === "--tui") { - if (args.mode && args.mode !== "tui") { - throw new Error("Choose only one of --tui or --web."); - } - args.mode = "tui"; - continue; - } - if (arg === "--web") { - if (args.mode && args.mode !== "web") { - throw new Error("Choose only one of --tui or --web."); - } - args.mode = "web"; - continue; - } - if (arg === "--prompt-bar") { - args.promptBar = true; - continue; - } - if (arg === "--dry-run") { - args.dryRun = true; - continue; - } - throw new Error(`Unknown argument: ${arg}\n\n${getConnectedUsage()}`); - } - if (args.help) return args; - if (!args.mode) { - throw new Error(`Specify --tui or --web.\n\n${getConnectedUsage()}`); - } - if (args.promptBar && args.mode !== "tui") { - throw new Error("--prompt-bar is only valid with --tui."); - } - return args; -} - -export function connectedLaunchArgs(mode) { - return mode === "web" ? ["web"] : []; -} - -export const OPENCODE_MODELS_ARGS = Object.freeze(["models"]); -export const CONNECTED_WEB_COMMANDS = Object.freeze(["/quota", "/quota_status"]); -export const WEB_DIAGNOSTICS_TEMP_PREFIX = "opencode-quota-web-diagnostics-"; -export const WEB_FAILURE_INVALID_MODEL_BEFORE_HOOK = "invalid-model-before-hook"; -export const WEB_FAILURE_HOOK_ENTRY_BUILD_INJECTION = "hook-entry-build-injection-failure"; -export const WEB_FAILURE_POST_INJECTION_BUSY_272 = "post-injection-busy-272"; -export const WEB_FAILURE_UNKNOWN = "unknown-unclassified"; -export const WEB_DIAGNOSTIC_SOURCE_MAX_BYTES = 64 * 1024; -export const WEB_DIAGNOSTIC_LOG_MAX_BYTES = 128 * 1024; -export const WEB_DIAGNOSTIC_TOTAL_MAX_BYTES = 256 * 1024; -export const WEB_DIAGNOSTIC_MAX_LOG_FILES = 32; -export const CAPTURED_PROCESS_STREAM_MAX_BYTES = 256 * 1024; -export const DIAGNOSTIC_TRUNCATED_MARKER = "\n[TRUNCATED: diagnostic byte limit reached]\n"; -export const DIAGNOSTIC_REDACTED_HEADER_MARKER = "[REDACTED_HEADER]"; -export const DIAGNOSTIC_REDACTED_BODY_MARKER = "[REDACTED_BODY]"; -export const DIAGNOSTIC_REDACTED_CONFIG_MARKER = "[REDACTED_CONFIG]"; -export const DIAGNOSTIC_REDACTED_PRIVATE_MARKER = "[REDACTED_PRIVATE]"; - -const MODEL_REQUEST_COMMANDS = new Set(["run", "generate", "prompt"]); -const ANSI_COLOR_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"); - -export function assertNoModelRequest(args) { - for (const arg of args) { - if (MODEL_REQUEST_COMMANDS.has(arg)) { - throw new Error("Refusing to send a model request from the connected Web runner."); - } - } - return args; -} - -export function normalizeConfiguredModel(value) { - if (typeof value === "string") { - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; - } - if (!value || typeof value !== "object" || Array.isArray(value)) return null; - const provider = - typeof value.providerID === "string" - ? value.providerID.trim() - : typeof value.providerId === "string" - ? value.providerId.trim() - : ""; - const id = - typeof value.modelID === "string" - ? value.modelID.trim() - : typeof value.id === "string" - ? value.id.trim() - : ""; - if (provider && id) return `${provider}/${id}`; - if (id.includes("/")) return id; - return null; -} - -function compareModelIds(a, b) { - if (a === b) return 0; - return a < b ? -1 : 1; -} - -function isModelId(value) { - return typeof value === "string" && value.includes("/") && !value.includes("://"); -} - -export function uniqueSortedModels(models) { - const seen = new Set(); - const out = []; - for (const model of models) { - const id = typeof model === "string" ? model.trim() : model?.id?.trim(); - if (!isModelId(id) || (typeof model === "object" && model?.usability !== "usable")) { - continue; - } - if (seen.has(id)) continue; - seen.add(id); - out.push(id); - } - out.sort(compareModelIds); - return out; -} - -function structuredModelId(value, inheritedProvider) { - if (!value || typeof value !== "object" || Array.isArray(value)) return null; - const provider = - typeof value.providerID === "string" - ? value.providerID.trim() - : typeof value.providerId === "string" - ? value.providerId.trim() - : typeof value.provider === "string" - ? value.provider.trim() - : inheritedProvider; - const rawId = - typeof value.modelID === "string" - ? value.modelID.trim() - : typeof value.modelId === "string" - ? value.modelId.trim() - : typeof value.id === "string" - ? value.id.trim() - : ""; - if (rawId.includes("/")) return rawId; - return provider && rawId ? `${provider}/${rawId}` : null; -} - -function structuredModelUsability(value) { - const positiveStates = new Set(["available", "enabled", "active", "ready", "online"]); - const negativeStates = new Set([ - "alias", - "disabled", - "hidden", - "unavailable", - "offline", - "removed", - ]); - const alias = - value.alias === true || - typeof value.alias === "string" || - typeof value.aliasOf === "string" || - typeof value.targetModel === "string"; - if (alias) return "rejected"; - - let usable = false; - let unknown = false; - for (const key of ["status", "availability", "state"]) { - if (!(key in value)) continue; - if (typeof value[key] !== "string") { - unknown = true; - continue; - } - const state = value[key].trim().toLowerCase(); - if (negativeStates.has(state)) return "rejected"; - if (positiveStates.has(state)) usable = true; - else unknown = true; - } - - for (const [key, positive, negative] of [ - ["available", true, false], - ["enabled", true, false], - ["disabled", false, true], - ["hidden", false, true], - ["visible", false, false], - ["unavailable", false, true], - ]) { - if (!(key in value)) continue; - if (typeof value[key] !== "boolean") { - unknown = true; - continue; - } - if (value[key] === negative) return "rejected"; - if (positive && value[key] === positive) usable = true; - } - - for (const key of ["alias", "aliasOf", "targetModel"]) { - if (key in value && value[key] !== false && value[key] != null) unknown = true; - } - return usable && !unknown ? "usable" : "unknown"; -} - -function extractStructuredModels(value, inheritedProvider = "") { - if (Array.isArray(value)) { - return value.flatMap((item) => extractStructuredModels(item, inheritedProvider)); - } - if (!value || typeof value !== "object") return []; - const provider = - typeof value.providerID === "string" - ? value.providerID.trim() - : typeof value.providerId === "string" - ? value.providerId.trim() - : typeof value.provider === "string" - ? value.provider.trim() - : inheritedProvider; - const nested = []; - for (const key of ["models", "data", "items", "all", "providers"]) { - if (key in value) nested.push(...extractStructuredModels(value[key], provider)); - } - const hasNestedModels = ["models", "providers"].some((key) => key in value); - const id = hasNestedModels ? null : structuredModelId(value, inheritedProvider); - if (!isModelId(id)) return nested; - return [ - { - id, - usability: structuredModelUsability(value), - source: "structured", - metadata: value, - }, - ...nested, - ]; -} - -function parseWarningPrefixedJson(raw) { - const lines = raw.split(/\r?\n/); - const index = lines.findIndex((line) => { - const trimmed = line.trimStart(); - return trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith('"'); - }); - if (index < 0) return { kind: "plain", value: null }; - try { - return { - kind: "structured", - value: JSON.parse([lines[index].trimStart(), ...lines.slice(index + 1)].join("\n")), - }; - } catch { - return { kind: "malformed", value: null }; - } -} - -export function parseOpenCodeModelsOutput(text) { - const raw = limitDiagnosticText(String(text), CAPTURED_PROCESS_STREAM_MAX_BYTES).replace( - ANSI_COLOR_PATTERN, - "", - ); - const structured = parseWarningPrefixedJson(raw); - if (structured.kind === "structured") return extractStructuredModels(structured.value); - if (structured.kind === "malformed") return []; - - const models = []; - for (const line of raw.split(/\r?\n/)) { - const trimmed = line.replace(ANSI_COLOR_PATTERN, "").trim(); - if (!trimmed || trimmed.startsWith("#") || /^warn(?:ing)?\b/i.test(trimmed)) continue; - const [token, ...metadataParts] = trimmed.split(/\s+/); - if (!isModelId(token)) continue; - const metadataText = metadataParts.join(" ").trim(); - const normalizedMetadata = metadataText.toLowerCase(); - const rejected = /\b(?:alias|disabled|hidden|unavailable|offline|removed)\b/.test( - normalizedMetadata, - ); - const explicitlyUsable = /\b(?:available|enabled|active|ready|online)\b/.test( - normalizedMetadata, - ); - models.push({ - id: token, - usability: rejected ? "rejected" : !metadataText || explicitlyUsable ? "usable" : "unknown", - source: "plain", - metadata: metadataText || null, - }); - } - return [...new Map(models.map((model) => [model.id, model])).values()]; -} - -export function chooseConnectedWebModel(originalModel, catalog) { - const models = uniqueSortedModels(catalog); - if (models.length === 0) return { status: "empty-catalog", model: null }; - if (originalModel && models.includes(originalModel)) { - return { status: "keep", model: originalModel }; - } - if (originalModel && !originalModel.includes("/")) { - const qualified = models.find((id) => id.endsWith(`/${originalModel}`)); - if (qualified) return { status: "replace", model: qualified }; - } - let chosen = models[0]; - if (originalModel?.includes("/")) { - const provider = originalModel.slice(0, originalModel.indexOf("/")); - const sameProvider = models.filter((id) => id.startsWith(`${provider}/`)); - if (sameProvider.length > 0) chosen = sameProvider[0]; - } - return { status: "replace", model: chosen }; -} - -export function formatModelLabel(model) { - return model ?? "(unset)"; -} - -export function formatWebModelChoice(result) { - return [ - `Copied default model: ${formatModelLabel(result.originalModel)}`, - `Temporary Web model: ${formatModelLabel(result.temporaryModel)}`, - ].join("\n"); -} - -export function formatConnectedWebModelGuidance(options) { - const original = formatModelLabel(options.originalModel); - const catalog = uniqueSortedModels(options.catalog ?? []); - const lines = [ - "Connected Web preflight refused to launch because the temp default model is not a currently available connected model.", - `Copied default model: ${original}`, - "The real OpenCode config was not edited.", - "OpenCode Web validates the session model before /quota and /quota_status plugin hooks run.", - ]; - if (options.reason === "models-command-failed") { - lines.push( - "Could not list models with the fixed OpenCode command `opencode models` (no shell, no model request).", - ); - if (options.detail) lines.push(redactConnectedDiagnostics(String(options.detail))); - } else if (options.reason === "empty-catalog") { - lines.push("The model catalog for this temp OpenCode environment is empty."); - } else if (options.reason === "unusable-catalog") { - lines.push("The model catalog did not contain a non-alias model with explicit usable status."); - } - lines.push("To continue this Web check:"); - lines.push( - "1. Click the model name in the OpenCode Web session header to open the model selector.", - ); - if (catalog.length > 0) { - lines.push("2. Select one of these currently available models:"); - for (const model of catalog) lines.push(` - ${model}`); - } else { - lines.push( - "2. Authenticate a connected provider, then reopen the model selector and choose an available model.", - ); - } - lines.push(`3. Run ${CONNECTED_WEB_COMMANDS[0]}.`); - lines.push(`4. Run ${CONNECTED_WEB_COMMANDS[1]}.`); - lines.push("Do not send a chat prompt or other model request."); - return lines.join("\n"); -} - -export function limitDiagnosticText(text, maxBytes, marker = DIAGNOSTIC_TRUNCATED_MARKER) { - const input = Buffer.from(String(text), "utf8"); - if (input.length <= maxBytes) return input.toString("utf8"); - const markerBytes = Buffer.byteLength(marker); - if (maxBytes <= markerBytes) return Buffer.from(marker).subarray(0, maxBytes).toString("utf8"); - const contentBytes = maxBytes - markerBytes; - let end = contentBytes; - let prefix = input.subarray(0, end).toString("utf8"); - while (Buffer.byteLength(prefix) > contentBytes && end > 0) { - end -= 1; - prefix = input.subarray(0, end).toString("utf8"); - } - return `${prefix}${marker}`; -} - -export function appendBoundedDiagnostic(current, chunk, maxBytes) { - if (current.endsWith(DIAGNOSTIC_TRUNCATED_MARKER)) return current; - return limitDiagnosticText(`${current}${String(chunk)}`, maxBytes); -} - -function diagnosticRedactionForLine(line) { - const patterns = [ - [ - /^\s*(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-auth-token)\s*:|["']?(?:headers?|cookies?|set-cookie)["']?\s*[:=]/i, - DIAGNOSTIC_REDACTED_HEADER_MARKER, - ], - [ - /["']?(?:(?:request|response)\s*(?:body|payload|content)|request_body|response_body|requestBody|responseBody)["']?\s*[:=]/i, - DIAGNOSTIC_REDACTED_BODY_MARKER, - ], - [ - /["']?(?:serialized\s+)?(?:opencode\s+)?config(?:uration)?(?:\s+contents?)?["']?\s*[:=]/i, - DIAGNOSTIC_REDACTED_CONFIG_MARKER, - ], - [/private\s+(?:data|content|message|reply)\s*[:=]/i, DIAGNOSTIC_REDACTED_PRIVATE_MARKER], - ]; - for (const [pattern, marker] of patterns) { - const match = pattern.exec(line); - if (match) { - return { marker, value: line.slice(match.index + match[0].length).trimStart() }; - } - } - return null; -} - -function scanDiagnosticDelimitedValue(text, state = { stack: [], quote: null, escaped: false }) { - for (const character of text) { - if (state.quote) { - if (state.escaped) state.escaped = false; - else if (character === "\\") state.escaped = true; - else if (character === state.quote) state.quote = null; - continue; - } - if (character === '"' || character === "'") { - state.quote = character; - continue; - } - if (character === "{" || character === "[") { - state.stack.push(character); - continue; - } - if (character !== "}" && character !== "]") continue; - const expected = character === "}" ? "{" : "["; - if (state.stack.pop() !== expected) return { complete: false, invalid: true, state }; - if (state.stack.length === 0) return { complete: true, invalid: false, state }; - } - return { complete: false, invalid: false, state }; -} - -export function redactConnectedDiagnostics(text) { - let out = String(text); - out = out.replace( - /-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g, - DIAGNOSTIC_REDACTED_PRIVATE_MARKER, - ); - let blockState = null; - let pendingBlock = false; - let redactRemainder = false; - out = out - .split(/\r?\n/) - .map((line) => { - if (redactRemainder) return ""; - if (blockState) { - const scanned = scanDiagnosticDelimitedValue(line, blockState); - if (scanned.complete) blockState = null; - else if (scanned.invalid) redactRemainder = true; - return ""; - } - if (pendingBlock) { - if (!line.trim()) return ""; - pendingBlock = false; - const value = line.trimStart(); - if (!value.startsWith("{") && !value.startsWith("[")) { - redactRemainder = true; - return ""; - } - const scanned = scanDiagnosticDelimitedValue(value); - if (!scanned.complete) { - if (scanned.invalid) redactRemainder = true; - else blockState = scanned.state; - } - return ""; - } - - const redaction = diagnosticRedactionForLine(line); - if (!redaction) return line; - const { marker, value } = redaction; - if (!value) pendingBlock = true; - else if (value.startsWith("{") || value.startsWith("[")) { - const scanned = scanDiagnosticDelimitedValue(value); - if (!scanned.complete) { - if (scanned.invalid) redactRemainder = true; - else blockState = scanned.state; - } - } else if (marker === DIAGNOSTIC_REDACTED_BODY_MARKER) { - redactRemainder = true; - } - return marker; - }) - .join("\n"); - out = out.replace(/\bBearer\s+[^\s,"']+/gi, "Bearer [REDACTED]"); - out = out.replace(/\bBasic\s+[^\s,"']+/gi, "Basic [REDACTED]"); - const sensitiveKey = - "(?:authorization|x-api-key|api[-_]?key|client[-_]?secret|(?:access|refresh|id|auth)[-_]?token|password|secret|token|session|credential|cookie|set-cookie)"; - out = out.replace( - new RegExp( - String.raw`(["']?${sensitiveKey}["']?\s*[:=]\s*)"(?:\\[^\r\n]|[^"\\\r\n])*(?:"|$)`, - "gim", - ), - '$1"[REDACTED]"', - ); - out = out.replace( - new RegExp( - String.raw`(["']?${sensitiveKey}["']?\s*[:=]\s*)'(?:\\[^\r\n]|[^'\\\r\n])*(?:'|$)`, - "gim", - ), - "$1'[REDACTED]'", - ); - out = out.replace( - new RegExp(`(["']?${sensitiveKey}["']?\\s*[:=]\\s*)([^\\s,}\\r\\n"']+)`, "gi"), - "$1[REDACTED]", - ); - out = out.replace( - /([?&](?:access_token|refresh_token|api[_-]?key|token|key|secret)=)[^&#\s]+/gi, - "$1[REDACTED]", - ); - return out; -} - -function firstMarkerIndex(text, pattern, fromIndex = 0) { - const match = pattern.exec(text.slice(fromIndex)); - return match ? fromIndex + match.index : -1; -} - -function firstAffirmativeMarkerIndex(text, pattern, fromIndex = 0) { - let offset = fromIndex; - for (const line of text.slice(fromIndex).split(/\r?\n/)) { - const match = pattern.exec(line); - const prefix = match ? line.slice(0, match.index) : ""; - const negated = - /\b(?:not|never|no|without|failed to|did not|could not|cannot)(?:\s+\w+){0,2}\s*$/i.test( - prefix, - ); - const suffix = match ? line.slice(match.index + match[0].length) : ""; - const assignment = /^\s*["']?\s*[:=]\s*(.*)$/.exec(suffix); - const affirmative = - !assignment || /^(?:true|"true"|'true')(?:\s|[,;}\]]|$)/i.test(assignment[1]); - if (match && !negated && affirmative) return offset + match.index; - offset += line.length + 1; - } - return -1; -} - -export function classifyWebDiagnostic(text) { - const value = String(text); - const hookIndex = firstMarkerIndex( - value, - /command\.execute\.before|handleDeterministicSlashCommand|injectRawOutput/i, - ); - const invalidIndex = firstMarkerIndex(value, /ProviderModelNotFoundError|Model not found:/i); - if (invalidIndex >= 0 && hookIndex < 0) return WEB_FAILURE_INVALID_MODEL_BEFORE_HOOK; - if (hookIndex < 0) return WEB_FAILURE_UNKNOWN; - - const hookFailureIndex = firstMarkerIndex( - value, - /Failed to inject raw output|Failed to build quota|quota dialog command failed/i, - hookIndex, - ); - if (hookFailureIndex > hookIndex) return WEB_FAILURE_HOOK_ENTRY_BUILD_INJECTION; - - const outputIndex = firstAffirmativeMarkerIndex( - value, - /raw output injected|injected raw output|quota output (?:ready|rendered|written)|slash command output complete/i, - hookIndex, - ); - const busyIndex = firstAffirmativeMarkerIndex( - value, - /session is busy|session busy|session-busy/i, - outputIndex >= 0 ? outputIndex : hookIndex, - ); - if (outputIndex > hookIndex && busyIndex > outputIndex) { - return WEB_FAILURE_POST_INJECTION_BUSY_272; - } - return WEB_FAILURE_UNKNOWN; -} - -export function sliceLogFromOffset(content, offset) { - const raw = String(content); - const start = Number(offset); - if (!Number.isFinite(start) || start <= 0) { - return limitDiagnosticText(raw, WEB_DIAGNOSTIC_SOURCE_MAX_BYTES); - } - const buf = Buffer.from(raw, "utf8"); - if (start >= buf.length) return ""; - return limitDiagnosticText(buf.subarray(start).toString("utf8"), WEB_DIAGNOSTIC_SOURCE_MAX_BYTES); -} - -export function resolveOpenCodeLogDir( - env = process.env, - home = homedir(), - platform = process.platform, -) { - const platformPath = platform === "win32" ? path.win32 : path.posix; - const fallback = - platform === "darwin" - ? platformPath.join(home, "Library", "Application Support") - : platformPath.join(home, ".local", "share"); - const dataBase = env.XDG_DATA_HOME?.trim() || fallback; - return platformPath.join(dataBase, "opencode", "log"); -} - -export function resolveOpenCodeLogDirs( - env = process.env, - home = homedir(), - platform = process.platform, -) { - const platformPath = platform === "win32" ? path.win32 : path.posix; - const dirs = [resolveOpenCodeLogDir(env, home, platform)]; - if (platform === "darwin") { - dirs.push(platformPath.join(home, "Library", "Application Support", "opencode", "log")); - } - return [...new Set(dirs.filter(Boolean))]; -} - -async function listBoundedLogFiles(logDirs) { - const files = []; - for (const logDir of logDirs) { - if (files.length >= WEB_DIAGNOSTIC_MAX_LOG_FILES || !existsSync(logDir)) continue; - let directory; - try { - directory = await opendir(logDir); - for await (const entry of directory) { - if (!entry.isFile()) continue; - files.push(path.join(logDir, entry.name)); - if (files.length >= WEB_DIAGNOSTIC_MAX_LOG_FILES) break; - } - } catch { - // Logs are optional diagnostics. - } - } - return files.sort(compareModelIds); -} - -export async function snapshotLogOffsets(logDirs) { - const offsets = new Map(); - for (const filePath of await listBoundedLogFiles(logDirs)) { - const fileStat = await stat(filePath).catch(() => null); - if (fileStat?.isFile()) offsets.set(filePath, fileStat.size); - } - return offsets; -} - -async function readBoundedLogSlice(filePath, offset) { - const fileStat = await stat(filePath).catch(() => null); - if (!fileStat?.isFile()) return ""; - const requestedStart = Number.isFinite(offset) && offset > 0 ? offset : 0; - const start = fileStat.size < requestedStart ? 0 : requestedStart; - if (start >= fileStat.size) return ""; - const available = fileStat.size - start; - const truncated = available > WEB_DIAGNOSTIC_SOURCE_MAX_BYTES; - const markerBytes = truncated ? Buffer.byteLength(DIAGNOSTIC_TRUNCATED_MARKER) : 0; - const length = Math.min(available, WEB_DIAGNOSTIC_SOURCE_MAX_BYTES - markerBytes); - const handle = await open(filePath, "r"); - try { - const buffer = Buffer.alloc(length); - const { bytesRead } = await handle.read(buffer, 0, length, start); - const suffix = truncated ? DIAGNOSTIC_TRUNCATED_MARKER : ""; - return limitDiagnosticText( - `${buffer.subarray(0, bytesRead).toString("utf8")}${suffix}`, - WEB_DIAGNOSTIC_SOURCE_MAX_BYTES, - ); - } finally { - await handle.close().catch(() => {}); - } -} - -export async function readNewLogRecords(logDirs, offsets) { - let output = ""; - const currentFiles = await listBoundedLogFiles(logDirs); - const files = [...new Set([...offsets.keys(), ...currentFiles])] - .sort(compareModelIds) - .slice(0, WEB_DIAGNOSTIC_MAX_LOG_FILES); - for (const filePath of files) { - const slice = await readBoundedLogSlice(filePath, offsets.get(filePath) ?? 0).catch(() => ""); - if (!slice.trim()) continue; - output = appendBoundedDiagnostic( - output, - `=== ${path.basename(filePath)} ===\n${slice}\n`, - WEB_DIAGNOSTIC_LOG_MAX_BYTES, - ); - if (output.endsWith(DIAGNOSTIC_TRUNCATED_MARKER)) break; - } - return output; -} - -export function formatWebDiagnostics(options) { - const runner = limitDiagnosticText( - redactConnectedDiagnostics(options.runner ?? ""), - WEB_DIAGNOSTIC_SOURCE_MAX_BYTES, - ); - const server = limitDiagnosticText( - redactConnectedDiagnostics(options.server ?? ""), - WEB_DIAGNOSTIC_SOURCE_MAX_BYTES, - ); - const log = limitDiagnosticText( - redactConnectedDiagnostics(options.log ?? ""), - WEB_DIAGNOSTIC_LOG_MAX_BYTES, - ); - const sourceClasses = [runner, server, log].map(classifyWebDiagnostic); - const combinedHasHook = - /command\.execute\.before|handleDeterministicSlashCommand|injectRawOutput/i.test( - `${runner}\n${server}\n${log}`, - ); - const observedClass = sourceClasses.includes(WEB_FAILURE_HOOK_ENTRY_BUILD_INJECTION) - ? WEB_FAILURE_HOOK_ENTRY_BUILD_INJECTION - : sourceClasses.includes(WEB_FAILURE_POST_INJECTION_BUSY_272) - ? WEB_FAILURE_POST_INJECTION_BUSY_272 - : !combinedHasHook && sourceClasses.includes(WEB_FAILURE_INVALID_MODEL_BEFORE_HOOK) - ? WEB_FAILURE_INVALID_MODEL_BEFORE_HOOK - : WEB_FAILURE_UNKNOWN; - const failureClass = options.failureClass ?? observedClass; - const content = [ - "# opencode-quota connected Web diagnostics", - "# bounded to this run; sensitive values redacted; config contents omitted", - `Copied default model: ${formatModelLabel(options.originalModel)}`, - `Temporary Web model: ${formatModelLabel(options.temporaryModel)}`, - `Failure class: ${failureClass}`, - "", - "## Runner", - runner.trim() || "(empty)", - "", - "## Server", - server.trim() || "(empty)", - "", - "## OpenCode log (new bounded records only)", - log.trim() || "(no OpenCode log records for this run)", - "", - ].join("\n"); - return limitDiagnosticText(content, WEB_DIAGNOSTIC_TOTAL_MAX_BYTES); -} - -export async function createWebDiagnosticsFile(options = {}) { - const tmpdirBase = options.tmpdir ?? tmpdir(); - const dir = await mkdtemp(path.join(tmpdirBase, WEB_DIAGNOSTICS_TEMP_PREFIX)); - await chmod(dir, 0o700); - const filePath = path.join(dir, "web-diagnostics.log"); - await writeFile(filePath, "", { encoding: "utf8", mode: 0o600 }); - return { dir, filePath }; -} - -export async function writeWebDiagnosticsFile(filePath, content) { - await writeFile( - filePath, - limitDiagnosticText(redactConnectedDiagnostics(content), WEB_DIAGNOSTIC_TOTAL_MAX_BYTES), - { encoding: "utf8", mode: 0o600 }, - ); - await chmod(filePath, 0o600); - return filePath; -} - -export function formatWebDiagnosticsPath(filePath) { - return `Web diagnostics (sanitized, this run only): ${filePath}`; -} - -export function signalExitCode(signal) { - const number = osConstants.signals[signal]; - return typeof number === "number" ? 128 + number : 130; -} - -export function resolveSourceConfigDir(env = process.env, home = homedir()) { - const configBase = env.XDG_CONFIG_HOME?.trim() || xdgConfig || path.join(home, ".config"); - const defaultDir = path.join(configBase, "opencode"); - const configured = env.OPENCODE_CONFIG_DIR?.trim(); - if (!configured) return defaultDir; - return path.isAbsolute(configured) ? configured : path.resolve(defaultDir, configured); -} - -export function findExecutable(name, env = process.env) { - const pathEnv = env.PATH ?? ""; - const extensions = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""]; - for (const dir of pathEnv.split(path.delimiter)) { - if (!dir) continue; - for (const extension of extensions) { - const candidate = path.join(dir, `${name}${extension}`); - if (existsSync(candidate)) return candidate; - } - } - return null; -} - -export function findOpenCodeExecutable(env = process.env) { - return findExecutable("opencode", env); -} - -export function pluginFileUrls(root = repoRoot) { - return { - server: pathToFileURL(path.resolve(root, "dist", "index.js")).href, - tui: pathToFileURL(path.resolve(root, "dist", "tui.js")).href, - }; -} - -export function getPluginSpecFromEntry(entry) { - const spec = - typeof entry === "string" - ? entry - : Array.isArray(entry) && typeof entry[0] === "string" - ? entry[0] - : null; - if (typeof spec !== "string") return null; - const trimmed = spec.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -export function isQuotaPluginSpec(spec) { - const normalized = spec.replace(/\\/g, "/").toLowerCase(); - if (normalized.includes("@slkiser/opencode-quota")) return true; - if (normalized.includes("opencode-quota/dist/index.js")) return true; - if (normalized.includes("opencode-quota/dist/tui.js")) return true; - if (normalized.includes("opencode-quota/dist/tui.tsx")) return true; - return normalized.includes("/opencode-quota") && !normalized.includes("/opencode-quota/dist/"); -} - -export function rewritePluginList(list, fileUrl) { - let found = false; - const next = []; - for (const entry of list) { - const spec = getPluginSpecFromEntry(entry); - if (spec && isQuotaPluginSpec(spec)) { - if (!found) { - if (typeof entry === "string") next.push(fileUrl); - else if (Array.isArray(entry)) next.push([fileUrl, ...entry.slice(1)]); - else next.push(fileUrl); - found = true; - } - continue; - } - next.push(entry); - } - if (!found) next.push(fileUrl); - return next; -} - -function ensureObject(parent, key) { - const current = parent[key]; - if (current && typeof current === "object" && !Array.isArray(current)) { - return current; - } - parent[key] = {}; - return parent[key]; -} - -export function applyQuotaSurfaceSettings(config, options) { - if (!config || typeof config !== "object" || Array.isArray(config)) { - throw new Error("Quota settings must be a JSON object."); - } - const promptBarEnabled = Boolean(options?.promptBarEnabled); - config.enableToast = true; - ensureObject(config, "tuiSidebarPanel").enabled = true; - ensureObject(config, "tuiCompactStatus").enabled = true; - ensureObject(config, "tuiPromptBar").enabled = promptBarEnabled; - return config; -} - -function stringifyJsonc(data) { - const rendered = stringify(data, null, 2); - return rendered.endsWith("\n") ? rendered : `${rendered}\n`; -} - -function resolveExistingConfigFile(dir, kind) { - const jsoncPath = path.join(dir, `${kind}.jsonc`); - const jsonPath = path.join(dir, `${kind}.json`); - if (existsSync(jsoncPath)) return jsoncPath; - if (existsSync(jsonPath)) return jsonPath; - return null; -} - -function resolveQuotaSettingsPath(dir) { - const jsoncPath = path.join(dir, "opencode-quota", "quota-toast.jsonc"); - const jsonPath = path.join(dir, "opencode-quota", "quota-toast.json"); - if (existsSync(jsoncPath)) return jsoncPath; - if (existsSync(jsonPath)) return jsonPath; - return jsoncPath; -} - -async function readJsoncFile(filePath) { - const content = await readFile(filePath, "utf8"); - try { - return parse(content); - } catch { - throw new Error(`Failed to parse copied ${path.basename(filePath)}.`); - } -} - -async function writeJsoncFile(filePath, data) { - await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 }); - await writeFile(filePath, stringifyJsonc(data), { encoding: "utf8", mode: 0o600 }); -} - -function assertPluginArray(value, label) { - if (!Array.isArray(value)) { - throw new Error(`Cannot update ${label} because plugin is not an array.`); - } - return value; -} - -export async function assertOpenCodeConfigDir(sourceConfigDir) { - const sourceStat = await stat(sourceConfigDir).catch(() => null); - if (!sourceStat || !sourceStat.isDirectory()) { - throw new Error(`OpenCode config directory is missing: ${sourceConfigDir}`); - } - if (!resolveExistingConfigFile(sourceConfigDir, "opencode")) { - throw new Error( - `OpenCode config is missing opencode.json or opencode.jsonc in ${sourceConfigDir}`, - ); - } -} - -export async function transformCopiedConfigDir(configDir, options) { - const urls = pluginFileUrls(options.repoRoot ?? repoRoot); - const promptBarEnabled = Boolean(options.promptBarEnabled); - const ensureTuiConfig = Boolean(options.ensureTuiConfig); - - const mainPath = resolveExistingConfigFile(configDir, "opencode"); - if (!mainPath) { - throw new Error("Copied OpenCode config is missing opencode.json or opencode.jsonc."); - } - - const main = await readJsoncFile(mainPath); - if (!main || typeof main !== "object" || Array.isArray(main)) { - throw new Error("Copied opencode config must be a JSON object."); - } - if (!Object.hasOwn(main, "plugin")) { - main.plugin = []; - } - main.plugin = rewritePluginList(assertPluginArray(main.plugin, "opencode plugin"), urls.server); - if ( - main.tui && - typeof main.tui === "object" && - !Array.isArray(main.tui) && - "plugin" in main.tui - ) { - main.tui.plugin = rewritePluginList( - assertPluginArray(main.tui.plugin, "opencode tui.plugin"), - urls.tui, - ); - } - await writeJsoncFile(mainPath, main); - - let tuiPath = resolveExistingConfigFile(configDir, "tui"); - if (!tuiPath && ensureTuiConfig) { - tuiPath = path.join(configDir, "tui.jsonc"); - await writeJsoncFile(tuiPath, { - $schema: TUI_SCHEMA_URL, - plugin: [urls.tui], - }); - } else if (tuiPath) { - const tui = await readJsoncFile(tuiPath); - if (!tui || typeof tui !== "object" || Array.isArray(tui)) { - throw new Error("Copied tui config must be a JSON object."); - } - if (!Object.hasOwn(tui, "plugin")) { - tui.plugin = []; - } - tui.plugin = rewritePluginList(assertPluginArray(tui.plugin, "tui plugin"), urls.tui); - await writeJsoncFile(tuiPath, tui); - } - - const quotaPath = resolveQuotaSettingsPath(configDir); - let quota = existsSync(quotaPath) ? await readJsoncFile(quotaPath) : {}; - if (!quota || typeof quota !== "object" || Array.isArray(quota)) { - quota = {}; - } - applyQuotaSurfaceSettings(quota, { promptBarEnabled }); - await writeJsoncFile(quotaPath, quota); - - return { mainPath, tuiPath, quotaPath, urls }; -} - -async function walkCopiedTree(destDir, onEntry) { - const entries = await readdir(destDir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(destDir, entry.name); - await onEntry(fullPath, entry); - if (entry.isDirectory() && !entry.isSymbolicLink()) { - await walkCopiedTree(fullPath, onEntry); - } - } -} - -export async function assertCopiedConfigIsolated(sourceDir, destDir) { - const sourceReal = await realpath(sourceDir); - await walkCopiedTree(destDir, async (fullPath, entry) => { - if (entry.isSymbolicLink()) { - throw new Error(`Copied config still contains a symlink (${entry.name}).`); - } - if (!entry.isFile()) return; - const real = await realpath(fullPath); - if (real === sourceReal || real.startsWith(`${sourceReal}${path.sep}`)) { - throw new Error("Copied config is not isolated from the real config directory."); - } - }); -} - -export async function tightenCopiedPermissions(rootDir) { - await chmod(rootDir, 0o700); - await walkCopiedTree(rootDir, async (fullPath, entry) => { - if (entry.isSymbolicLink()) { - throw new Error(`Copied config still contains a symlink (${entry.name}).`); - } - if (entry.isDirectory()) { - await chmod(fullPath, 0o700); - return; - } - if (entry.isFile()) { - await chmod(fullPath, 0o600); - } - }); -} - -export async function copyConfigDirIsolated(sourceDir, destDir) { - await cp(sourceDir, destDir, { - recursive: true, - dereference: true, - errorOnExist: true, - force: false, - }); - await assertCopiedConfigIsolated(sourceDir, destDir); - await tightenCopiedPermissions(destDir); -} - -function isStrictPathInside(inner, outer) { - const base = outer.endsWith(path.sep) ? outer : `${outer}${path.sep}`; - return inner.startsWith(base); -} - -export function isGuardedConnectedTempPath( - resolvedTempRoot, - resolvedTmpdir, - prefix = CONNECTED_TEMP_PREFIX, -) { - if (!resolvedTempRoot || !resolvedTmpdir) return false; - if (resolvedTempRoot === resolvedTmpdir) return false; - if (!isStrictPathInside(resolvedTempRoot, resolvedTmpdir)) return false; - return path.basename(resolvedTempRoot).startsWith(prefix); -} - -export async function prepareConnectedWorkspace(options) { - const sourceConfigDir = options.sourceConfigDir; - await assertOpenCodeConfigDir(sourceConfigDir); - - const tmpdirBase = options.tmpdir ?? tmpdir(); - let tempRoot; - try { - tempRoot = options.tempRoot ?? (await mkdtemp(path.join(tmpdirBase, CONNECTED_TEMP_PREFIX))); - await chmod(tempRoot, 0o700); - const configDir = path.join(tempRoot, "config"); - const projectDir = path.join(tempRoot, "project"); - await mkdir(projectDir, { recursive: true, mode: 0o700 }); - await chmod(projectDir, 0o700); - await copyConfigDirIsolated(sourceConfigDir, configDir); - const transformed = await transformCopiedConfigDir(configDir, { - repoRoot: options.repoRoot ?? repoRoot, - promptBarEnabled: Boolean(options.promptBarEnabled), - ensureTuiConfig: options.ensureTuiConfig !== false, - }); - await tightenCopiedPermissions(tempRoot); - return { tempRoot, configDir, projectDir, sourceConfigDir, tmpdir: tmpdirBase, ...transformed }; - } catch (error) { - if (tempRoot) { - await cleanupConnectedWorkspace(tempRoot, { tmpdir: tmpdirBase }).catch(() => undefined); - } - throw error; - } -} - -export async function cleanupConnectedWorkspace(tempRoot, options = {}) { - if (!tempRoot) return; - const tmpdirBase = options.tmpdir ?? tmpdir(); - let resolvedTempRoot; - let resolvedTmpdir; - try { - resolvedTempRoot = await realpath(tempRoot); - resolvedTmpdir = await realpath(tmpdirBase); - } catch { - return; - } - if (!isGuardedConnectedTempPath(resolvedTempRoot, resolvedTmpdir)) { - throw new Error("Refusing to delete a path outside the expected stabilization temp directory."); - } - await rm(resolvedTempRoot, { recursive: true, force: true }); -} - -function resolvePnpmCommand(root = repoRoot, env = process.env) { - const npmExecPath = env.npm_execpath; - if (npmExecPath && existsSync(npmExecPath)) { - return { command: process.execPath, args: [npmExecPath, "run", "build"] }; - } - const localName = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; - const localBin = path.join(root, "node_modules", ".bin", localName); - if (existsSync(localBin)) { - return { command: localBin, args: ["run", "build"] }; - } - const fromPath = findExecutable("pnpm", env); - if (fromPath) return { command: fromPath, args: ["run", "build"] }; - throw new Error("pnpm is not available. Run this script from the repo with pnpm installed."); -} - -function attachSignalForwarder(source, handler) { - const attached = []; - for (const signal of FORWARD_SIGNALS) { - const onSignal = () => { - handler(signal); - }; - try { - source.on(signal, onSignal); - attached.push([signal, onSignal]); - } catch { - // Unsupported on this platform. - } - } - return () => { - for (const [signal, onSignal] of attached) { - source.off(signal, onSignal); - } - }; -} - -function isChildAlive(child) { - return Boolean(child) && child.exitCode === null && child.signalCode === null; -} - -function signalChild(child, signal, processGroup) { - if (!isChildAlive(child) || !child.pid) return; - if (processGroup && process.platform !== "win32") { - try { - process.kill(-child.pid, signal); - return; - } catch { - // Fall back to a direct child signal if the process group is gone. - } - } - try { - child.kill(signal); - } catch { - try { - child.kill(); - } catch { - // already exited - } - } -} - -const childStopTimers = new WeakMap(); - -export function clearChildStopTimer(child) { - const timer = childStopTimers.get(child); - if (!timer) return; - clearTimeout(timer); - childStopTimers.delete(child); -} - -export function stopChild(child, signal, options = {}) { - if (!isChildAlive(child)) return; - const graceMs = options.graceMs ?? CONNECTED_SIGNAL_GRACE_MS; - const processGroup = options.processGroup ?? process.platform !== "win32"; - signalChild(child, signal, processGroup); - if (childStopTimers.has(child)) return; - const timer = setTimeout(() => { - childStopTimers.delete(child); - signalChild(child, "SIGKILL", processGroup); - }, graceMs); - childStopTimers.set(child, timer); -} - -function killChild(child, signal, options = {}) { - stopChild(child, signal, options); -} - -export function runProcess(command, args, options) { - return new Promise((resolve, reject) => { - const processGroup = process.platform !== "win32"; - const child = spawn(command, args, { - cwd: options.cwd, - env: options.env, - stdio: options.stdio ?? "inherit", - shell: false, - detached: processGroup, - }); - const graceMs = options.signalGraceMs ?? CONNECTED_SIGNAL_GRACE_MS; - options.onSpawn?.(child); - const detach = - options.signalSource === null - ? () => {} - : attachSignalForwarder(options.signalSource ?? process, (signal) => { - stopChild(child, signal, { graceMs, processGroup }); - }); - child.on("error", (error) => { - clearChildStopTimer(child); - detach(); - reject(error); - }); - child.on("close", (code, signal) => { - clearChildStopTimer(child); - detach(); - resolve({ code, signal }); - }); - }); -} - -export function runCapturedProcess(command, args, options = {}) { - return new Promise((resolve, reject) => { - const processGroup = process.platform !== "win32"; - const child = spawn(command, args, { - cwd: options.cwd, - env: options.env, - stdio: options.stdio ?? ["ignore", "pipe", "pipe"], - shell: false, - detached: processGroup, - }); - const graceMs = options.signalGraceMs ?? CONNECTED_SIGNAL_GRACE_MS; - options.onSpawn?.(child); - const detach = - options.signalSource === null - ? () => {} - : attachSignalForwarder(options.signalSource ?? process, (signal) => { - stopChild(child, signal, { graceMs, processGroup }); - }); - let stdout = ""; - let stderr = ""; - const maxCaptureBytes = options.maxCaptureBytes ?? CAPTURED_PROCESS_STREAM_MAX_BYTES; - child.stdout?.on("data", (chunk) => { - const text = String(chunk); - stdout = appendBoundedDiagnostic(stdout, text, maxCaptureBytes); - options.onChunk?.("stdout", text); - options.forward?.stdout?.write(text); - }); - child.stderr?.on("data", (chunk) => { - const text = String(chunk); - stderr = appendBoundedDiagnostic(stderr, text, maxCaptureBytes); - options.onChunk?.("stderr", text); - options.forward?.stderr?.write(text); - }); - child.on("error", (error) => { - clearChildStopTimer(child); - detach(); - reject(error); - }); - child.on("close", (code, signal) => { - clearChildStopTimer(child); - detach(); - resolve({ code, signal, stdout, stderr }); - }); - }); -} - -export async function listOpenCodeModels(options) { - const args = assertNoModelRequest([...OPENCODE_MODELS_ARGS]); - const runCaptured = options.runCaptured ?? runCapturedProcess; - const result = await runCaptured(options.opencodeBin, args, { - cwd: options.cwd, - env: options.env, - signalSource: options.signalSource, - signalGraceMs: options.signalGraceMs, - onSpawn: options.onSpawn, - }); - if ((result.code ?? 0) !== 0 || result.signal) { - const detail = redactConnectedDiagnostics( - limitDiagnosticText( - result.stderr?.trim() || - result.stdout?.trim() || - `opencode models exited ${result.code ?? result.signal}`, - WEB_DIAGNOSTIC_SOURCE_MAX_BYTES, - ), - ); - return { ok: false, args, models: [], detail, signal: result.signal ?? null }; - } - return { - ok: true, - args, - models: parseOpenCodeModelsOutput(result.stdout), - detail: null, - signal: null, - }; -} - -export async function readCopiedDefaultModel(mainPath) { - const main = await readJsoncFile(mainPath); - if (!main || typeof main !== "object" || Array.isArray(main)) { - throw new Error("Copied opencode config must be a JSON object."); - } - return { main, originalModel: normalizeConfiguredModel(main.model) }; -} - -export async function prepareConnectedWebModel(options) { - const { main, originalModel } = await readCopiedDefaultModel(options.mainPath); - const listed = await listOpenCodeModels({ - opencodeBin: options.opencodeBin, - cwd: options.projectDir, - env: buildConnectedChildEnv(options.env, options.configDir), - runCaptured: options.runCaptured, - signalSource: options.signalSource, - signalGraceMs: options.signalGraceMs, - onSpawn: options.onSpawn, - }); - if (!listed.ok) { - return { - ok: false, - reason: "models-command-failed", - signal: listed.signal, - originalModel, - temporaryModel: originalModel, - catalog: [], - replaced: false, - spawnedArgs: listed.args, - guidance: formatConnectedWebModelGuidance({ - originalModel, - catalog: [], - reason: "models-command-failed", - detail: listed.detail, - }), - }; - } - const choice = chooseConnectedWebModel(originalModel, listed.models); - if (choice.status === "empty-catalog") { - const reason = listed.models.length > 0 ? "unusable-catalog" : "empty-catalog"; - return { - ok: false, - reason, - signal: null, - originalModel, - temporaryModel: originalModel, - catalog: listed.models, - replaced: false, - spawnedArgs: listed.args, - guidance: formatConnectedWebModelGuidance({ - originalModel, - catalog: listed.models, - reason, - }), - }; - } - if (choice.status === "replace") { - main.model = choice.model; - await writeJsoncFile(options.mainPath, main); - } - return { - ok: true, - reason: choice.status === "keep" ? "valid-model" : "stale-model", - signal: null, - originalModel, - temporaryModel: choice.model, - catalog: listed.models, - replaced: choice.status === "replace", - spawnedArgs: listed.args, - guidance: null, - }; -} - -export function buildConnectedChildEnv(env, configDir) { - const childEnv = { - ...env, - OPENCODE_CONFIG_DIR: configDir, - }; - delete childEnv.OPENCODE_CONFIG; - return childEnv; -} - -async function confirmPromptBarStage(stdio) { - const input = stdio?.stdin ?? process.stdin; - const output = stdio?.stdout ?? process.stdout; - if (!input.isTTY || !output.isTTY) { - output.write( - "Skipping prompt-bar stage because this session is not a TTY. Re-run with --prompt-bar.\n", - ); - return false; - } - const rl = createInterface({ input, output }); - try { - const answer = ( - await rl.question("Launch a second TUI session with the prompt bar enabled? [Y/n] ") - ) - .trim() - .toLowerCase(); - return answer === "" || answer === "y" || answer === "yes"; - } finally { - rl.close(); - } -} - -function printPlan(options) { - const stream = options.stderr ?? process.stderr; - stream.write(`${CONNECTED_CREDENTIAL_WARNING}\n`); - stream.write(`Mode: ${options.mode}${options.promptBarEnabled ? " (prompt bar enabled)" : ""}\n`); - if (options.promptBarEnabled) { - stream.write( - "Prompt bar: fixed 12-cell fill. Check provider identity and placement/clipping on resize, not bar growth.\n", - ); - } - stream.write(`Source config dir: ${options.sourceConfigDir}\n`); - stream.write(`OpenCode: ${options.opencodeBin}\n`); - stream.write(`Server plugin: ${options.urls.server}\n`); - stream.write(`TUI plugin: ${options.urls.tui}\n`); - if (options.configDir) stream.write(`Temp config dir: ${options.configDir}\n`); - if (options.projectDir) stream.write(`Temp project dir: ${options.projectDir}\n`); -} - -export async function runConnected(argv, io = {}) { - const stdout = io.stdout ?? process.stdout; - const stderr = io.stderr ?? process.stderr; - const env = io.env ?? process.env; - let args; - try { - args = parseConnectedArgs(argv); - } catch (error) { - stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); - return 2; - } - if (args.help) { - stdout.write(`${getConnectedUsage()}\n`); - return 0; - } - - const sourceConfigDir = resolveSourceConfigDir(env); - try { - await assertOpenCodeConfigDir(sourceConfigDir); - } catch (error) { - stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); - return 1; - } - const opencodeBin = findOpenCodeExecutable(env); - if (!opencodeBin) { - stderr.write("OpenCode executable was not found on PATH.\n"); - return 1; - } - const urls = pluginFileUrls(io.repoRoot ?? repoRoot); - - if (args.dryRun) { - printPlan({ - mode: args.mode, - promptBarEnabled: args.promptBar, - sourceConfigDir, - opencodeBin, - urls, - stderr, - }); - stderr.write("Dry run: no temp copy, build, or OpenCode launch.\n"); - return 0; - } - - const tmpdirBase = io.tmpdir ?? tmpdir(); - const signalGraceMs = io.signalGraceMs ?? CONNECTED_SIGNAL_GRACE_MS; - const signalSource = io.signalSource ?? process; - const createDiagnostics = io.createWebDiagnosticsFile ?? createWebDiagnosticsFile; - const writeDiagnostics = io.writeWebDiagnosticsFile ?? writeWebDiagnosticsFile; - const readLogs = io.readNewLogRecords ?? readNewLogRecords; - let workspace; - let cleaned = false; - let activeChild = null; - let interruptSignal = null; - let webDiagnostics = null; - let webDiagnosticsFlushed = false; - let webDiagnosticsFlushPromise = null; - let webDiagnosticsFailureReported = false; - let webRunner = ""; - let webServer = ""; - let webLogDirs = []; - let webLogOffsets = new Map(); - let webOriginalModel = null; - let webTemporaryModel = null; - const runnerStream = { - write(chunk) { - const text = String(chunk); - if (args.mode === "web") { - webRunner = appendBoundedDiagnostic(webRunner, text, WEB_DIAGNOSTIC_SOURCE_MAX_BYTES); - } - return stderr.write(text); - }, - }; - const reportDiagnosticsFailure = (error) => { - if (webDiagnosticsFailureReported) return; - webDiagnosticsFailureReported = true; - const detail = redactConnectedDiagnostics( - limitDiagnosticText( - error instanceof Error ? error.message : String(error), - WEB_DIAGNOSTIC_SOURCE_MAX_BYTES, - ), - ); - try { - stderr.write(`Web diagnostics unavailable (run result preserved): ${detail}\n`); - } catch { - // Diagnostics reporting is also best-effort. - } - }; - const flushWebDiagnostics = async () => { - if (!webDiagnostics || webDiagnosticsFlushed) return false; - if (webDiagnosticsFlushPromise) return webDiagnosticsFlushPromise; - webDiagnosticsFlushPromise = (async () => { - try { - const log = await readLogs(webLogDirs, webLogOffsets); - const content = formatWebDiagnostics({ - originalModel: webOriginalModel, - temporaryModel: webTemporaryModel, - runner: webRunner, - server: webServer, - log, - }); - await writeDiagnostics(webDiagnostics.filePath, content); - stderr.write(`${formatWebDiagnosticsPath(webDiagnostics.filePath)}\n`); - webDiagnosticsFlushed = true; - return true; - } catch (error) { - reportDiagnosticsFailure(error); - return false; - } finally { - webDiagnosticsFlushPromise = null; - } - })(); - return webDiagnosticsFlushPromise; - }; - const cleanup = async () => { - if (cleaned || !workspace) return; - cleaned = true; - await cleanupConnectedWorkspace(workspace.tempRoot, { - tmpdir: workspace.tmpdir ?? tmpdirBase, - }); - }; - const onSignal = (signal) => { - if (!interruptSignal) interruptSignal = signal; - killChild(activeChild, signal, { graceMs: signalGraceMs }); - }; - const detachSignals = attachSignalForwarder(signalSource, onSignal); - - try { - if (args.mode === "web") { - try { - webDiagnostics = await createDiagnostics({ tmpdir: tmpdirBase }); - } catch (error) { - reportDiagnosticsFailure(error); - } - } - const pnpm = resolvePnpmCommand(io.repoRoot ?? repoRoot, env); - const build = await runProcess(pnpm.command, pnpm.args, { - cwd: io.repoRoot ?? repoRoot, - env, - signalGraceMs, - onSpawn(child) { - activeChild = child; - }, - }); - activeChild = null; - if (interruptSignal) { - await flushWebDiagnostics(); - await cleanup(); - return signalExitCode(interruptSignal); - } - if (build.code !== 0) { - await flushWebDiagnostics(); - return build.code ?? 1; - } - if (!existsSync(path.resolve(io.repoRoot ?? repoRoot, "dist", "index.js"))) { - runnerStream.write("Build did not produce dist/index.js.\n"); - await flushWebDiagnostics(); - return 1; - } - if ( - args.mode === "tui" && - !existsSync(path.resolve(io.repoRoot ?? repoRoot, "dist", "tui.js")) - ) { - stderr.write("Build did not produce dist/tui.js.\n"); - return 1; - } - - workspace = await prepareConnectedWorkspace({ - sourceConfigDir, - repoRoot: io.repoRoot ?? repoRoot, - promptBarEnabled: args.promptBar, - ensureTuiConfig: args.mode === "tui", - tmpdir: tmpdirBase, - }); - if (interruptSignal) { - await flushWebDiagnostics(); - await cleanup(); - return signalExitCode(interruptSignal); - } - printPlan({ - mode: args.mode, - promptBarEnabled: args.promptBar, - sourceConfigDir, - opencodeBin, - urls: workspace.urls, - configDir: workspace.configDir, - projectDir: workspace.projectDir, - stderr: runnerStream, - }); - - const childEnv = buildConnectedChildEnv(env, workspace.configDir); - const launchArgs = assertNoModelRequest(connectedLaunchArgs(args.mode)); - if (args.mode === "web") { - webLogDirs = resolveOpenCodeLogDirs( - env, - io.home ?? homedir(), - io.platform ?? process.platform, - ); - webLogOffsets = await snapshotLogOffsets(webLogDirs); - if (interruptSignal) { - await flushWebDiagnostics(); - await cleanup(); - return signalExitCode(interruptSignal); - } - const modelPrep = await prepareConnectedWebModel({ - opencodeBin, - configDir: workspace.configDir, - projectDir: workspace.projectDir, - env, - mainPath: workspace.mainPath, - runCaptured: io.runCaptured, - signalSource: null, - signalGraceMs, - onSpawn(child) { - activeChild = child; - }, - }); - activeChild = null; - const preflightSignal = interruptSignal ?? modelPrep.signal; - if (preflightSignal) { - await flushWebDiagnostics(); - await cleanup(); - return signalExitCode(preflightSignal); - } - webOriginalModel = modelPrep.originalModel; - webTemporaryModel = modelPrep.temporaryModel; - runnerStream.write(`${formatWebModelChoice(modelPrep)}\n`); - if (modelPrep.replaced) { - await tightenCopiedPermissions(workspace.tempRoot); - } - if (!modelPrep.ok) { - runnerStream.write(`${modelPrep.guidance}\n`); - await flushWebDiagnostics(); - await cleanup(); - return 1; - } - if (interruptSignal) { - await flushWebDiagnostics(); - await cleanup(); - return signalExitCode(interruptSignal); - } - const first = await runCapturedProcess(opencodeBin, launchArgs, { - cwd: workspace.projectDir, - env: childEnv, - stdio: ["inherit", "pipe", "pipe"], - forward: { stdout, stderr }, - signalGraceMs, - signalSource: null, - onSpawn(child) { - activeChild = child; - }, - onChunk(_stream, text) { - webServer = appendBoundedDiagnostic(webServer, text, WEB_DIAGNOSTIC_SOURCE_MAX_BYTES); - }, - }); - activeChild = null; - await flushWebDiagnostics(); - await cleanup(); - if (interruptSignal) return signalExitCode(interruptSignal); - return first.code ?? (first.signal ? signalExitCode(first.signal) : 0); - } - const first = await runProcess(opencodeBin, launchArgs, { - cwd: workspace.projectDir, - env: childEnv, - signalGraceMs, - onSpawn(child) { - activeChild = child; - }, - }); - activeChild = null; - if (interruptSignal) { - await cleanup(); - return signalExitCode(interruptSignal); - } - if (args.mode === "tui" && !args.promptBar && first.signal == null && first.code === 0) { - const shouldLaunchPromptBar = await confirmPromptBarStage({ - stdin: io.stdin ?? process.stdin, - stdout, - }); - if (interruptSignal) { - await cleanup(); - return signalExitCode(interruptSignal); - } - if (shouldLaunchPromptBar) { - await transformCopiedConfigDir(workspace.configDir, { - repoRoot: io.repoRoot ?? repoRoot, - promptBarEnabled: true, - ensureTuiConfig: true, - }); - await tightenCopiedPermissions(workspace.tempRoot); - stderr.write( - "Launching second TUI session with the prompt bar enabled. The bar is fixed at 12 cells; check provider identity and placement/clipping on resize, not bar growth.\n", - ); - const second = await runProcess(opencodeBin, [], { - cwd: workspace.projectDir, - env: childEnv, - signalGraceMs, - onSpawn(child) { - activeChild = child; - }, - }); - activeChild = null; - await cleanup(); - if (interruptSignal) return signalExitCode(interruptSignal); - return second.code ?? (second.signal ? 1 : 0); - } - } - await cleanup(); - return first.code ?? (first.signal ? 1 : 0); - } catch (error) { - const message = `${error instanceof Error ? error.message : String(error)}\n`; - runnerStream.write(message); - await flushWebDiagnostics(); - await cleanup(); - return interruptSignal ? signalExitCode(interruptSignal) : 1; - } finally { - detachSignals(); - await flushWebDiagnostics(); - await cleanup(); - } -} - -function isDirectExecution() { - const entry = process.argv[1]; - if (!entry) return false; - try { - return import.meta.url === pathToFileURL(path.resolve(entry)).href; - } catch { - return false; - } -} - -if (isDirectExecution()) { - const code = await runConnected(process.argv.slice(2)); - process.exit(code); -} diff --git a/scripts/test-stabilization-fixtures.mjs b/scripts/test-stabilization-fixtures.mjs deleted file mode 100644 index b35b7510..00000000 --- a/scripts/test-stabilization-fixtures.mjs +++ /dev/null @@ -1,445 +0,0 @@ -#!/usr/bin/env node -/** - * Maintainer fake-data gate for the 4.x stabilization suites. - * - * This runner must not use real credentials, network provider calls, or the - * real `bl` executable. It spawns this repo's Vitest CLI with a frozen file - * list and no shell. Extra files or tests that would escape mocks are rejected. - * - * Before spawning Vitest, it creates a 0700 temp HOME/XDG sandbox, strips known - * provider credential/session env vars and OPENCODE_CONFIG/OPENCODE_CONFIG_DIR, - * then deletes the sandbox on success, error, or forwarded signals. - */ -import { spawn } from "node:child_process"; -import { existsSync } from "node:fs"; -import { chmod, mkdir, mkdtemp, readFile, realpath, rm } from "node:fs/promises"; -import { constants as osConstants, tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -const scriptPath = fileURLToPath(import.meta.url); -export const repoRoot = path.resolve(path.dirname(scriptPath), ".."); - -export const FIXTURE_TEMP_PREFIX = "opencode-quota-fixtures-"; -export const FORWARD_SIGNALS = Object.freeze(["SIGINT", "SIGTERM", "SIGHUP"]); - -export const FIXTURE_STRIP_ENV_VARS = Object.freeze([ - "OPENCODE_CONFIG", - "OPENCODE_CONFIG_DIR", - "OPENCODE_API_KEY", - "OPENCODE_GO_AUTH_COOKIE", - "OPENCODE_GO_WORKSPACE_ID", - "OPENCODE_AGY_PROJECT_ID", - "OPENCODE_GEMINI_PROJECT_ID", - "OPENROUTER_API_KEY", - "SYNTHETIC_API_KEY", - "CHUTES_API_KEY", - "DEEPSEEK_API_KEY", - "KILO_API_KEY", - "KIMI_API_KEY", - "KIMI_CODE_API_KEY", - "NANOGPT_API_KEY", - "NANO_GPT_API_KEY", - "OLLAMA_API_KEY", - "ALIBABA_CODING_PLAN_API_KEY", - "ALIBABA_API_KEY", - "MINIMAX_CODING_PLAN_API_KEY", - "MINIMAX_API_KEY", - "MINIMAX_CHINA_CODING_PLAN_API_KEY", - "ZAI_API_KEY", - "ZAI_CODING_PLAN_API_KEY", - "ZHIPU_API_KEY", - "ZHIPU_CODING_PLAN_API_KEY", - "MIMO_USAGE_COOKIE", - "CURSOR_ACP_HOME_DIR", - "GOOGLE_CLOUD_PROJECT", - "GOOGLE_CLOUD_PROJECT_ID", -]); - -export const FIXTURE_TEST_FILES = Object.freeze([ - "tests/lib.config-symlink-write.test.ts", - "tests/lib.config-write-target.test.ts", - "tests/lib.atomic-json.test.ts", - "tests/lib.opencode-go.test.ts", - "tests/providers.opencode-go.test.ts", - "tests/providers.opencode-go.surfaces.test.ts", - "tests/providers.synthetic.test.ts", - "tests/providers.synthetic.surfaces.test.ts", - "tests/lib.openrouter.test.ts", - "tests/providers.openrouter.test.ts", - "tests/providers.openrouter.surfaces.test.ts", - "tests/lib.alibaba-token-plan.test.ts", - "tests/providers.alibaba-token-plan.test.ts", - "tests/lib.quota-status.test.ts", - "tests/quota-render-data.test.ts", - "tests/tui-runtime.test.ts", - "tests/tui-prompt-bar-format.test.ts", - "tests/lib.quota-export.test.ts", - "tests/lib.quota-export.production-policy.test.ts", - "tests/lib.api-key-provider-queries.test.ts", - "tests/lib.provider-api-key-configs.test.ts", - "tests/lib.api-key-resolver.test.ts", - "tests/contribution-guidance.test.ts", -]); - -const ESCAPE_CHECKS = [ - { - id: "real-bl-spawn", - message: "would spawn the real bl executable", - regex: - /\b(?:spawn|spawnSync|execFile|execFileSync)\s*\(\s*(?:ALIBABA_TOKEN_PLAN_COMMAND|["'`]bl["'`])/, - }, - { - id: "shell-true", - message: "would run a shell, which can escape the frozen Vitest argv", - regex: /\bshell\s*:\s*true\b/, - }, - { - id: "live-openrouter-fetch", - message: "would make a live OpenRouter network call", - regex: /\bfetch\s*\(\s*[`'"]https:\/\/openrouter\.ai/, - }, - { - id: "live-http-client", - message: "would use a live HTTP client outside the mocked provider boundary", - regex: - /\b(?:from\s+["'](?:axios|got|undici|node-fetch)["']|require\(\s*["'](?:axios|got|undici|node-fetch)["']\))/, - }, -]; - -export function getFixturesUsage() { - return `Usage: - node scripts/test-stabilization-fixtures.mjs [--dry-run] - node scripts/test-stabilization-fixtures.mjs --help - -Runs the frozen fake-data Vitest set for config symlinks, config write targets, -atomic JSON, OpenCode Go, Synthetic empty responses/surfaces, OpenRouter -diagnostics/surfaces, Alibaba Token Plan process/provider, quota status, prompt -selection, prompt-bar identity, TUI runtime, quota export, API-key query/config, and contribution guidance. - -Does not use real credentials, network provider calls, or the real bl executable. -Vitest runs in a 0700 temp HOME/XDG sandbox with known credential/session env vars -and OPENCODE_CONFIG/OPENCODE_CONFIG_DIR stripped.`; -} - -export function parseFixturesArgs(argv) { - const args = { dryRun: false, help: false }; - for (const arg of argv) { - if (arg === "--help" || arg === "-h") { - args.help = true; - continue; - } - if (arg === "--dry-run") { - args.dryRun = true; - continue; - } - throw new Error(`Unknown argument: ${arg}\n\n${getFixturesUsage()}`); - } - return args; -} - -export function resolveVitestExecutable(root = repoRoot) { - const vitestCli = path.join(root, "node_modules", "vitest", "vitest.mjs"); - if (!existsSync(vitestCli)) { - throw new Error(`Vitest CLI is missing: ${vitestCli}`); - } - return vitestCli; -} - -export function buildVitestArgs(files = FIXTURE_TEST_FILES) { - return ["run", ...files]; -} - -export function assertFrozenFixtureFiles(files = FIXTURE_TEST_FILES) { - if (files.length !== FIXTURE_TEST_FILES.length) { - throw new Error("Fixture file list does not match the frozen fake-data allowlist."); - } - for (const [index, file] of files.entries()) { - if (file !== FIXTURE_TEST_FILES[index]) { - throw new Error(`Unexpected fixture test path: ${file}`); - } - if (path.isAbsolute(file) || file.includes("..") || file.includes("\\")) { - throw new Error(`Fixture test path is not a repo-relative POSIX path: ${file}`); - } - } -} - -export function assertFixtureContentsAreIsolated(contentsByFile) { - for (const file of Object.keys(contentsByFile)) { - if (!FIXTURE_TEST_FILES.includes(file)) { - throw new Error( - `Refusing to run a test file outside the frozen fake-data allowlist: ${file}`, - ); - } - } - for (const file of FIXTURE_TEST_FILES) { - const contents = contentsByFile[file]; - if (typeof contents !== "string") { - throw new Error(`Missing fixture test contents: ${file}`); - } - for (const check of ESCAPE_CHECKS) { - if (check.regex.test(contents)) { - throw new Error(`${file} ${check.message}.`); - } - } - } -} - -export async function assertFixtureTestsAreIsolated(root = repoRoot, files = FIXTURE_TEST_FILES) { - assertFrozenFixtureFiles(files); - const contentsByFile = {}; - for (const file of files) { - const fullPath = path.join(root, file); - if (!existsSync(fullPath)) { - throw new Error(`Frozen fixture test is missing: ${file}`); - } - contentsByFile[file] = await readFile(fullPath, "utf8"); - } - assertFixtureContentsAreIsolated(contentsByFile); - return { - command: process.execPath, - args: [resolveVitestExecutable(root), ...buildVitestArgs(files)], - cwd: root, - shell: false, - }; -} - -function describeCommand(command) { - return [command.command, ...command.args].join(" "); -} - -function isStrictPathInside(inner, outer) { - const base = outer.endsWith(path.sep) ? outer : `${outer}${path.sep}`; - return inner.startsWith(base); -} - -export function isGuardedFixtureTempPath( - resolvedTempRoot, - resolvedTmpdir, - prefix = FIXTURE_TEMP_PREFIX, -) { - if (!resolvedTempRoot || !resolvedTmpdir) return false; - if (resolvedTempRoot === resolvedTmpdir) return false; - if (!isStrictPathInside(resolvedTempRoot, resolvedTmpdir)) return false; - return path.basename(resolvedTempRoot).startsWith(prefix); -} - -export async function prepareFixtureSandbox(options = {}) { - const tmpdirBase = options.tmpdir ?? tmpdir(); - let tempRoot; - try { - tempRoot = await mkdtemp(path.join(tmpdirBase, FIXTURE_TEMP_PREFIX)); - await chmod(tempRoot, 0o700); - const home = path.join(tempRoot, "home"); - const config = path.join(tempRoot, "config"); - const data = path.join(tempRoot, "data"); - const state = path.join(tempRoot, "state"); - const cache = path.join(tempRoot, "cache"); - for (const dir of [home, config, data, state, cache]) { - await mkdir(dir, { recursive: true, mode: 0o700 }); - await chmod(dir, 0o700); - } - return { tempRoot, home, config, data, state, cache, tmpdir: tmpdirBase }; - } catch (error) { - if (tempRoot) { - await cleanupFixtureSandbox(tempRoot, { tmpdir: tmpdirBase }).catch(() => undefined); - } - throw error; - } -} - -export async function cleanupFixtureSandbox(tempRoot, options = {}) { - if (!tempRoot) return; - const tmpdirBase = options.tmpdir ?? tmpdir(); - let resolvedTempRoot; - let resolvedTmpdir; - try { - resolvedTempRoot = await realpath(tempRoot); - resolvedTmpdir = await realpath(tmpdirBase); - } catch { - return; - } - if (!isGuardedFixtureTempPath(resolvedTempRoot, resolvedTmpdir)) { - throw new Error("Refusing to delete a path outside the expected fixture temp directory."); - } - await rm(resolvedTempRoot, { recursive: true, force: true }); -} - -export function buildFixtureChildEnv(env, sandbox) { - const childEnv = { ...env }; - childEnv.HOME = sandbox.home; - childEnv.USERPROFILE = sandbox.home; - childEnv.XDG_CONFIG_HOME = sandbox.config; - childEnv.XDG_DATA_HOME = sandbox.data; - childEnv.XDG_STATE_HOME = sandbox.state; - childEnv.XDG_CACHE_HOME = sandbox.cache; - childEnv.APPDATA = sandbox.config; - childEnv.LOCALAPPDATA = sandbox.cache; - for (const key of FIXTURE_STRIP_ENV_VARS) { - delete childEnv[key]; - } - return childEnv; -} - -export function signalExitCode(signal) { - const number = osConstants.signals[signal]; - return typeof number === "number" ? 128 + number : 130; -} - -function attachSignalForwarder(source, handler) { - const attached = []; - for (const signal of FORWARD_SIGNALS) { - const onSignal = () => { - handler(signal); - }; - try { - source.on(signal, onSignal); - attached.push([signal, onSignal]); - } catch { - // Unsupported on this platform. - } - } - return () => { - for (const [signal, onSignal] of attached) { - source.off(signal, onSignal); - } - }; -} - -function isChildAlive(child) { - return Boolean(child) && child.exitCode === null && child.signalCode === null; -} - -function killChild(child, signal) { - if (!isChildAlive(child)) return; - try { - child.kill(signal); - } catch { - try { - child.kill(); - } catch { - // already exited - } - } -} - -export async function runFixtures(argv, io = {}) { - const stdout = io.stdout ?? process.stdout; - const stderr = io.stderr ?? process.stderr; - let args; - try { - args = parseFixturesArgs(argv); - } catch (error) { - stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); - return 2; - } - if (args.help) { - stdout.write(`${getFixturesUsage()}\n`); - return 0; - } - - let command; - try { - command = await assertFixtureTestsAreIsolated(io.repoRoot ?? repoRoot); - } catch (error) { - stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); - return 1; - } - - stderr.write( - "Fake-data invariant: no real credentials, network provider calls, or real bl executable.\n", - ); - stderr.write(`${describeCommand(command)}\n`); - if (args.dryRun) { - stderr.write("Dry run: isolation check passed; Vitest was not launched.\n"); - return 0; - } - - const tmpdirBase = io.tmpdir ?? tmpdir(); - const parentEnv = io.env ?? process.env; - const spawnImpl = io.spawn ?? spawn; - let sandbox; - let cleaned = false; - let activeChild = null; - let interruptSignal = null; - const cleanup = async () => { - if (cleaned || !sandbox) return; - cleaned = true; - await cleanupFixtureSandbox(sandbox.tempRoot, { tmpdir: sandbox.tmpdir ?? tmpdirBase }); - }; - const onSignal = (signal) => { - interruptSignal = signal; - killChild(activeChild, signal); - }; - const detachSignals = attachSignalForwarder(io.signalSource ?? process, onSignal); - - try { - sandbox = await prepareFixtureSandbox({ tmpdir: tmpdirBase }); - if (interruptSignal) { - await cleanup(); - return signalExitCode(interruptSignal); - } - const childEnv = buildFixtureChildEnv(parentEnv, sandbox); - return await new Promise((resolve) => { - let child; - try { - child = spawnImpl(command.command, command.args, { - cwd: command.cwd, - env: childEnv, - stdio: io.stdio ?? "inherit", - shell: false, - }); - } catch (error) { - cleanup() - .catch(() => undefined) - .finally(() => { - stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); - resolve(1); - }); - return; - } - activeChild = child; - child.on("error", (error) => { - activeChild = null; - stderr.write(`${error.message}\n`); - cleanup() - .catch(() => undefined) - .finally(() => resolve(1)); - }); - child.on("close", (code, signal) => { - activeChild = null; - cleanup() - .catch(() => undefined) - .finally(() => { - if (interruptSignal) { - resolve(signalExitCode(interruptSignal)); - return; - } - resolve(code ?? (signal ? 1 : 0)); - }); - }); - }); - } catch (error) { - await cleanup(); - stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); - return 1; - } finally { - detachSignals(); - await cleanup(); - } -} - -function isDirectExecution() { - const entry = process.argv[1]; - if (!entry) return false; - try { - return import.meta.url === pathToFileURL(path.resolve(entry)).href; - } catch { - return false; - } -} - -if (isDirectExecution()) { - const code = await runFixtures(process.argv.slice(2)); - process.exit(code); -} diff --git a/src/lib/config-file-utils.ts b/src/lib/config-file-utils.ts index 3aca95f7..4dfd5881 100644 --- a/src/lib/config-file-utils.ts +++ b/src/lib/config-file-utils.ts @@ -220,6 +220,6 @@ export function isQuotaPluginSpec(spec: string, kind: ConfigFileKind): boolean { } return kind === "tui" - ? normalized.includes("opencode-quota/dist/tui.tsx") + ? normalized.includes("opencode-quota/dist/tui.js") : normalized.includes("opencode-quota/dist/index.js"); } diff --git a/src/lib/tui-line-style.ts b/src/lib/tui-line-style.ts deleted file mode 100644 index 6d015ff8..00000000 --- a/src/lib/tui-line-style.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TuiPluginApi } from "@opencode-ai/plugin/tui"; - -import { SESSION_TOKEN_SECTION_HEADING } from "./session-tokens-format.js"; - -export function getSidebarBodyLineColor( - line: string, - theme: Pick, -): TuiPluginApi["theme"]["current"]["text"] | TuiPluginApi["theme"]["current"]["textMuted"] { - return line.length > 0 && SESSION_TOKEN_SECTION_HEADING.startsWith(line) - ? theme.text - : theme.textMuted; -} diff --git a/src/lib/tui-native-provider-quota.ts b/src/lib/tui-native-provider-quota.ts deleted file mode 100644 index eeb1b6dc..00000000 --- a/src/lib/tui-native-provider-quota.ts +++ /dev/null @@ -1,25 +0,0 @@ -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object"; -} - -function isQuotaApiLike(value: unknown): boolean { - return Boolean(value) && (typeof value === "object" || typeof value === "function"); -} - -/** - * Detect whether an OpenCode client advertises native provider-quota support. - * - * This is intentionally a no-fetch duck-typing guard for slot-registration decisions. - */ -export function hasNativeProviderQuotaClient(client: unknown): boolean { - if (!isRecord(client)) return false; - - const experimental = client.experimental; - if (!isRecord(experimental)) return false; - - if (isQuotaApiLike(experimental.providerQuota)) return true; - if (isQuotaApiLike(experimental.provider_quota)) return true; - - const provider = experimental.provider; - return isRecord(provider) && isQuotaApiLike(provider.quota); -} diff --git a/src/lib/tui-refresh-lifecycle.ts b/src/lib/tui-refresh-lifecycle.ts deleted file mode 100644 index a34d423d..00000000 --- a/src/lib/tui-refresh-lifecycle.ts +++ /dev/null @@ -1,102 +0,0 @@ -export interface TuiRefreshLifecycle { - reload: () => void; - retain: () => void; - release: () => void; -} - -export interface TuiRefreshLifecycleOptions { - load: () => Promise; - apply: (value: T) => void; - afterApply?: (value: T) => void; - intervalMs: number; - eventRefreshDelaysMs: readonly number[]; - recoveryDelaysMs?: readonly number[]; - subscribe: (scheduleRefresh: () => void) => Array<() => void>; - onDispose: () => void; -} - -export function createTuiRefreshLifecycle( - options: TuiRefreshLifecycleOptions, -): TuiRefreshLifecycle { - let refCount = 0; - let disposed = false; - let loadVersion = 0; - let inFlight = false; - let queued = false; - const timers = new Set>(); - - const reload = () => { - if (disposed) return; - - if (inFlight) { - queued = true; - return; - } - - inFlight = true; - const currentVersion = ++loadVersion; - - void options - .load() - .then((next) => { - if (disposed || currentVersion !== loadVersion) return; - options.apply(next); - options.afterApply?.(next); - }) - .catch(() => { - if (disposed || currentVersion !== loadVersion) return; - }) - .finally(() => { - if (disposed) return; - inFlight = false; - if (queued) { - queued = false; - reload(); - } - }); - }; - - const queueRefresh = (delay: number) => { - if (disposed) return; - - const timer = setTimeout(() => { - timers.delete(timer); - reload(); - }, delay); - timers.add(timer); - }; - - const scheduleRefresh = () => { - for (const delay of options.eventRefreshDelaysMs) queueRefresh(delay); - }; - - const interval = setInterval(reload, options.intervalMs); - const unsubscribers = options.subscribe(scheduleRefresh); - - const dispose = () => { - if (disposed) return; - - disposed = true; - clearInterval(interval); - for (const timer of timers) clearTimeout(timer); - timers.clear(); - for (const unsubscribe of unsubscribers) unsubscribe(); - options.onDispose(); - }; - - const lifecycle: TuiRefreshLifecycle = { - reload, - retain: () => { - refCount += 1; - }, - release: () => { - refCount -= 1; - if (refCount <= 0) dispose(); - }, - }; - - reload(); - for (const delay of options.recoveryDelaysMs ?? []) queueRefresh(delay); - - return lifecycle; -} diff --git a/src/lib/tui-runtime.ts b/src/lib/tui-runtime.ts deleted file mode 100644 index d8170fcf..00000000 --- a/src/lib/tui-runtime.ts +++ /dev/null @@ -1,721 +0,0 @@ -import type { TuiPluginApi } from "@opencode-ai/plugin/tui"; -import { type RuntimeContextRootHints, resolveRuntimeContextRoots } from "./config-file-utils.js"; -import type { AccountingWindow } from "./entries.js"; -import { groupQuotaEntries } from "./grouped-entry-normalization.js"; -import { - BUNDLED_MAINTAINER_ANNOUNCEMENTS, - formatMaintainerAnnouncementHomeCountLine, - getMaintainerAnnouncementsSummary, - getMaintainerAnnouncementTargetProviderIds, - type MaintainerAnnouncement, -} from "./maintainer-announcements.js"; -import { getQuotaProviderShape, normalizeQuotaProviderId } from "./provider-metadata.js"; -import { projectQuotaProviderResults } from "./quota-accounting-projection.js"; -import { - buildQuotaExport, - createExportProviderContext, - resolveExportPath, - writeQuotaExport, -} from "./quota-export.js"; -import { resolveQuotaFormatStyle } from "./quota-format-style.js"; -import type { CollectQuotaRenderDataResult, SessionModelMeta } from "./quota-render-data.js"; -import { collectConcreteEnabledProviderIds, collectQuotaRenderData } from "./quota-render-data.js"; -import type { QuotaRuntimeContext } from "./quota-runtime-context.js"; -import { - createQuotaProviderRuntimeContext, - createQuotaRuntimeRequestContext, - resolveQuotaRuntimeContext, -} from "./quota-runtime-context.js"; -import { buildCompactQuotaStatusLine } from "./tui-compact-format.js"; -import { hasNativeProviderQuotaClient } from "./tui-native-provider-quota.js"; -import type { - CompactStatusState, - HomeBottomState, - PromptBarState, - SidebarPanelState, -} from "./tui-panel-state.js"; -import { pickPromptBarEntry } from "./tui-prompt-bar-format.js"; -import { buildSidebarQuotaPanelLines, TUI_SIDEBAR_MAX_WIDTH } from "./tui-sidebar-format.js"; -import type { OpenCodeGoWindowKey, TuiCommandDisplay } from "./types.js"; - -const COMPACT_UNAVAILABLE_TEXT = "Quota unavailable"; -const tuiQuotaClients = new WeakMap>(); - -export function getTuiRuntimeRootHints(api: TuiPluginApi): RuntimeContextRootHints { - return { - worktreeRoot: api.state.path.worktree, - activeDirectory: api.state.path.directory, - fallbackDirectory: process.cwd(), - }; -} - -export function resolveWorkspaceDir(api: TuiPluginApi): string { - return resolveRuntimeContextRoots(getTuiRuntimeRootHints(api)).workspaceRoot; -} - -function makeTuiQuotaClient(api: TuiPluginApi) { - return { - config: { - providers: async () => { - try { - if (api.client.config?.providers) { - const response = await api.client.config.providers(); - return { - data: { - providers: response.data?.providers ?? [], - }, - }; - } - } catch { - // Fall back to TUI state provider list below. - } - - return { - data: { - providers: api.state.provider.map((provider) => ({ id: provider.id })), - }, - }; - }, - get: async () => { - try { - if (api.client.config?.get) { - const response = await api.client.config.get(); - return { - data: response?.data && typeof response.data === "object" ? response.data : {}, - }; - } - } catch { - // Fall back to empty config below. - } - - return { data: {} }; - }, - }, - }; -} - -export function createTuiQuotaClient(api: TuiPluginApi) { - const existing = tuiQuotaClients.get(api); - if (existing) return existing; - const client = makeTuiQuotaClient(api); - tuiQuotaClients.set(api, client); - return client; -} - -export function normalizeTuiSessionID(sessionID: unknown): string | undefined { - if (typeof sessionID !== "string") return undefined; - - const trimmed = sessionID.trim(); - if (!trimmed) return undefined; - - let decoded = trimmed; - try { - decoded = decodeURIComponent(trimmed).trim(); - } catch { - decoded = trimmed; - } - - if (!decoded || decoded.includes("{") || decoded.includes("}")) return undefined; - if (decoded === "sessionID" || decoded === "session_id" || decoded === "id") return undefined; - - return trimmed; -} - -function extractSessionModelMeta(input: unknown): SessionModelMeta { - if (!input || typeof input !== "object") return {}; - const item = input as { - model?: { providerID?: string; id?: string }; - }; - const providerID = item.model?.providerID; - const modelID = item.model?.id; - return providerID || modelID ? { providerID, modelID } : {}; -} - -function extractMessageModelMeta(input: unknown): SessionModelMeta { - if (!input || typeof input !== "object") return {}; - const item = input as { - role?: string; - providerID?: string; - modelID?: string; - model?: { providerID?: string; modelID?: string }; - }; - if (item.role === "assistant") { - return item.providerID || item.modelID - ? { providerID: item.providerID, modelID: item.modelID } - : {}; - } - if (item.role === "user") { - return item.model?.providerID || item.model?.modelID - ? { providerID: item.model?.providerID, modelID: item.model?.modelID } - : {}; - } - return {}; -} - -function getMessageSessionModelMeta(api: TuiPluginApi, sessionID: string): SessionModelMeta { - const messages = api.state.session.messages(sessionID); - for (let index = messages.length - 1; index >= 0; index--) { - const meta = extractMessageModelMeta(messages[index]); - if (meta.providerID || meta.modelID) return meta; - } - return {}; -} - -export async function getTuiSessionModelMeta( - api: TuiPluginApi, - sessionID: string, -): Promise { - const safeSessionID = normalizeTuiSessionID(sessionID); - if (!safeSessionID) return {}; - - const stateSession = api.state.session as { get?: (sessionID: string) => unknown }; - try { - const stateMeta = extractSessionModelMeta(stateSession.get?.(safeSessionID)); - if (stateMeta.providerID || stateMeta.modelID) return stateMeta; - } catch { - // Fall back to the client lookup below. - } - - try { - const sessionGet = (api.client.session as any)?.get; - const response = await sessionGet?.({ sessionID: safeSessionID }); - const meta = extractSessionModelMeta(response?.data); - if (meta.providerID || meta.modelID) return meta; - } catch { - // Fall back to session message state below. - } - - return getMessageSessionModelMeta(api, safeSessionID); -} - -export type TuiSidebarPanelRegistration = { - enabled: boolean; -}; - -export type TuiCompactStatusRegistration = { - enabled: boolean; - homeBottom: boolean; - sessionPrompt: boolean; - hasNativeProviderQuota: boolean; - suppressedByNativeProviderQuota: boolean; -}; - -export type TuiPromptBarRegistration = { - enabled: boolean; -}; - -export type TuiMaintainerAnnouncementsRegistration = { - homeBottom: boolean; -}; - -export type TuiSurfaceRegistration = { - commandDisplay: TuiCommandDisplay; - sidebar: TuiSidebarPanelRegistration; - compact: TuiCompactStatusRegistration; - promptBar: TuiPromptBarRegistration; - announcements: TuiMaintainerAnnouncementsRegistration; - homeBottom: boolean; -}; - -export type TuiSessionQuotaSurfaces = { - sidebar: SidebarPanelState; - compact: CompactStatusState; - promptBar: PromptBarState; -}; - -export type TuiInitialRuntimeSeed = Readonly< - Pick ->; - -export type TuiSurfaceRegistrationOptions = { - captureInitialRuntime?: (seed: TuiInitialRuntimeSeed) => void; -}; - -function getMatchingInitialRuntimeSeed( - api: TuiPluginApi, - seed: TuiInitialRuntimeSeed | undefined, -): TuiInitialRuntimeSeed | undefined { - if (!seed) return undefined; - - const roots = resolveRuntimeContextRoots(getTuiRuntimeRootHints(api)); - return roots.workspaceRoot === seed.roots.workspaceRoot && - roots.configRoot === seed.roots.configRoot - ? seed - : undefined; -} - -function isSessionSidebarEnabled(runtime: QuotaRuntimeContext): boolean { - return runtime.config.enabled && runtime.config.tuiSidebarPanel.enabled; -} - -function isSessionCompactEnabled(runtime: QuotaRuntimeContext): boolean { - return ( - runtime.config.enabled && - runtime.config.tuiCompactStatus.enabled && - runtime.config.tuiCompactStatus.sessionPrompt - ); -} - -function isSessionPromptBarEnabled(runtime: QuotaRuntimeContext): boolean { - return runtime.config.enabled && runtime.config.tuiPromptBar.enabled; -} - -function buildDisabledSessionQuotaSurfaces(): TuiSessionQuotaSurfaces { - return { - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { status: "disabled" }, - }; -} - -function buildCompactStatusFromData(params: { - runtime: QuotaRuntimeContext; - result: CollectQuotaRenderDataResult; - enabled: boolean; - maxWidth?: number; - formatStyle?: ReturnType; -}): CompactStatusState { - if (!params.enabled) return { status: "disabled" }; - - if (params.result.selection?.waitingForCurrentSelection) { - return { status: "loading" }; - } - - const effectiveFormatStyle = - params.formatStyle ?? resolveQuotaFormatStyle(params.runtime.config.formatStyle); - const data = - effectiveFormatStyle === "allWindows" && params.result.allWindowsData - ? params.result.allWindowsData - : effectiveFormatStyle === "singleWindow" && params.result.singleWindowData !== undefined - ? params.result.singleWindowData - : params.result.data; - - const text = data - ? buildCompactQuotaStatusLine({ - data, - percentDisplayMode: params.runtime.config.percentDisplayMode, - accountingDetail: params.runtime.config.accountingDetail, - ...(params.runtime.config.resetTimeSpaced !== undefined - ? { resetTimeSpaced: params.runtime.config.resetTimeSpaced } - : {}), - maxWidth: params.maxWidth ?? params.runtime.config.tuiCompactStatus.maxWidth, - }) - : ""; - - return { - status: "ready", - text: text.trim() ? text : COMPACT_UNAVAILABLE_TEXT, - }; -} - -const OPENCODE_GO_ACCOUNTING_WINDOWS: Readonly> = { - rolling: "five_hour", - weekly: "week", - monthly: "month", -}; - -function buildSidebarPanelFromData(params: { - runtime: QuotaRuntimeContext; - result: CollectQuotaRenderDataResult; - formatStyle: ReturnType; -}): SidebarPanelState { - if (params.result.selection?.waitingForCurrentSelection) { - return { - status: "loading", - lines: [], - }; - } - - const hasExpandedDetail = - params.formatStyle === "allWindows" && Boolean(params.result.allWindowsData); - const baseCompactData = params.result.singleWindowData ?? params.result.data; - const preferredWindowKey = params.runtime.config.tuiSidebarPanel.opencodeGoPreferredWindow; - const providerResults = params.result.providerResults ?? []; - const openCodeGoResultIndex = providerResults.findIndex( - ({ providerId }) => providerId === "opencode-go", - ); - const compactData = - baseCompactData && preferredWindowKey && openCodeGoResultIndex >= 0 - ? { - ...baseCompactData, - entries: projectQuotaProviderResults( - providerResults.map(({ result }) => result), - "singleWindow", - params.runtime.config.accountingDetail, - { - preferredWindowsByResultIndex: new Map([ - [openCodeGoResultIndex, OPENCODE_GO_ACCOUNTING_WINDOWS[preferredWindowKey]], - ]), - quotaProjection: params.runtime.config.quotaProjection, - }, - ), - } - : baseCompactData; - const primaryData = compactData; - const primaryFormatStyle = - params.formatStyle === "allWindows" && params.result.allWindowsData - ? "singleWindow" - : params.formatStyle; - - const lines = primaryData - ? hasExpandedDetail - ? [ - buildCompactQuotaStatusLine({ - data: primaryData, - percentDisplayMode: params.runtime.config.percentDisplayMode, - accountingDetail: params.runtime.config.accountingDetail, - ...(params.runtime.config.resetTimeSpaced !== undefined - ? { resetTimeSpaced: params.runtime.config.resetTimeSpaced } - : {}), - maxWidth: TUI_SIDEBAR_MAX_WIDTH, - }), - ].filter((line): line is string => Boolean(line)) - : buildSidebarQuotaPanelLines({ - data: primaryData, - config: { ...params.runtime.config, formatStyle: primaryFormatStyle }, - }) - : []; - - const expandedLines = params.result.allWindowsData - ? (buildSidebarQuotaPanelLines({ - data: params.result.allWindowsData, - config: { ...params.runtime.config, formatStyle: "allWindows" }, - }) ?? []) - : []; - const linesExpanded = - expandedLines.length > 0 && expandedLines.join("\n") !== lines.join("\n") - ? expandedLines - : undefined; - - const visibleData = params.result.allWindowsData ?? primaryData; - const providerCount = visibleData ? groupQuotaEntries(visibleData.entries, "toast").length : 0; - - return { - status: "ready", - lines, - ...(providerCount > 0 ? { providerCount } : {}), - ...(linesExpanded ? { linesExpanded } : {}), - ...(params.runtime.config.percentLabelStyle === "bare" - ? { headerPercentMode: params.runtime.config.percentDisplayMode } - : {}), - }; -} - -function buildPromptBarFromData(params: { - runtime: QuotaRuntimeContext; - result: CollectQuotaRenderDataResult; - enabled: boolean; -}): PromptBarState { - if (!params.enabled) { - return { status: "disabled" }; - } - - if (params.result.selection?.waitingForCurrentSelection) { - return { status: "loading" }; - } - - const entry = pickPromptBarEntry(params.result.allWindowsData ?? params.result.data); - return { - status: "ready", - ...(entry ? { entry } : {}), - percentDisplayMode: params.runtime.config.percentDisplayMode, - resetTimeDecimals: params.runtime.config.resetTimeDecimals, - ...(params.runtime.config.resetTimeSpaced !== undefined - ? { resetTimeSpaced: params.runtime.config.resetTimeSpaced } - : {}), - }; -} - -async function collectTuiQuotaRenderData(params: { - runtime: QuotaRuntimeContext; - request: ReturnType; -}): Promise<{ - result: CollectQuotaRenderDataResult; - formatStyle: ReturnType; - sidebarFormatStyle: ReturnType; - compactFormatStyle: ReturnType; -}> { - const formatStyle = resolveQuotaFormatStyle(params.runtime.config.formatStyle); - const sidebarFormatStyle = params.runtime.config.tuiSidebarPanel.formatStyle - ? resolveQuotaFormatStyle(params.runtime.config.tuiSidebarPanel.formatStyle) - : formatStyle; - const compactFormatStyle = params.runtime.config.tuiCompactStatus.formatStyle - ? resolveQuotaFormatStyle(params.runtime.config.tuiCompactStatus.formatStyle) - : formatStyle; - const result = await collectQuotaRenderData({ - client: params.runtime.client, - resolveRuntimeProviderIds: params.runtime.resolveRuntimeProviderIds, - config: params.runtime.config, - configMeta: params.runtime.configMeta, - request: params.request, - surfaceExplicitProviderIssues: true, - formatStyle, - providers: params.runtime.providers, - includeAllWindowsData: true, - }); - - return { result, formatStyle, sidebarFormatStyle, compactFormatStyle }; -} - -export async function resolveTuiSurfaceRegistration( - api: TuiPluginApi, - options?: TuiSurfaceRegistrationOptions, -): Promise { - const quotaClient = createTuiQuotaClient(api); - const runtime = await resolveQuotaRuntimeContext({ - client: quotaClient, - roots: getTuiRuntimeRootHints(api), - }); - const compact = runtime.config.tuiCompactStatus; - const hasNativeProviderQuota = hasNativeProviderQuotaClient(api.client); - const suppressedByNativeProviderQuota = - compact.suppressWhenNativeProviderQuota && hasNativeProviderQuota; - const compactEnabled = - runtime.config.enabled && compact.enabled && !suppressedByNativeProviderQuota; - const announcementHomeBottom = - runtime.config.enabled && - runtime.config.maintainerAnnouncements.enabled && - runtime.config.maintainerAnnouncements.home; - const exportHomeBottom = runtime.config.enabled && runtime.config.export.enabled; - const compactHomeBottom = compactEnabled && compact.homeBottom; - - const registration = { - commandDisplay: runtime.config.tuiCommandDisplay, - sidebar: { - enabled: runtime.config.enabled && runtime.config.tuiSidebarPanel.enabled, - }, - compact: { - enabled: compactEnabled, - homeBottom: compactHomeBottom, - sessionPrompt: compactEnabled && compact.sessionPrompt, - hasNativeProviderQuota, - suppressedByNativeProviderQuota, - }, - promptBar: { - enabled: runtime.config.enabled && runtime.config.tuiPromptBar.enabled, - }, - announcements: { - homeBottom: announcementHomeBottom, - }, - homeBottom: compactHomeBottom || announcementHomeBottom || exportHomeBottom, - }; - - options?.captureInitialRuntime?.({ - roots: runtime.roots, - config: runtime.config, - configMeta: runtime.configMeta, - providers: runtime.providers, - }); - return registration; -} - -export async function loadTuiSessionQuotaSurfaces(params: { - api: TuiPluginApi; - sessionID: string; - initialRuntimeSeed?: TuiInitialRuntimeSeed; -}): Promise { - const quotaClient = createTuiQuotaClient(params.api); - const initialRuntimeSeed = getMatchingInitialRuntimeSeed(params.api, params.initialRuntimeSeed); - const runtime = await resolveQuotaRuntimeContext({ - client: quotaClient, - roots: getTuiRuntimeRootHints(params.api), - sessionID: params.sessionID, - resolveSessionMeta: (sessionID) => getTuiSessionModelMeta(params.api, sessionID), - includeSessionMeta: (config) => config.onlyCurrentModel, - config: initialRuntimeSeed?.config, - configMeta: initialRuntimeSeed?.configMeta, - providers: initialRuntimeSeed?.providers, - }); - - const sidebarEnabled = isSessionSidebarEnabled(runtime); - const compactEnabled = isSessionCompactEnabled(runtime); - const promptBarEnabled = isSessionPromptBarEnabled(runtime); - - if (!sidebarEnabled && !compactEnabled && !promptBarEnabled) { - return buildDisabledSessionQuotaSurfaces(); - } - - const { result, sidebarFormatStyle, compactFormatStyle } = await collectTuiQuotaRenderData({ - runtime, - request: createQuotaRuntimeRequestContext(runtime), - }); - - return { - sidebar: sidebarEnabled - ? buildSidebarPanelFromData({ runtime, result, formatStyle: sidebarFormatStyle }) - : { status: "disabled", lines: [] }, - compact: buildCompactStatusFromData({ - runtime, - result, - enabled: compactEnabled, - formatStyle: compactFormatStyle, - }), - promptBar: buildPromptBarFromData({ - runtime, - result, - enabled: promptBarEnabled, - }), - }; -} - -export async function loadTuiHomeBottomStatus(params: { - api: TuiPluginApi; - nowMs?: number; - announcements?: readonly MaintainerAnnouncement[]; - initialRuntimeSeed?: TuiInitialRuntimeSeed; -}): Promise { - const quotaClient = createTuiQuotaClient(params.api); - const initialRuntimeSeed = getMatchingInitialRuntimeSeed(params.api, params.initialRuntimeSeed); - const runtime = await resolveQuotaRuntimeContext({ - client: quotaClient, - roots: getTuiRuntimeRootHints(params.api), - config: initialRuntimeSeed?.config, - configMeta: initialRuntimeSeed?.configMeta, - providers: initialRuntimeSeed?.providers, - }); - - const announcementEnabled = - runtime.config.enabled && - runtime.config.maintainerAnnouncements.enabled && - runtime.config.maintainerAnnouncements.home; - const compactSuppressedByNativeProviderQuota = - runtime.config.tuiCompactStatus.suppressWhenNativeProviderQuota && - hasNativeProviderQuotaClient(params.api.client); - const compactEnabled = - runtime.config.enabled && - runtime.config.tuiCompactStatus.enabled && - runtime.config.tuiCompactStatus.homeBottom && - !compactSuppressedByNativeProviderQuota; - - if (!announcementEnabled && !compactEnabled) { - return { status: "disabled", compact: { status: "disabled" } }; - } - - let announcementText: string | undefined; - if (announcementEnabled) { - const announcements = params.announcements ?? BUNDLED_MAINTAINER_ANNOUNCEMENTS; - const targetProviderIds = new Set( - getMaintainerAnnouncementTargetProviderIds({ announcements }), - ); - const announcementProviders = runtime.providers.filter((provider) => { - const shape = getQuotaProviderShape(normalizeQuotaProviderId(provider.id)); - return shape ? targetProviderIds.has(shape.id) : false; - }); - const providerIds = await collectConcreteEnabledProviderIds({ - providers: announcementProviders, - ctx: createQuotaProviderRuntimeContext(runtime), - enabledProviders: runtime.config.enabledProviders, - }); - const summary = getMaintainerAnnouncementsSummary({ - nowMs: params.nowMs, - enabledProviders: providerIds, - announcements, - }); - announcementText = formatMaintainerAnnouncementHomeCountLine(summary.activeCount) || undefined; - } - - if (!compactEnabled) { - return announcementText - ? { status: "ready", announcementText, compact: { status: "disabled" } } - : { status: "disabled", compact: { status: "disabled" } }; - } - - const homeRuntime: QuotaRuntimeContext = { - ...runtime, - config: { - ...runtime.config, - onlyCurrentModel: false, - showSessionTokens: false, - }, - session: {}, - }; - - const { result, compactFormatStyle } = await collectTuiQuotaRenderData({ - runtime: homeRuntime, - request: createQuotaRuntimeRequestContext(homeRuntime), - }); - - const compact = buildCompactStatusFromData({ - runtime: homeRuntime, - result, - enabled: true, - formatStyle: compactFormatStyle, - }); - - return { status: "ready", announcementText, compact }; -} - -export async function loadTuiHomeCompactStatus(params: { - api: TuiPluginApi; -}): Promise { - const quotaClient = createTuiQuotaClient(params.api); - const runtime = await resolveQuotaRuntimeContext({ - client: quotaClient, - roots: getTuiRuntimeRootHints(params.api), - }); - const compactSuppressedByNativeProviderQuota = - runtime.config.tuiCompactStatus.suppressWhenNativeProviderQuota && - hasNativeProviderQuotaClient(params.api.client); - - if ( - !runtime.config.enabled || - !runtime.config.tuiCompactStatus.enabled || - !runtime.config.tuiCompactStatus.homeBottom || - compactSuppressedByNativeProviderQuota - ) { - return { status: "disabled" }; - } - - const homeRuntime: QuotaRuntimeContext = { - ...runtime, - config: { - ...runtime.config, - onlyCurrentModel: false, - showSessionTokens: false, - }, - session: {}, - }; - - const { result, compactFormatStyle } = await collectTuiQuotaRenderData({ - runtime: homeRuntime, - request: createQuotaRuntimeRequestContext(homeRuntime), - }); - - return buildCompactStatusFromData({ - runtime: homeRuntime, - result, - enabled: true, - formatStyle: compactFormatStyle, - }); -} - -/** - * Writes the quota export file if `config.export.enabled` is true. - * - * Called from the TUI home bottom status refresh loop. Errors propagate to - * the caller; the call-site in `tui.tsx` is responsible for catching and - * logging them so a failed write never affects rendering. - */ -export async function writeTuiQuotaExportIfEnabled(params: { api: TuiPluginApi }): Promise { - const quotaClient = createTuiQuotaClient(params.api); - const runtime = await resolveQuotaRuntimeContext({ - client: quotaClient, - roots: getTuiRuntimeRootHints(params.api), - }); - - if (!runtime.config.enabled || !runtime.config.export.enabled) { - return; - } - - const resolvedPath = resolveExportPath(runtime.config.export.path); - const ctx = createExportProviderContext(runtime); - - const exportData = await buildQuotaExport({ - providers: runtime.providers, - ctx, - ttlMs: runtime.config.minIntervalMs, - fromCache: true, - }); - - await writeQuotaExport(exportData, resolvedPath); -} diff --git a/src/lib/tui-sidebar-content.ts b/src/lib/tui-sidebar-content.ts deleted file mode 100644 index cfefc74d..00000000 --- a/src/lib/tui-sidebar-content.ts +++ /dev/null @@ -1,95 +0,0 @@ -export type SidebarTextStyle = "title" | "muted" | "body"; - -export type SidebarContentSegment = { - text: string; - style: SidebarTextStyle; -}; - -export type SidebarContentRow = { - kind: "header" | "body"; - text: string; - segments: SidebarContentSegment[]; -}; - -function sidebarHeadingTitle(params: { - collapsed: boolean; - heading: string; - hasDetailLines: boolean; -}): string { - return params.hasDetailLines - ? `${params.collapsed ? "▶" : "▼"} ${params.heading}` - : params.heading; -} - -function sidebarHeadingSuffix(params: { - collapsed: boolean; - hasDetailLines: boolean; - providerCount: number; -}): string | undefined { - if (params.hasDetailLines && params.collapsed && params.providerCount > 0) { - return ` (${params.providerCount} providers)`; - } - return undefined; -} - -export function formatSidebarHeadingSegments(params: { - collapsed: boolean; - heading: string; - hasDetailLines: boolean; - providerCount: number; -}): SidebarContentSegment[] { - const title = sidebarHeadingTitle(params); - const suffix = sidebarHeadingSuffix(params); - return suffix - ? [ - { text: title, style: "title" }, - { text: suffix, style: "muted" }, - ] - : [{ text: title, style: "title" }]; -} - -export function formatSidebarHeading(params: { - collapsed: boolean; - heading: string; - hasDetailLines: boolean; - providerCount: number; -}): string { - return formatSidebarHeadingSegments(params) - .map((segment) => segment.text) - .join(""); -} - -export function buildSidebarContentRows(params: { - collapsed: boolean; - heading: string; - hasDetailLines: boolean; - providerCount: number; - lines: string[]; -}): SidebarContentRow[] { - const segments = formatSidebarHeadingSegments(params); - return [ - { - kind: "header", - text: formatSidebarHeading(params), - segments, - }, - ...params.lines.map((line) => ({ - kind: "body" as const, - text: line, - segments: [{ text: line, style: "body" as const }], - })), - ]; -} - -export function renderSidebarContentFrame(rows: SidebarContentRow[]): string { - return rows.map((row) => row.text).join("\n"); -} - -const SIDEBAR_HEADING_RE = /^(?:[▶▼] )?Quota(?: \[(?:Used|Remaining)\])?(?: \(\d+ providers\))?$/; - -export function collectSidebarHeadingLines(frame: string): string[] { - return frame - .split("\n") - .map((line) => line.trimEnd()) - .filter((line) => SIDEBAR_HEADING_RE.test(line)); -} diff --git a/src/tui.tsx b/src/tui.tsx deleted file mode 100644 index b3c7c785..00000000 --- a/src/tui.tsx +++ /dev/null @@ -1,1030 +0,0 @@ -/** @jsxImportSource @opentui/solid */ - -import type { - TuiPlugin, - TuiPluginApi, - TuiPluginModule, - TuiPromptRef, -} from "@opencode-ai/plugin/tui"; -import type { JSX } from "@opentui/solid"; -import { createEffect, createSignal, Index, onCleanup, Show } from "solid-js"; -import { - formatQuotaModeHeading, - formatResetCountdown, - isResetTimeDecimals, - resolveDisplayedPercent, -} from "./lib/format-utils.js"; -import { - buildQuotaDialogCommandOutput, - QUOTA_DIALOG_COMMANDS, - type QuotaDialogCommandId, - type QuotaDialogCommandSpec, -} from "./lib/quota-dialog-commands.js"; -import type { SessionTokenError } from "./lib/quota-status.js"; -import { disposeQuotaTelemetryOwner } from "./lib/quota-telemetry.js"; -import { getSidebarBodyLineColor } from "./lib/tui-line-style.js"; -import type { - CompactStatusState, - HomeBottomState, - PromptBarState, - SidebarPanelState, -} from "./lib/tui-panel-state.js"; -import { - getCompactStatusText, - getHomeBottomAnnouncementText, - getSidebarPanelLines, - getSidebarPanelLinesExpanded, - shouldRenderCompactStatus, - shouldRenderHomeBottom, - shouldRenderSidebarPanel, -} from "./lib/tui-panel-state.js"; -import { - formatPromptBarPercentMeta, - PROMPT_BAR_WIDTH, - resolvePromptBarLabel, -} from "./lib/tui-prompt-bar-format.js"; -import { createTuiRefreshLifecycle } from "./lib/tui-refresh-lifecycle.js"; -import { - createTuiQuotaClient, - getTuiRuntimeRootHints, - getTuiSessionModelMeta, - loadTuiHomeBottomStatus, - loadTuiSessionQuotaSurfaces, - normalizeTuiSessionID, - resolveTuiSurfaceRegistration, - type TuiInitialRuntimeSeed, - type TuiSurfaceRegistration, - writeTuiQuotaExportIfEnabled, -} from "./lib/tui-runtime.js"; -import { buildSidebarContentRows, type SidebarContentRow } from "./lib/tui-sidebar-content.js"; -import type { TuiCommandDisplay } from "./lib/types.js"; - -const id = "@slkiser/opencode-quota"; -// Place Quota near the top so variable-height built-in sections -// (MCP/LSP/Todo/Files) do not push it below the visible fold. -const SIDEBAR_ORDER = 150; -const COMPACT_ORDER = 90; -const REFRESH_INTERVAL_MS = 60_000; -const EVENT_REFRESH_DELAYS_MS = [150, 600] as const; -const MOUNT_RECOVERY_DELAYS_MS = [500, 1_500, 4_000] as const; - -type TuiPromptRefCallback = (ref: TuiPromptRef | undefined) => void; -type DialogSize = "medium" | "large" | "xlarge"; - -type QuotaDialogCommandState = { - lastSessionTokenError?: SessionTokenError; -}; -type SessionQuotaResource = { - sessionID: string; - sidebar: () => SidebarPanelState; - compact: () => CompactStatusState; - promptBar: () => PromptBarState; - retain: () => SessionQuotaResource; - release: () => void; -}; - -type HomeBottomResource = { - bottom: () => HomeBottomState; - retain: () => HomeBottomResource; - release: () => void; -}; - -type TuiInitialLoadCoordinator = { - takeInitialSession: () => TuiInitialRuntimeSeed | undefined; - takeInitialHome: () => TuiInitialRuntimeSeed | undefined; -}; - -type TuiRegistrationState = - | { status: "pending" } - | { - status: "active"; - registration: TuiSurfaceRegistration; - initialLoads?: TuiInitialLoadCoordinator; - } - | { status: "disposed" }; - -type TuiRegistrationGate = { - current: () => TuiRegistrationState; - activate: ( - registration: TuiSurfaceRegistration, - initialLoads?: TuiInitialLoadCoordinator, - ) => void; - dispose: () => void; -}; - -const FALLBACK_SURFACE_REGISTRATION: TuiSurfaceRegistration = { - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: false }, - announcements: { homeBottom: false }, - homeBottom: false, -}; - -function createTuiInitialLoadCoordinator(seed: TuiInitialRuntimeSeed): TuiInitialLoadCoordinator { - let sessionAvailable = true; - let homeAvailable = true; - - return { - takeInitialSession() { - if (!sessionAvailable) return undefined; - sessionAvailable = false; - return seed; - }, - takeInitialHome() { - if (!homeAvailable) return undefined; - homeAvailable = false; - return seed; - }, - }; -} - -function createTuiRegistrationGate(): TuiRegistrationGate { - const [current, setCurrent] = createSignal({ status: "pending" }); - - return { - current, - activate(registration, initialLoads) { - if (current().status !== "pending") return; - setCurrent({ status: "active", registration, initialLoads }); - }, - dispose() { - if (current().status === "disposed") return; - setCurrent({ status: "disposed" }); - }, - }; -} - -const sessionResources = new WeakMap>(); -const homeResources = new WeakMap(); - -function getSessionResourceMap(api: TuiPluginApi): Map { - const existing = sessionResources.get(api); - if (existing) return existing; - - const next = new Map(); - sessionResources.set(api, next); - return next; -} - -function createSessionQuotaResource( - api: TuiPluginApi, - sessionID: string, - initialLoads?: TuiInitialLoadCoordinator, -): SessionQuotaResource { - const [sidebar, setSidebar] = createSignal({ - status: "loading", - lines: [], - }); - const [compact, setCompact] = createSignal({ status: "loading" }); - const [promptBar, setPromptBar] = createSignal({ status: "loading" }); - - let loadOrdinal = 0; - const lifecycle = createTuiRefreshLifecycle({ - load: () => { - const initialRuntimeSeed = loadOrdinal === 0 ? initialLoads?.takeInitialSession() : undefined; - loadOrdinal += 1; - return loadTuiSessionQuotaSurfaces({ - api, - sessionID, - ...(initialRuntimeSeed ? { initialRuntimeSeed } : {}), - }); - }, - apply: (next) => { - setSidebar(next.sidebar); - setCompact(next.compact); - setPromptBar(next.promptBar ?? { status: "loading" }); - }, - intervalMs: REFRESH_INTERVAL_MS, - eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS, - // TUI/session state can hydrate asynchronously after mount or session switch, - // so retry a few times to recover from empty first-load reads. - recoveryDelaysMs: MOUNT_RECOVERY_DELAYS_MS, - subscribe: (scheduleRefresh) => [ - api.event.on("session.updated", (event) => { - if (event.properties?.info?.id === sessionID) { - scheduleRefresh(); - } - }), - api.event.on("message.updated", (event) => { - if (event.properties?.info?.sessionID === sessionID) { - scheduleRefresh(); - } - }), - api.event.on("message.removed", (event) => { - if (event.properties?.sessionID === sessionID) { - scheduleRefresh(); - } - }), - api.event.on("tui.session.select", (event) => { - if (event.properties?.sessionID === sessionID) { - scheduleRefresh(); - } - }), - ], - onDispose: () => { - getSessionResourceMap(api).delete(sessionID); - }, - }); - - const resource: SessionQuotaResource = { - sessionID, - sidebar, - compact, - promptBar, - retain: () => { - lifecycle.retain(); - return resource; - }, - release: lifecycle.release, - }; - - return resource; -} - -function acquireSessionQuotaResource( - api: TuiPluginApi, - sessionID: string, - initialLoads?: TuiInitialLoadCoordinator, -): SessionQuotaResource { - const resources = getSessionResourceMap(api); - const existing = resources.get(sessionID); - if (existing) return existing.retain(); - - const next = createSessionQuotaResource(api, sessionID, initialLoads).retain(); - resources.set(sessionID, next); - return next; -} - -function createHomeBottomResource( - api: TuiPluginApi, - compactHomeBottomEnabled: boolean, - initialLoads?: TuiInitialLoadCoordinator, -): HomeBottomResource { - const [bottom, setBottom] = createSignal({ - status: "loading", - compact: compactHomeBottomEnabled ? { status: "loading" } : { status: "disabled" }, - }); - - let loadOrdinal = 0; - const lifecycle = createTuiRefreshLifecycle({ - load: () => { - const initialRuntimeSeed = loadOrdinal === 0 ? initialLoads?.takeInitialHome() : undefined; - loadOrdinal += 1; - return loadTuiHomeBottomStatus({ - api, - ...(initialRuntimeSeed ? { initialRuntimeSeed } : {}), - }); - }, - apply: setBottom, - afterApply: () => { - // Fire-and-forget: write export file if enabled. A failed write must - // never affect TUI rendering, so log a warning and continue. - void writeTuiQuotaExportIfEnabled({ api }).catch((err) => { - console.warn(`[opencode-quota] quota export write failed: ${String(err)}`); - }); - }, - intervalMs: REFRESH_INTERVAL_MS, - eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS, - subscribe: (scheduleRefresh) => [ - api.event.on("session.updated", scheduleRefresh), - api.event.on("message.updated", scheduleRefresh), - api.event.on("message.removed", scheduleRefresh), - api.event.on("tui.session.select", scheduleRefresh), - ], - onDispose: () => { - homeResources.delete(api); - }, - }); - - const resource: HomeBottomResource = { - bottom, - retain: () => { - lifecycle.retain(); - return resource; - }, - release: lifecycle.release, - }; - - return resource; -} - -function acquireHomeBottomResource( - api: TuiPluginApi, - compactHomeBottomEnabled: boolean, - initialLoads?: TuiInitialLoadCoordinator, -): HomeBottomResource { - const existing = homeResources.get(api); - if (existing) return existing.retain(); - - const next = createHomeBottomResource(api, compactHomeBottomEnabled, initialLoads).retain(); - homeResources.set(api, next); - return next; -} - -function useSessionQuotaResource( - api: TuiPluginApi, - sessionID: () => string, - initialLoads?: TuiInitialLoadCoordinator, -): () => SessionQuotaResource { - let current = acquireSessionQuotaResource(api, sessionID(), initialLoads); - const [resource, setResource] = createSignal(current); - - createEffect(() => { - const nextSessionID = sessionID(); - if (current.sessionID === nextSessionID) return; - - const previous = current; - current = acquireSessionQuotaResource(api, nextSessionID, initialLoads); - setResource(current); - previous.release(); - }); - - onCleanup(() => { - current.release(); - }); - - return resource; -} - -function SidebarContentView(props: { - api: TuiPluginApi; - sessionID: string; - initialLoads?: TuiInitialLoadCoordinator; -}) { - const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads); - const panel = () => resource().sidebar(); - - const lines = () => getSidebarPanelLines(panel()); - const hasDetailLines = () => Boolean(panel().linesExpanded?.length); - - const [collapsed, setCollapsed] = createSignal( - props.api.kv?.get("quota-sidebar-collapsed", true) ?? true, - ); - - const toggleCollapsed = () => { - if (!hasDetailLines()) return; - - const next = !collapsed(); - setCollapsed(next); - props.api.kv?.set("quota-sidebar-collapsed", next); - }; - - const displayLines = () => { - if (!hasDetailLines()) return lines(); - return collapsed() ? lines() : getSidebarPanelLinesExpanded(panel()); - }; - - const contentRows = (): SidebarContentRow[] => { - const heading = panel().headerPercentMode - ? formatQuotaModeHeading(panel().headerPercentMode) - : "Quota"; - return buildSidebarContentRows({ - collapsed: collapsed(), - heading, - hasDetailLines: hasDetailLines(), - providerCount: panel().providerCount ?? 0, - lines: displayLines(), - }); - }; - - return ( - - - - {(row: () => SidebarContentRow) => ( - - {row().kind === "header" - ? row().segments.map((segment) => - segment.style === "muted" ? ( - {segment.text} - ) : ( - {segment.text} - ), - ) - : row().text || " "} - - )} - - - - ); -} - -function CompactStatusLine(props: { - api: TuiPluginApi; - panel: () => CompactStatusState; - justifyContent: "flex-start" | "center" | "flex-end"; - blankLineBefore?: boolean; -}) { - const text = () => { - const panel = props.panel(); - if (!shouldRenderCompactStatus(panel)) return ""; - return getCompactStatusText(panel); - }; - - const line = () => ( - - - {text()} - - - ); - - return ( - - - - - {line()} - - - - ); -} - -function SessionPromptWithCompactStatus(props: { - api: TuiPluginApi; - sessionID: string; - initialLoads?: TuiInitialLoadCoordinator; - visible?: boolean; - disabled?: boolean; - onSubmit?: () => void; - promptRef?: TuiPromptRefCallback; -}) { - const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads); - const panel = () => resource().compact(); - - return ( - - - - - ); -} - -function shouldRenderPromptBar( - bar: PromptBarState, -): bar is Extract { - return bar.status === "ready" && Boolean(bar.entry); -} - -function useSessionRunning(api: TuiPluginApi, sessionID: () => string): () => boolean { - const [running, setRunning] = createSignal(false); - createEffect(() => { - const id = sessionID(); - if (!id) { - setRunning(false); - return; - } - const update = () => { - try { - const sessionState = api.state.session as { - status?: (sessionID: string) => { type?: string } | undefined; - }; - const status = sessionState.status?.(id); - setRunning(status?.type === "busy" || status?.type === "retry"); - } catch { - setRunning(false); - } - }; - update(); - const disposers = [ - api.event.on("session.status", (event) => { - if (event.properties?.sessionID === id) { - update(); - } - }), - api.event.on("session.updated", (event) => { - if (event.properties?.info?.id === id) { - update(); - } - }), - ]; - onCleanup(() => { - for (const dispose of disposers) { - if (typeof dispose === "function") { - dispose(); - } - } - }); - }); - return running; -} - -function buildPromptBarParts(params: { - bar: () => PromptBarState; - running: () => boolean; - phase: () => number; -}): { label: string; barText: string; meta: string } | undefined { - const bar = params.bar(); - if (!shouldRenderPromptBar(bar)) return undefined; - const entry = bar.entry; - if (!entry) return undefined; - - const hasPercent = Number.isFinite(entry.percentRemaining); - if (entry.semanticSegment && !hasPercent) { - const reset = entry.resetTimeIso - ? formatResetCountdown( - entry.resetTimeIso, - isResetTimeDecimals(bar.resetTimeDecimals) - ? { compactRounded: true, decimals: bar.resetTimeDecimals } - : { spaced: bar.resetTimeSpaced }, - ) - : ""; - return { label: resolvePromptBarLabel(entry), barText: "", meta: reset }; - } - - const p = Math.min( - 100, - resolveDisplayedPercent(entry.percentRemaining ?? 0, bar.percentDisplayMode ?? "remaining"), - ); - const filled = Math.round((p / 100) * PROMPT_BAR_WIDTH); - const empty = PROMPT_BAR_WIDTH - filled; - let barText = "█".repeat(filled) + "░".repeat(empty); - if (params.running() && filled > 0) { - const cells = Array(filled).fill("▓"); - const center = params.phase() % filled; - const gradient = ["▒", "▓", "█", "▓", "▒"]; - for (let offset = -2; offset <= 2; offset++) { - const position = (center + offset + filled) % filled; - cells[position] = gradient[offset + 2]; - } - barText = cells.join("") + "░".repeat(empty); - } - return { - label: resolvePromptBarLabel(entry), - barText, - meta: formatPromptBarPercentMeta({ - percentRemaining: entry.percentRemaining ?? 0, - percentDisplayMode: bar.percentDisplayMode, - resetTimeIso: entry.resetTimeIso, - resetTimeDecimals: bar.resetTimeDecimals, - resetTimeSpaced: bar.resetTimeSpaced, - runway: entry.runway, - }), - }; -} - -function PromptQuotaHint(props: { - api: TuiPluginApi; - bar: () => PromptBarState; - running: () => boolean; - phase: () => number; -}) { - const parts = () => buildPromptBarParts(props); - const barColor = () => props.api.theme.current.textMuted; - const label = () => parts()?.label ?? ""; - const bar = () => parts()?.barText ?? ""; - const meta = () => parts()?.meta ?? ""; - - return ( - - - - {label()} - - - {bar()} - - - {meta()} - - - - ); -} - -function SessionQuotaPromptBar(props: { - api: TuiPluginApi; - sessionID: string; - initialLoads?: TuiInitialLoadCoordinator; - visible?: boolean; - disabled?: boolean; - onSubmit?: () => void; - promptRef?: TuiPromptRefCallback; -}) { - const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads); - const promptBar = () => resource().promptBar(); - const running = useSessionRunning(props.api, () => props.sessionID); - const [phase, setPhase] = createSignal(0); - createEffect(() => { - if (!running() || !shouldRenderPromptBar(promptBar())) { - setPhase(0); - return; - } - const interval = setInterval(() => setPhase((p) => p + 1), 160); - onCleanup(() => clearInterval(interval)); - }); - - return ( - - - - - ); -} - -function HomeBottomView(props: { - api: TuiPluginApi; - compactHomeBottomEnabled: boolean; - initialLoads?: TuiInitialLoadCoordinator; -}) { - const resource = acquireHomeBottomResource( - props.api, - props.compactHomeBottomEnabled, - props.initialLoads, - ); - onCleanup(() => resource.release()); - - const announcement = () => getHomeBottomAnnouncementText(resource.bottom()); - const compact = () => resource.bottom().compact; - const visible = () => shouldRenderHomeBottom(resource.bottom()); - - return ( - - - - - - - - {announcement()} - - - - - - - - ); -} - -function getActiveTuiSessionID(api: TuiPluginApi): string | undefined { - if (api.route.current.name !== "session") return undefined; - return normalizeTuiSessionID(api.route.current.params?.sessionID); -} - -function getTuiCommandArguments(input: unknown): string | undefined { - if (!input || typeof input !== "object") return undefined; - const record = input as Record; - for (const key of ["arguments", "args", "query"] as const) { - const value = record[key]; - if (typeof value === "string" && value.trim()) return value.trim(); - } - return undefined; -} - -function CommandLoadingDialog(props: { api: TuiPluginApi; title: string }) { - return ( - - - {props.title} - - Loading deterministic local output… - - ); -} - -function CommandOutputDialog(props: { api: TuiPluginApi; title: string; output: string }) { - const lines = () => props.output.split("\n"); - const bodyHeight = () => Math.min(28, Math.max(6, lines().length)); - return ( - - - {props.title} - - - - {lines().map((line) => ( - - {line || " "} - - ))} - - - esc closes - - ); -} - -function CommandErrorDialog(props: { api: TuiPluginApi; title: string; error: unknown }) { - const message = props.error instanceof Error ? props.error.message : String(props.error); - return ( - - - {props.title} - - OpenCode Quota command failed. - - {message || "Unknown error"} - - esc closes - - ); -} - -function getCommandPromptCopy(spec: QuotaDialogCommandSpec): { - title: string; - placeholder: string; - description: string; -} { - switch (spec.id) { - case "tokens_between": - return { - title: "OpenCode Quota Token Range", - placeholder: "YYYY-MM-DD YYYY-MM-DD", - description: "Enter start and end dates, for example: 2026-01-01 2026-01-15", - }; - default: - return { - title: spec.title, - placeholder: "Optional arguments", - description: "Leave blank to run with no arguments.", - }; - } -} - -function replaceDialog(api: TuiPluginApi, size: DialogSize, render: () => JSX.Element): void { - api.ui.dialog.replace(render); - // OpenCode dialog.replace() resets size to medium. - api.ui.dialog.setSize(size); -} - -async function runQuotaDialogCommandAsync( - api: TuiPluginApi, - command: QuotaDialogCommandId, - commandDisplay: TuiCommandDisplay, - rawInput?: unknown, - state?: QuotaDialogCommandState, -): Promise { - const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.id === command)!; - const argumentsText = getTuiCommandArguments(rawInput); - const sessionID = getActiveTuiSessionID(api); - - if (spec.acceptsArguments && rawInput === undefined) { - const prompt = getCommandPromptCopy(spec); - replaceDialog(api, "medium", () => ( - ( - - {prompt.description} - - )} - onCancel={() => api.ui.dialog.clear()} - onConfirm={(value) => { - void runQuotaDialogCommandAsync( - api, - command, - commandDisplay, - { arguments: value.trim() }, - state, - ); - }} - /> - )); - return; - } - - const destination = - commandDisplay === "inline" && sessionID - ? { type: "inline" as const, sessionID } - : { type: "dialog" as const }; - if (destination.type === "dialog") { - replaceDialog(api, spec.dialogSize, () => ( - - )); - } - - try { - const result = await buildQuotaDialogCommandOutput({ - command, - arguments: argumentsText, - client: createTuiQuotaClient(api), - roots: getTuiRuntimeRootHints(api), - sessionID, - resolveSessionMeta: (id) => getTuiSessionModelMeta(api, id), - lastSessionTokenError: state?.lastSessionTokenError, - setLastSessionTokenError: state - ? (error) => { - state.lastSessionTokenError = error; - } - : undefined, - log: async (message, extra) => { - await api.client.app.log({ - body: { - service: "quota-toast", - level: "debug", - message, - extra, - }, - }); - }, - }); - - if (result.state === "noop") { - if (destination.type === "dialog") api.ui.dialog.clear(); - return; - } - - if (destination.type === "inline") { - await api.client.session.prompt({ - sessionID: destination.sessionID, - noReply: true, - parts: [{ type: "text", text: result.output, ignored: true }], - }); - return; - } - - replaceDialog(api, result.dialogSize, () => ( - - )); - } catch (error) { - replaceDialog(api, "large", () => ( - - )); - api.ui.toast({ - variant: "error", - message: "OpenCode Quota command failed", - }); - } -} - -function registerQuotaDialogCommands(api: TuiPluginApi, gate: TuiRegistrationGate): void { - const commandState: QuotaDialogCommandState = {}; - const dispose = api.keymap.registerLayer({ - commands: QUOTA_DIALOG_COMMANDS.map((spec) => ({ - namespace: "palette", - name: `opencode-quota.${spec.id}`, - title: spec.title, - desc: spec.description, - category: "OpenCode Quota", - slashName: spec.slashName, - run(input?: unknown) { - const state = gate.current(); - if (state.status !== "active") return; - void runQuotaDialogCommandAsync( - api, - spec.id, - state.registration.commandDisplay, - input, - commandState, - ); - }, - })), - bindings: [], - }); - - api.lifecycle.onDispose(dispose); -} - -function registerStableTuiSlots(api: TuiPluginApi, current: () => TuiRegistrationState): void { - api.slots.register({ - order: SIDEBAR_ORDER, - slots: { - sidebar_content(_ctx, props: { session_id: string }) { - const state = current(); - if (state.status !== "active" || !state.registration.sidebar.enabled) return null; - return ( - - ); - }, - }, - }); - - api.slots.register({ - order: COMPACT_ORDER, - slots: { - session_prompt( - _ctx, - props: { - session_id: string; - visible?: boolean; - disabled?: boolean; - on_submit?: () => void; - ref?: TuiPromptRefCallback; - }, - ) { - const state = current(); - if (state.status !== "active") return null; - if (state.registration.promptBar.enabled) { - return ( - - ); - } - if (!state.registration.compact.sessionPrompt) return null; - return ( - - ); - }, - home_bottom() { - const state = current(); - if (state.status !== "active" || !state.registration.homeBottom) return null; - return ( - - ); - }, - }, - }); -} - -async function initializeTuiRegistration( - api: TuiPluginApi, - gate: TuiRegistrationGate, -): Promise { - let initialRuntimeSeed: TuiInitialRuntimeSeed | undefined; - let surfaceRegistration: Promise<{ - registration: TuiSurfaceRegistration; - initialRuntimeSeed?: TuiInitialRuntimeSeed; - }>; - try { - surfaceRegistration = resolveTuiSurfaceRegistration(api, { - captureInitialRuntime(seed) { - initialRuntimeSeed = seed; - }, - }) - .then((registration) => ({ registration, initialRuntimeSeed })) - .catch(() => ({ registration: FALLBACK_SURFACE_REGISTRATION })); - } catch { - surfaceRegistration = Promise.resolve({ registration: FALLBACK_SURFACE_REGISTRATION }); - } - - registerQuotaDialogCommands(api, gate); - void surfaceRegistration.then(({ registration, initialRuntimeSeed }) => - gate.activate( - registration, - initialRuntimeSeed ? createTuiInitialLoadCoordinator(initialRuntimeSeed) : undefined, - ), - ); - registerStableTuiSlots(api, gate.current); -} - -const tui: TuiPlugin = async (api) => { - const registrationGate = createTuiRegistrationGate(); - api.lifecycle.onDispose(() => { - registrationGate.dispose(); - disposeQuotaTelemetryOwner(createTuiQuotaClient(api)); - }); - - void initializeTuiRegistration(api, registrationGate).catch(() => {}); -}; - -const pluginModule: TuiPluginModule & { id: string } = { - id, - tui, -}; - -export default pluginModule; diff --git a/src/types/tui-runtime-shims.d.ts b/src/types/tui-runtime-shims.d.ts deleted file mode 100644 index ce361ae0..00000000 --- a/src/types/tui-runtime-shims.d.ts +++ /dev/null @@ -1,228 +0,0 @@ -declare module "@opentui/solid" { - export namespace JSX { - interface Element {} - interface IntrinsicElements { - box: any; - text: any; - b: any; - span: any; - scrollbox: any; - } - } - - export type SolidPlugin, Context = unknown> = { - order?: number; - slots?: Record JSX.Element | null>; - }; -} - -declare module "@opentui/solid/jsx-runtime" { - export namespace JSX { - interface Element {} - interface IntrinsicElements { - box: any; - text: any; - b: any; - span: any; - scrollbox: any; - } - } - - export const jsx: any; - export const jsxs: any; - export const Fragment: any; -} - -declare module "solid-js" { - export function createSignal(value: T): [() => T, (value: T | ((prev: T) => T)) => T]; - export function createEffect(fn: () => void): void; - export function onCleanup(fn: () => void): void; - export function Show(props: { - when: T | undefined | null | false; - children?: any; - fallback?: any; - }): any; - export function Index(props: { - each: readonly T[] | undefined | null | false; - children?: (item: () => T, index: number) => any; - }): any; -} - -declare module "@opencode-ai/plugin/tui" { - import type { OpencodeClient } from "@opencode-ai/sdk/v2"; - import type { JSX, SolidPlugin } from "@opentui/solid"; - - export type TuiPromptInfo = { - input: string; - parts: ReadonlyArray; - }; - - export type TuiPromptRef = { - focused: boolean; - current: TuiPromptInfo; - set(prompt: TuiPromptInfo): void; - reset(): void; - blur(): void; - focus(): void; - submit(): void; - }; - - export type TuiPromptProps = { - sessionID?: string; - workspaceID?: string; - visible?: boolean; - disabled?: boolean; - onSubmit?: () => void; - ref?: (ref: TuiPromptRef | undefined) => void; - hint?: JSX.Element; - right?: JSX.Element; - showPlaceholder?: boolean; - placeholders?: { - normal?: string[]; - shell?: string[]; - }; - }; - - export type TuiPluginApi = { - state: { - provider: ReadonlyArray<{ id: string }>; - path: { - worktree: string; - directory: string; - }; - session: { - messages: (sessionID: string) => ReadonlyArray; - }; - }; - theme: { - current: { - text: unknown; - textMuted: unknown; - }; - }; - route: { - readonly current: - | { name: "home" } - | { name: "session"; params: { sessionID: string } } - | { name: string; params?: Record }; - }; - ui: { - Prompt: (props: TuiPromptProps) => JSX.Element; - DialogPrompt: (props: { - title: string; - description?: () => JSX.Element; - placeholder?: string; - value?: string; - busy?: boolean; - busyText?: string; - onConfirm?: (value: string) => void; - onCancel?: () => void; - }) => JSX.Element; - toast: (input: { - variant?: "info" | "success" | "warning" | "error"; - title?: string; - message: string; - duration?: number; - }) => void; - dialog: { - replace: (render: () => JSX.Element, onClose?: () => void) => void; - clear: () => void; - setSize: (size: "medium" | "large" | "xlarge") => void; - }; - }; - event: { - on: (type: string, handler: (event: any) => void) => () => void; - }; - slots: { - register: ( - plugin: Omit, "id"> & { - id?: never; - order?: number; - slots: Record JSX.Element | null>; - }, - ) => string; - }; - lifecycle: { - onDispose: (fn: () => void | Promise) => () => void; - }; - keymap: { - registerLayer: (layer: { - commands: Array<{ - namespace: string; - name: string; - title: string; - desc?: string; - category?: string; - slashName?: string; - run: (input?: unknown) => void | Promise; - }>; - bindings?: unknown[]; - }) => () => void; - }; - client: { - app: { - log: (params: { - body: { - service: string; - level: "debug" | "info" | "warn" | "error"; - message: string; - extra?: Record; - }; - }) => Promise; - }; - config?: { - providers?: () => Promise<{ - data?: { - providers?: Array<{ id: string }>; - }; - }>; - get?: () => Promise<{ - data?: Record; - }>; - }; - session: { - prompt: OpencodeClient["session"]["prompt"]; - get?: (params: { path: { id: string } }) => Promise<{ - data?: { - model?: { - id?: string; - providerID?: string; - }; - }; - }>; - }; - }; - kv: { - get: (key: string, fallback?: Value) => Value; - set: (key: string, value: unknown) => void; - }; - }; - - export type TuiPluginMeta = { - state: "first" | "updated" | "same"; - id: string; - source: string; - spec: string; - target: string; - requested?: string; - version?: string; - modified?: number; - first_time: number; - last_time: number; - time_changed: number; - load_count: number; - fingerprint: string; - }; - - export type TuiPlugin = ( - api: TuiPluginApi, - options: unknown, - meta: TuiPluginMeta, - ) => Promise; - - export type TuiPluginModule = { - id?: string; - tui: TuiPlugin; - server?: never; - }; -} diff --git a/tests/helpers/plugin-test-harness.ts b/tests/helpers/plugin-test-harness.ts index e5d6e6b5..c94f691a 100644 --- a/tests/helpers/plugin-test-harness.ts +++ b/tests/helpers/plugin-test-harness.ts @@ -78,25 +78,6 @@ interface PluginTuiConfigInspectionOverrides { workspaceRoot?: string; } -function createSchemaChain() { - const chain: any = {}; - chain.optional = () => chain; - chain.describe = () => chain; - chain.int = () => chain; - chain.min = () => chain; - return chain; -} - -export function createPluginToolMockModule() { - const toolFn = ((definition: unknown) => definition) as any; - toolFn.schema = { - boolean: () => createSchemaChain(), - number: () => createSchemaChain(), - }; - - return { tool: toolFn }; -} - export function createConfigModuleMock(loadConfig: MockFunction) { return { loadConfig, diff --git a/tests/helpers/tui-first-quota-timing.ts b/tests/helpers/tui-first-quota-timing.ts deleted file mode 100644 index 4e8d9a62..00000000 --- a/tests/helpers/tui-first-quota-timing.ts +++ /dev/null @@ -1,367 +0,0 @@ -import { vi } from "vitest"; - -export const FIRST_QUOTA_OBSERVATION_HORIZON_MS = 10_000; -export const FIRST_QUOTA_WARMUP_SAMPLES = 3; -export const FIRST_QUOTA_RECORDED_SAMPLES = 30; - -export type FirstQuotaObservation = { - firstApplyMs: number | null; - loaderStartsAtFirstApply: number | null; - loaderCompletionsAtFirstApply: number | null; - acceptedLoaderOrdinal: number | null; - applyCount: number; - afterApplyCount: number; - loaderCompletionsTotal: number; - unsubscribeCount: number; - disposeCount: number; - predecessorApplyCount: number; - sinkValue: string | null; - censored: boolean; -}; - -export type TimingAggregate = { - min: number | null; - median: number | null; - p95: number | null; - max: number | null; - censored: number; -}; - -export type FirstQuotaScenario = { - id: string; - description: string; - loaderSteps: readonly LoaderStep[]; - eventAtMs?: number; - eventRefreshDelaysMs?: readonly number[]; - recoveryDelaysMs?: readonly number[]; - disposeAtMs?: number; - successor?: { - createAtMs: number; - predecessorSettlesAtMs: number; - successorLatencyMs: number; - }; -}; - -export type FirstQuotaScenarioResult = { - scenario: FirstQuotaScenario; - samples: FirstQuotaObservation[]; - firstApplyMs: TimingAggregate; - loaderCompletions: TimingAggregate; - loaderStarts: TimingAggregate; -}; - -type LoaderStep = { - afterMs: number; - outcome: "resolve" | "reject"; -}; - -type Lifecycle = { - retain: () => void; - release: () => void; -}; - -type LifecycleOptions = { - load: () => Promise; - apply: (value: T) => void; - afterApply?: (value: T) => void; - intervalMs: number; - eventRefreshDelaysMs: readonly number[]; - recoveryDelaysMs?: readonly number[]; - subscribe: (scheduleRefresh: () => void) => Array<() => void>; - onDispose: () => void; -}; - -export type CreateTuiRefreshLifecycle = (options: LifecycleOptions) => Lifecycle; - -type ScriptedValue = { - ordinal: number; - value: string; -}; - -function flushPromises(): Promise { - return Promise.resolve() - .then(() => undefined) - .then(() => undefined) - .then(() => undefined); -} - -function aggregate(values: Array): TimingAggregate { - const recorded = values - .filter((value): value is number => value !== null) - .sort((left, right) => left - right); - - if (recorded.length === 0) { - return { - min: null, - median: null, - p95: null, - max: null, - censored: values.length, - }; - } - - const valueAt = (index: number): number => { - const value = recorded[index]; - if (value === undefined) throw new Error(`Missing aggregate value at index ${index}`); - return value; - }; - const medianIndex = Math.floor(recorded.length / 2); - const median = - recorded.length % 2 === 0 - ? (valueAt(medianIndex - 1) + valueAt(medianIndex)) / 2 - : valueAt(medianIndex); - - return { - min: valueAt(0), - median, - p95: valueAt(Math.ceil(recorded.length * 0.95) - 1), - max: valueAt(recorded.length - 1), - censored: values.length - recorded.length, - }; -} - -async function runStandardSample( - createLifecycle: CreateTuiRefreshLifecycle, - scenario: FirstQuotaScenario, -): Promise { - vi.clearAllTimers(); - vi.setSystemTime(0); - - const createdAt = Date.now(); - let loaderStarts = 0; - let loaderCompletions = 0; - let applyCount = 0; - let afterApplyCount = 0; - let unsubscribeCount = 0; - let disposeCount = 0; - let released = false; - let scheduleRefresh: (() => void) | undefined; - let firstApplyMs: number | null = null; - let loaderStartsAtFirstApply: number | null = null; - let loaderCompletionsAtFirstApply: number | null = null; - let acceptedLoaderOrdinal: number | null = null; - let sinkValue: string | null = null; - - const lifecycle = createLifecycle({ - load: () => { - const ordinal = ++loaderStarts; - const step = scenario.loaderSteps[ordinal - 1]; - if (!step) return new Promise(() => {}); - - return new Promise((resolve, reject) => { - setTimeout(() => { - loaderCompletions += 1; - if (step.outcome === "reject") { - reject(new Error(`scripted rejection ${ordinal}`)); - return; - } - resolve({ ordinal, value: `loader-${ordinal}` }); - }, step.afterMs); - }); - }, - apply: (next) => { - applyCount += 1; - sinkValue = next.value; - if (firstApplyMs !== null) return; - - firstApplyMs = Date.now() - createdAt; - loaderStartsAtFirstApply = loaderStarts; - loaderCompletionsAtFirstApply = loaderCompletions; - acceptedLoaderOrdinal = next.ordinal; - }, - afterApply: () => { - afterApplyCount += 1; - }, - intervalMs: 60_000, - eventRefreshDelaysMs: scenario.eventRefreshDelaysMs ?? [], - recoveryDelaysMs: scenario.recoveryDelaysMs, - subscribe: (receivedScheduleRefresh) => { - scheduleRefresh = receivedScheduleRefresh; - return [ - () => { - unsubscribeCount += 1; - }, - ]; - }, - onDispose: () => { - disposeCount += 1; - }, - }); - lifecycle.retain(); - - if (scenario.eventAtMs !== undefined) { - if (scenario.eventAtMs === 0) { - scheduleRefresh?.(); - } else { - setTimeout(() => scheduleRefresh?.(), scenario.eventAtMs); - } - } - - if (scenario.disposeAtMs !== undefined) { - setTimeout(() => { - released = true; - lifecycle.release(); - }, scenario.disposeAtMs); - } - - await flushPromises(); - await vi.advanceTimersByTimeAsync(FIRST_QUOTA_OBSERVATION_HORIZON_MS); - await flushPromises(); - - if (!released) lifecycle.release(); - - return { - firstApplyMs, - loaderStartsAtFirstApply, - loaderCompletionsAtFirstApply, - acceptedLoaderOrdinal, - applyCount, - afterApplyCount, - loaderCompletionsTotal: loaderCompletions, - unsubscribeCount, - disposeCount, - predecessorApplyCount: 0, - sinkValue, - censored: firstApplyMs === null, - }; -} - -async function runSuccessorSample( - createLifecycle: CreateTuiRefreshLifecycle, - scenario: FirstQuotaScenario, -): Promise { - const successor = scenario.successor; - if (!successor) throw new Error(`Missing successor script for ${scenario.id}`); - - vi.clearAllTimers(); - vi.setSystemTime(0); - - let predecessorApplyCount = 0; - let loaderCompletions = 0; - let applyCount = 0; - let afterApplyCount = 0; - let unsubscribeCount = 0; - let disposeCount = 0; - let firstApplyMs: number | null = null; - let sinkValue: string | null = null; - let measuredLifecycle: Lifecycle | undefined; - - const predecessor = createLifecycle({ - load: () => - new Promise((resolve) => { - setTimeout(() => resolve("predecessor"), successor.predecessorSettlesAtMs); - }), - apply: (value) => { - predecessorApplyCount += 1; - sinkValue = value; - }, - intervalMs: 60_000, - eventRefreshDelaysMs: [], - subscribe: () => [ - () => { - unsubscribeCount += 1; - }, - ], - onDispose: () => { - disposeCount += 1; - }, - }); - predecessor.retain(); - - setTimeout(() => { - predecessor.release(); - const successorCreatedAt = Date.now(); - measuredLifecycle = createLifecycle({ - load: () => - new Promise((resolve) => { - setTimeout(() => { - loaderCompletions += 1; - resolve({ ordinal: 1, value: "successor" }); - }, successor.successorLatencyMs); - }), - apply: (next) => { - applyCount += 1; - sinkValue = next.value; - if (firstApplyMs === null) firstApplyMs = Date.now() - successorCreatedAt; - }, - afterApply: () => { - afterApplyCount += 1; - }, - intervalMs: 60_000, - eventRefreshDelaysMs: [], - subscribe: () => [ - () => { - unsubscribeCount += 1; - }, - ], - onDispose: () => { - disposeCount += 1; - }, - }); - measuredLifecycle.retain(); - }, successor.createAtMs); - - await vi.advanceTimersByTimeAsync(FIRST_QUOTA_OBSERVATION_HORIZON_MS); - await flushPromises(); - measuredLifecycle?.release(); - - return { - firstApplyMs, - loaderStartsAtFirstApply: firstApplyMs === null ? null : 1, - loaderCompletionsAtFirstApply: firstApplyMs === null ? null : loaderCompletions, - acceptedLoaderOrdinal: firstApplyMs === null ? null : 1, - applyCount, - afterApplyCount, - loaderCompletionsTotal: loaderCompletions, - unsubscribeCount, - disposeCount, - predecessorApplyCount, - sinkValue, - censored: firstApplyMs === null, - }; -} - -export async function runFirstQuotaScenarioSamples( - createLifecycle: CreateTuiRefreshLifecycle, - scenario: FirstQuotaScenario, -): Promise { - const runSample = scenario.successor ? runSuccessorSample : runStandardSample; - - for (let index = 0; index < FIRST_QUOTA_WARMUP_SAMPLES; index += 1) { - await runSample(createLifecycle, scenario); - } - - const samples: FirstQuotaObservation[] = []; - for (let index = 0; index < FIRST_QUOTA_RECORDED_SAMPLES; index += 1) { - samples.push(await runSample(createLifecycle, scenario)); - } - - return { - scenario, - samples, - firstApplyMs: aggregate(samples.map((sample) => sample.firstApplyMs)), - loaderCompletions: aggregate(samples.map((sample) => sample.loaderCompletionsAtFirstApply)), - loaderStarts: aggregate(samples.map((sample) => sample.loaderStartsAtFirstApply)), - }; -} - -function formatValue(value: number | null): string { - return value === null ? "CENSORED" : String(value); -} - -export function formatFirstQuotaReport(results: FirstQuotaScenarioResult[]): string { - const lines = [ - "TUI first accepted quota run (virtual ms)", - `Warmups: ${FIRST_QUOTA_WARMUP_SAMPLES}; recorded: ${FIRST_QUOTA_RECORDED_SAMPLES}; horizon: ${FIRST_QUOTA_OBSERVATION_HORIZON_MS} ms`, - "| Scenario | T min | T median | T p95 | T max | T censored | C min | C median | C p95 | C max | C censored | Starts median |", - "|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|", - ]; - - for (const result of results) { - lines.push( - `| ${result.scenario.id} | ${formatValue(result.firstApplyMs.min)} | ${formatValue(result.firstApplyMs.median)} | ${formatValue(result.firstApplyMs.p95)} | ${formatValue(result.firstApplyMs.max)} | ${result.firstApplyMs.censored} | ${formatValue(result.loaderCompletions.min)} | ${formatValue(result.loaderCompletions.median)} | ${formatValue(result.loaderCompletions.p95)} | ${formatValue(result.loaderCompletions.max)} | ${result.loaderCompletions.censored} | ${formatValue(result.loaderStarts.median)} |`, - ); - } - - return lines.join("\n"); -} diff --git a/tests/helpers/tui-provider-availability-timing.ts b/tests/helpers/tui-provider-availability-timing.ts deleted file mode 100644 index 313fafea..00000000 --- a/tests/helpers/tui-provider-availability-timing.ts +++ /dev/null @@ -1,177 +0,0 @@ -export const TUI_PROVIDER_AVAILABILITY_WARMUPS = 3; -export const TUI_PROVIDER_AVAILABILITY_SAMPLES = 30; -export const TUI_PROVIDER_AVAILABILITY_HORIZON_MS = 10_000; -export const TUI_PROVIDER_AVAILABILITY_JITTER_OFFSETS_MS = [0, 3, 7, 1, 5] as const; - -export type ProviderAvailabilityMetricAggregate = { - min: number | null; - median: number | null; - p95: number | null; - max: number | null; - censored: number; -}; - -export type ProviderAvailabilityTimingSample = { - scenario: string; - controlledDelayMs: number; - readyCompactMs: number | null; - homeCompletionMs: number | null; - callsByProvider: Record; - totalAvailabilityCalls: number; - fetchesByProvider: Record; - totalFetches: number; - outputSignature: string | null; - censored: boolean; -}; - -export type ProviderAvailabilityTimingResult = { - scenario: string; - delayDescription: string; - samples: ProviderAvailabilityTimingSample[]; - readyCompact: ProviderAvailabilityMetricAggregate; - homeCompletion: ProviderAvailabilityMetricAggregate; - distinctCallCountVectors: string[]; - distinctFetchCountVectors: string[]; - distinctOutputSignatures: string[]; -}; - -export function getProviderAvailabilityJitterOffsetMs(sampleIndex: number): number { - if (sampleIndex < 0) return 0; - return ( - TUI_PROVIDER_AVAILABILITY_JITTER_OFFSETS_MS[ - sampleIndex % TUI_PROVIDER_AVAILABILITY_JITTER_OFFSETS_MS.length - ] ?? 0 - ); -} - -function aggregate(values: Array): ProviderAvailabilityMetricAggregate { - const recorded = values - .filter((value): value is number => value !== null) - .sort((left, right) => left - right); - - if (recorded.length === 0) { - return { - min: null, - median: null, - p95: null, - max: null, - censored: values.length, - }; - } - - const valueAt = (index: number): number => { - const value = recorded[index]; - if (value === undefined) throw new Error(`Missing aggregate value at index ${index}`); - return value; - }; - const medianIndex = Math.floor(recorded.length / 2); - - return { - min: valueAt(0), - median: - recorded.length % 2 === 0 - ? (valueAt(medianIndex - 1) + valueAt(medianIndex)) / 2 - : valueAt(medianIndex), - p95: valueAt(Math.ceil(recorded.length * 0.95) - 1), - max: valueAt(recorded.length - 1), - censored: values.length - recorded.length, - }; -} - -function countVector( - counts: Record, - orderedProviderIds: readonly string[], - total: number, -): string { - return [ - ...orderedProviderIds.map((providerId) => `${providerId}=${counts[providerId] ?? 0}`), - `total=${total}`, - ].join(","); -} - -export async function runProviderAvailabilityTimingSamples(params: { - scenario: string; - delayDescription: string; - orderedProviderIds: readonly string[]; - runSample: (sampleIndex: number) => Promise; -}): Promise { - for (let index = 0; index < TUI_PROVIDER_AVAILABILITY_WARMUPS; index += 1) { - await params.runSample(-(index + 1)); - } - - const samples: ProviderAvailabilityTimingSample[] = []; - for (let index = 0; index < TUI_PROVIDER_AVAILABILITY_SAMPLES; index += 1) { - samples.push(await params.runSample(index)); - } - - return { - scenario: params.scenario, - delayDescription: params.delayDescription, - samples, - readyCompact: aggregate(samples.map((sample) => sample.readyCompactMs)), - homeCompletion: aggregate(samples.map((sample) => sample.homeCompletionMs)), - distinctCallCountVectors: [ - ...new Set( - samples.map((sample) => - countVector( - sample.callsByProvider, - params.orderedProviderIds, - sample.totalAvailabilityCalls, - ), - ), - ), - ], - distinctFetchCountVectors: [ - ...new Set( - samples.map((sample) => - countVector(sample.fetchesByProvider, params.orderedProviderIds, sample.totalFetches), - ), - ), - ], - distinctOutputSignatures: [ - ...new Set( - samples - .map((sample) => sample.outputSignature) - .filter((value): value is string => value !== null), - ), - ], - }; -} - -function formatMetric(value: number | null): string { - return value === null ? "N/A" : String(value); -} - -function formatRange(metric: ProviderAvailabilityMetricAggregate): string { - if (metric.min === null || metric.max === null) return "N/A"; - return String(metric.max - metric.min); -} - -export function formatProviderAvailabilityTimingReport( - results: readonly ProviderAvailabilityTimingResult[], -): string { - const lines = [ - "TUI home provider availability measurement (virtual ms)", - `Warmups: ${TUI_PROVIDER_AVAILABILITY_WARMUPS}; recorded: ${TUI_PROVIDER_AVAILABILITY_SAMPLES}; horizon: ${TUI_PROVIDER_AVAILABILITY_HORIZON_MS} ms; no discarded samples`, - `Deterministic jitter offsets: ${TUI_PROVIDER_AVAILABILITY_JITTER_OFFSETS_MS.join(",")}`, - "| Scenario | Delay | Metric | Min | Median | p95 | Max | Censored | Range | Availability calls | Fetches |", - "|---|---|---|---:|---:|---:|---:|---:|---:|---|---|", - ]; - - for (const result of results) { - const metric = - result.readyCompact.censored < result.samples.length - ? result.readyCompact - : result.homeCompletion; - const metricLabel = - result.readyCompact.censored < result.samples.length ? "ready compact" : "home completion"; - lines.push( - `| ${result.scenario} | ${result.delayDescription} | ${metricLabel} | ${formatMetric(metric.min)} | ${formatMetric(metric.median)} | ${formatMetric(metric.p95)} | ${formatMetric(metric.max)} | ${metric.censored} | ${formatRange(metric)} | ${result.distinctCallCountVectors.join(" || ")} | ${result.distinctFetchCountVectors.join(" || ")} |`, - ); - lines.push( - `Output signatures (${result.scenario}): ${result.distinctOutputSignatures.join(" || ")}`, - ); - } - - return lines.join("\n"); -} diff --git a/tests/helpers/tui-startup-runtime-resolution.ts b/tests/helpers/tui-startup-runtime-resolution.ts deleted file mode 100644 index f307db52..00000000 --- a/tests/helpers/tui-startup-runtime-resolution.ts +++ /dev/null @@ -1,251 +0,0 @@ -export const TUI_RUNTIME_RESOLUTION_WARMUPS = 3; -export const TUI_RUNTIME_RESOLUTION_SAMPLES = 30; -export const TUI_RUNTIME_RESOLUTION_HORIZON_MS = 10_000; - -export type StartupRuntimePhase = - | "registration" - | "initial-session" - | "initial-home" - | "home-export" - | "recovery-session" - | "event-session" - | "event-home" - | "interval-session" - | "interval-home" - | "refresh-session" - | "refresh-home"; - -export type StartupTraceEvent = { - sequence: number; - phase: StartupRuntimePhase; - operation: "runtime-context" | "config-load"; - edge: "start" | "complete"; - atMs: number; - configGeneration?: number; -}; - -export type StartupMilestones = { - sidebarMs: number | null; - compactMs: number | null; - homeMs: number | null; - firstContentMs: number | null; - allInitialContentMs: number | null; -}; - -export type StartupRuntimeSample = { - controlledDelayMs: number; - milestones: StartupMilestones; - trace: StartupTraceEvent[]; - sessionLoaderInvocations: number; - homeLoaderInvocations: number; - rendered: { - sidebar: boolean; - compact: boolean; - home: boolean; - }; -}; - -export type MetricAggregate = { - min: number | null; - median: number | null; - p95: number | null; - max: number | null; - censored: number; -}; - -type InitialCallCounts = { - runtimeStarts: number; - runtimeCompletions: number; - configStarts: number; - configCompletions: number; -}; - -export type StartupRuntimeResult = { - controlledDelayMs: number; - samples: StartupRuntimeSample[]; - milestones: Record; - countsAtMilestones: Record; - distinctCountVectors: string[]; - sessionLoaderInvocations: MetricAggregate; - homeLoaderInvocations: MetricAggregate; -}; - -const INITIAL_PHASES = new Set([ - "registration", - "initial-session", - "initial-home", -]); - -function aggregate(values: Array): MetricAggregate { - const recorded = values - .filter((value): value is number => value !== null) - .sort((left, right) => left - right); - - if (recorded.length === 0) { - return { - min: null, - median: null, - p95: null, - max: null, - censored: values.length, - }; - } - - const valueAt = (index: number): number => { - const value = recorded[index]; - if (value === undefined) throw new Error(`Missing aggregate value at index ${index}`); - return value; - }; - const medianIndex = Math.floor(recorded.length / 2); - - return { - min: valueAt(0), - median: - recorded.length % 2 === 0 - ? (valueAt(medianIndex - 1) + valueAt(medianIndex)) / 2 - : valueAt(medianIndex), - p95: valueAt(Math.ceil(recorded.length * 0.95) - 1), - max: valueAt(recorded.length - 1), - censored: values.length - recorded.length, - }; -} - -function countInitialCallsAt(sample: StartupRuntimeSample, atMs: number | null): InitialCallCounts { - if (atMs === null) { - return { runtimeStarts: 0, runtimeCompletions: 0, configStarts: 0, configCompletions: 0 }; - } - - const events = sample.trace.filter( - (event) => INITIAL_PHASES.has(event.phase) && event.atMs <= atMs, - ); - return { - runtimeStarts: events.filter( - (event) => event.operation === "runtime-context" && event.edge === "start", - ).length, - runtimeCompletions: events.filter( - (event) => event.operation === "runtime-context" && event.edge === "complete", - ).length, - configStarts: events.filter( - (event) => event.operation === "config-load" && event.edge === "start", - ).length, - configCompletions: events.filter( - (event) => event.operation === "config-load" && event.edge === "complete", - ).length, - }; -} - -function countVector(sample: StartupRuntimeSample): string { - const parts = (Object.keys(sample.milestones) as Array).map((key) => { - const counts = countInitialCallsAt(sample, sample.milestones[key]); - return `${key}:${counts.runtimeStarts}/${counts.runtimeCompletions}/${counts.configStarts}/${counts.configCompletions}`; - }); - parts.push(`loaders:${sample.sessionLoaderInvocations}/${sample.homeLoaderInvocations}`); - return parts.join(";"); -} - -export async function runStartupRuntimeSamples( - controlledDelayMs: number, - runSample: (controlledDelayMs: number, sampleIndex: number) => Promise, -): Promise { - for (let index = 0; index < TUI_RUNTIME_RESOLUTION_WARMUPS; index += 1) { - await runSample(controlledDelayMs, -(index + 1)); - } - - const samples: StartupRuntimeSample[] = []; - for (let index = 0; index < TUI_RUNTIME_RESOLUTION_SAMPLES; index += 1) { - samples.push(await runSample(controlledDelayMs, index)); - } - - const milestoneKeys = Object.keys(samples[0]?.milestones ?? {}) as Array; - const milestones = Object.fromEntries( - milestoneKeys.map((key) => [key, aggregate(samples.map((sample) => sample.milestones[key]))]), - ) as Record; - const countsAtMilestones = Object.fromEntries( - milestoneKeys.map((key) => [ - key, - samples.map((sample) => countInitialCallsAt(sample, sample.milestones[key])), - ]), - ) as Record; - - return { - controlledDelayMs, - samples, - milestones, - countsAtMilestones, - distinctCountVectors: [...new Set(samples.map(countVector))], - sessionLoaderInvocations: aggregate(samples.map((sample) => sample.sessionLoaderInvocations)), - homeLoaderInvocations: aggregate(samples.map((sample) => sample.homeLoaderInvocations)), - }; -} - -function formatMetric(value: number | null): string { - return value === null ? "CENSORED" : String(value); -} - -function formatCountRange(counts: InitialCallCounts[]): string { - const vectors = [ - ...new Set( - counts.map( - (count) => - `${count.runtimeStarts}/${count.runtimeCompletions} runtime, ${count.configStarts}/${count.configCompletions} config`, - ), - ), - ]; - return vectors.join(" | "); -} - -function formatTrace(trace: StartupTraceEvent[]): string { - return trace - .map( - (event) => - `${event.atMs}:${event.phase}:${event.operation}:${event.edge}${ - event.configGeneration === undefined ? "" : `:g${event.configGeneration}` - }`, - ) - .join(" -> "); -} - -export function formatStartupRuntimeReport(results: StartupRuntimeResult[]): string { - const lines = [ - "TUI startup runtime/config resolution measurement (virtual ms)", - `Warmups: ${TUI_RUNTIME_RESOLUTION_WARMUPS}; recorded: ${TUI_RUNTIME_RESOLUTION_SAMPLES}; horizon: ${TUI_RUNTIME_RESOLUTION_HORIZON_MS} ms; no discarded samples`, - "Counts are start/complete. Home export is traced but excluded from startup counts.", - "| Delay | Milestone | Min | Median | p95 | Max | Censored | Runtime/config counts at milestone |", - "|---:|---|---:|---:|---:|---:|---:|---|", - ]; - - for (const result of results) { - for (const [key, label] of [ - ["sidebarMs", "sidebar"], - ["compactMs", "compact"], - ["homeMs", "home"], - ["firstContentMs", "first content"], - ["allInitialContentMs", "all initial"], - ] as const) { - const metric = result.milestones[key]; - lines.push( - `| ${result.controlledDelayMs} | ${label} | ${formatMetric(metric.min)} | ${formatMetric(metric.median)} | ${formatMetric(metric.p95)} | ${formatMetric(metric.max)} | ${metric.censored} | ${formatCountRange(result.countsAtMilestones[key])} |`, - ); - } - - lines.push( - `| ${result.controlledDelayMs} | loader calls | ${formatMetric(result.sessionLoaderInvocations.min)}/${formatMetric(result.homeLoaderInvocations.min)} | ${formatMetric(result.sessionLoaderInvocations.median)}/${formatMetric(result.homeLoaderInvocations.median)} | ${formatMetric(result.sessionLoaderInvocations.p95)}/${formatMetric(result.homeLoaderInvocations.p95)} | ${formatMetric(result.sessionLoaderInvocations.max)}/${formatMetric(result.homeLoaderInvocations.max)} | ${result.sessionLoaderInvocations.censored + result.homeLoaderInvocations.censored} | session/home |`, - ); - lines.push( - `Count vectors: ${result.distinctCountVectors.length} distinct (${result.distinctCountVectors.join(" || ")})`, - ); - const sample = result.samples[0]; - if (sample) - lines.push( - `Ordering (${result.controlledDelayMs} ms sample 0): ${formatTrace(sample.trace)}`, - ); - const allCounts = result.countsAtMilestones.allInitialContentMs[0]; - if (allCounts) { - lines.push( - `Aggregate controlled config delay before all initial content: ${allCounts.configStarts * result.controlledDelayMs} ms; critical path median: ${formatMetric(result.milestones.allInitialContentMs.median)} ms`, - ); - } - } - - return lines.join("\n"); -} diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index bef71c65..d5ee8750 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -285,7 +285,7 @@ describe("init installer planning and merge behavior", () => { writeFileSync( join(projectDir, "tui.json"), JSON.stringify({ - plugin: ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.tsx"], + plugin: ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.js"], tui: { plugin: [["some-other-plugin", { debug: true }]], }, @@ -369,7 +369,7 @@ describe("init installer planning and merge behavior", () => { writeFileSync( join(projectDir, "opencode.json"), JSON.stringify({ - plugin: ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.tsx"], + plugin: ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.js"], }), "utf8", ); @@ -383,7 +383,7 @@ describe("init installer planning and merge behavior", () => { const opencode = readJson(join(projectDir, "opencode.jsonc")); expect(opencode.plugin).toEqual([ - "file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.tsx", + "file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.js", "@slkiser/opencode-quota@latest", ]); }); diff --git a/tests/lib.tui-native-provider-quota.test.ts b/tests/lib.tui-native-provider-quota.test.ts deleted file mode 100644 index 1afce9c9..00000000 --- a/tests/lib.tui-native-provider-quota.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { hasNativeProviderQuotaClient } from "../src/lib/tui-native-provider-quota.js"; - -describe("hasNativeProviderQuotaClient", () => { - it("detects supported experimental provider quota shapes without invoking them", () => { - const providerQuota = vi.fn(); - const provider_quota = vi.fn(); - const quota = vi.fn(); - - expect(hasNativeProviderQuotaClient({ experimental: { providerQuota } })).toBe(true); - expect(hasNativeProviderQuotaClient({ experimental: { provider_quota } })).toBe(true); - expect(hasNativeProviderQuotaClient({ experimental: { provider: { quota } } })).toBe(true); - - expect(providerQuota).not.toHaveBeenCalled(); - expect(provider_quota).not.toHaveBeenCalled(); - expect(quota).not.toHaveBeenCalled(); - }); - - it("returns false for missing or unrelated client shapes", () => { - expect(hasNativeProviderQuotaClient(undefined)).toBe(false); - expect(hasNativeProviderQuotaClient(null)).toBe(false); - expect(hasNativeProviderQuotaClient({})).toBe(false); - expect(hasNativeProviderQuotaClient({ experimental: null })).toBe(false); - expect(hasNativeProviderQuotaClient({ experimental: { providerQuota: undefined } })).toBe( - false, - ); - expect(hasNativeProviderQuotaClient({ experimental: { provider_quota: null } })).toBe(false); - expect(hasNativeProviderQuotaClient({ experimental: { provider: {} } })).toBe(false); - expect(hasNativeProviderQuotaClient({ experimental: { provider: { quota: undefined } } })).toBe( - false, - ); - expect(hasNativeProviderQuotaClient({ providerQuota: vi.fn() })).toBe(false); - }); - - it("ignores falsey primitive feature-flag sentinels", () => { - expect(hasNativeProviderQuotaClient({ experimental: { providerQuota: false } })).toBe(false); - expect(hasNativeProviderQuotaClient({ experimental: { provider_quota: 0 } })).toBe(false); - expect(hasNativeProviderQuotaClient({ experimental: { provider: { quota: "" } } })).toBe(false); - }); -}); diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 8f940c1c..8181ddf9 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -6,7 +6,6 @@ import { createPluginTestClient as createClient, createConfigModuleMock, createPluginRuntimePathsMockModule, - createPluginToolMockModule, createPluginTuiConfigInspection, createPricingModuleMock, createProvidersRegistryModuleMock, @@ -57,7 +56,6 @@ const resetMocks = vi.hoisted(() => ({ formatQuotaResetNotification: vi.fn(), })); -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); vi.mock("../src/providers/registry.js", () => createProvidersRegistryModuleMock(mocks.getProviders), diff --git a/tests/plugin.quota-command.test.ts b/tests/plugin.quota-command.test.ts index d9354f44..9bc2bcfa 100644 --- a/tests/plugin.quota-command.test.ts +++ b/tests/plugin.quota-command.test.ts @@ -8,7 +8,6 @@ import { createPluginTestClient as createClient, createConfigModuleMock, createPluginRuntimePathsMockModule, - createPluginToolMockModule, createPricingModuleMock, createProvidersRegistryModuleMock, createSessionTokensModuleMock, @@ -103,8 +102,6 @@ const mocks = vi.hoisted(() => ({ disposeQuotaTelemetryOwner: vi.fn(), })); -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); - vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); vi.mock("../src/providers/registry.js", () => diff --git a/tests/plugin.quota-status-command.test.ts b/tests/plugin.quota-status-command.test.ts index 0dfea115..9be582ff 100644 --- a/tests/plugin.quota-status-command.test.ts +++ b/tests/plugin.quota-status-command.test.ts @@ -5,7 +5,6 @@ import { createAlibabaAuthModuleMock, createPluginTestClient as createClient, createConfigModuleMock, - createPluginToolMockModule, createPluginTuiConfigInspection, createPricingModuleMock, createProvidersRegistryModuleMock, @@ -30,8 +29,6 @@ const mocks = vi.hoisted(() => ({ inspectTuiConfig: vi.fn(), })); -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); - vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); vi.mock("../src/providers/registry.js", () => diff --git a/tests/quota-surface-parity.test.ts b/tests/quota-surface-parity.test.ts deleted file mode 100644 index 2e5fd470..00000000 --- a/tests/quota-surface-parity.test.ts +++ /dev/null @@ -1,648 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import { - createPluginTestClient, - createPluginToolMockModule, - createPricingModuleMock, - createProvidersRegistryModuleMock, -} from "./helpers/plugin-test-harness.js"; - -const TEST_ACCOUNTING = { - resultType: "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", -} as const; - -const mocks = vi.hoisted(() => ({ - mockProviders: [] as any[], - getProviders: vi.fn(), - getPricingSnapshotMeta: vi.fn(), - getPricingSnapshotSource: vi.fn(), - getRuntimePricingRefreshStatePath: vi.fn(), - getRuntimePricingSnapshotPath: vi.fn(), - maybeRefreshPricingSnapshot: vi.fn(), - setPricingSnapshotAutoRefresh: vi.fn(), - setPricingSnapshotSelection: vi.fn(), -})); - -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); - -vi.mock("../src/providers/registry.js", () => - createProvidersRegistryModuleMock(mocks.getProviders), -); - -vi.mock("../src/lib/modelsdev-pricing.js", () => createPricingModuleMock(mocks)); - -async function loadSidebarPanel( - params: Parameters[0], -) { - const { loadTuiSessionQuotaSurfaces } = await import("../src/lib/tui-runtime.js"); - return (await loadTuiSessionQuotaSurfaces(params)).sidebar; -} - -function seedPricingMocks(): void { - mocks.getPricingSnapshotMeta.mockReturnValue({ - source: "runtime", - generatedAt: Date.UTC(2026, 0, 1), - units: "USD per 1M tokens", - }); - mocks.getPricingSnapshotSource.mockReturnValue("runtime"); - mocks.getRuntimePricingRefreshStatePath.mockReturnValue("/tmp/refresh-state.json"); - mocks.getRuntimePricingSnapshotPath.mockReturnValue("/tmp/pricing-runtime.json"); - mocks.maybeRefreshPricingSnapshot.mockResolvedValue({ - attempted: false, - updated: false, - state: { version: 1, updatedAt: Date.now() }, - }); -} - -function createClient(params: { - config: Record; - sessionMeta: { modelID?: string; providerID?: string }; -}) { - const client = createPluginTestClient({ - modelID: params.sessionMeta.modelID, - providerID: params.sessionMeta.providerID, - }); - - client.config.get.mockResolvedValue({ - data: { - experimental: { - quotaToast: params.config, - }, - }, - }); - client.config.providers.mockResolvedValue({ - data: { - providers: mocks.mockProviders.map((provider) => ({ id: provider.id })), - }, - }); - - return client; -} - -async function buildQuotaDialogOutputText(params: { - client: ReturnType; - sessionID: string; - roots?: { - workspaceRoot?: string; - worktreeRoot?: string; - configRoot?: string; - fallbackDirectory?: string; - activeDirectory?: string; - }; -}): Promise { - const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); - const result = await buildQuotaDialogCommandOutput({ - command: "quota", - client: params.client, - roots: params.roots ?? { - workspaceRoot: process.cwd(), - configRoot: process.cwd(), - fallbackDirectory: process.cwd(), - }, - sessionID: params.sessionID, - resolveSessionMeta: async (sessionID) => { - const response = await params.client.session.get({ path: { id: sessionID } }); - return { - modelID: response.data?.model?.id, - providerID: response.data?.model?.providerID, - }; - }, - }); - expect(params.client.session.prompt).not.toHaveBeenCalled(); - return result.state === "output" ? result.output : ""; -} - -async function resetQuotaStateForTests(): Promise { - const { __resetQuotaStateForTests } = await import("../src/lib/quota-state.js"); - __resetQuotaStateForTests(); -} - -describe("quota surface parity regressions", () => { - const originalCwd = process.cwd(); - const originalEnv = { ...process.env }; - let tempDir: string; - let worktreeDir: string; - let nestedDir: string; - - beforeEach(async () => { - vi.resetModules(); - vi.clearAllMocks(); - mocks.mockProviders.length = 0; - mocks.getProviders.mockImplementation(() => mocks.mockProviders); - seedPricingMocks(); - await resetQuotaStateForTests(); - - tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-surface-parity-")); - worktreeDir = join(tempDir, "worktree"); - nestedDir = join(worktreeDir, "packages", "feature"); - mkdirSync(nestedDir, { recursive: true }); - - process.env.HOME = tempDir; - process.env.XDG_CONFIG_HOME = join(tempDir, "xdg-config"); - process.env.XDG_DATA_HOME = join(tempDir, "xdg-data"); - process.env.XDG_CACHE_HOME = join(tempDir, "xdg-cache"); - process.env.XDG_STATE_HOME = join(tempDir, "xdg-state"); - delete process.env.OPENCODE_CONFIG_DIR; - - process.chdir(worktreeDir); - }); - - afterEach(async () => { - mocks.mockProviders.length = 0; - await resetQuotaStateForTests(); - process.chdir(originalCwd); - process.env = originalEnv; - rmSync(tempDir, { recursive: true, force: true }); - vi.clearAllMocks(); - }); - - it("uses the same effective worktree local root for plugin and sidebar in nested-directory sessions", async () => { - const syntheticProvider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "Synthetic Weekly", - group: "Synthetic", - label: "Weekly:", - percentRemaining: 64, - right: "$16/$24", - resetTimeIso: "2099-01-08T00:00:00.000Z", - }, - ], - errors: [], - }), - }; - mocks.mockProviders.push(syntheticProvider); - - // Stage 1 parity guard: both surfaces should resolve local config from worktree root, - // not nested active directory config. - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - enabledProviders: ["synthetic"], - formatStyle: "allWindows", - showOnQuestion: false, - showSessionTokens: false, - minIntervalMs: 60_000, - }, - }, - }), - "utf8", - ); - - writeFileSync( - join(nestedDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: false, - enabledProviders: [], - }, - }, - }), - "utf8", - ); - - const client = createClient({ - config: { - enabled: false, - enabledProviders: [], - }, - sessionMeta: { modelID: "synthetic/default", providerID: "synthetic" }, - }); - - const quotaOutput = await buildQuotaDialogOutputText({ - client, - sessionID: "session-worktree-root-parity", - roots: { - worktreeRoot: worktreeDir, - activeDirectory: nestedDir, - fallbackDirectory: nestedDir, - }, - }); - expect(quotaOutput).toContain("64% left"); - - await resetQuotaStateForTests(); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client, - } as any, - sessionID: "session-worktree-root-parity", - }); - - expect(panel.status).toBe("ready"); - expect(panel.lines.join("\n")).toContain("64%"); - expect(syntheticProvider.fetch).toHaveBeenCalledTimes(1); - }); - - it("resolves relative OPENCODE_CONFIG_DIR from the worktree root for plugin commands", async () => { - const syntheticProvider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "Synthetic Weekly", - group: "Synthetic", - label: "Weekly:", - percentRemaining: 64, - right: "$16/$24", - resetTimeIso: "2099-01-08T00:00:00.000Z", - }, - ], - errors: [], - }), - }; - mocks.mockProviders.push(syntheticProvider); - - mkdirSync(join(worktreeDir, ".git")); - mkdirSync(join(worktreeDir, ".opencode"), { recursive: true }); - mkdirSync(join(nestedDir, ".opencode"), { recursive: true }); - process.env.OPENCODE_CONFIG_DIR = ".opencode"; - - writeFileSync( - join(worktreeDir, ".opencode", "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: false, - }, - }, - }), - "utf8", - ); - writeFileSync( - join(nestedDir, ".opencode", "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - enabledProviders: ["synthetic"], - formatStyle: "allWindows", - showOnQuestion: false, - showSessionTokens: false, - minIntervalMs: 60_000, - }, - }, - }), - "utf8", - ); - - process.chdir(nestedDir); - - const client = createClient({ - config: { - enabled: false, - enabledProviders: [], - }, - sessionMeta: { modelID: "synthetic/default", providerID: "synthetic" }, - }); - - await expect( - buildQuotaDialogOutputText({ - client, - sessionID: "session-relative-config-root", - roots: { - workspaceRoot: worktreeDir, - configRoot: worktreeDir, - fallbackDirectory: nestedDir, - }, - }), - ).resolves.toBe(""); - expect(syntheticProvider.fetch).not.toHaveBeenCalled(); - }); - - it("keeps workspace overrides for formerly global-authoritative settings aligned between plugin and sidebar", async () => { - const syntheticProvider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "Synthetic Weekly", - group: "Synthetic", - label: "Weekly:", - percentRemaining: 17, - right: "$4/$24", - resetTimeIso: "2099-01-08T00:00:00.000Z", - }, - ], - errors: [], - }), - }; - const openaiProvider = { - id: "openai", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "OpenAI Pro", - group: "OpenAI", - label: "Pro:", - percentRemaining: 82, - right: "82/100", - resetTimeIso: "2099-01-08T00:00:00.000Z", - }, - ], - errors: [], - }), - }; - mocks.mockProviders.push(syntheticProvider, openaiProvider); - - const globalConfigDir = join(process.env.XDG_CONFIG_HOME!, "opencode"); - mkdirSync(globalConfigDir, { recursive: true }); - - writeFileSync( - join(globalConfigDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - enabledProviders: ["synthetic"], - formatStyle: "allWindows", - showOnQuestion: false, - showSessionTokens: false, - minIntervalMs: 60_000, - }, - }, - }), - "utf8", - ); - - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - enabledProviders: ["openai"], - formatStyle: "allWindows", - showOnQuestion: false, - showSessionTokens: false, - minIntervalMs: 60_000, - }, - }, - }), - "utf8", - ); - - const client = createClient({ - config: { - enabled: false, - enabledProviders: ["synthetic"], - }, - sessionMeta: { modelID: "openai/gpt-5", providerID: "openai" }, - }); - - const quotaOutput = await buildQuotaDialogOutputText({ - client, - sessionID: "session-layered-provider-override", - roots: { - worktreeRoot: worktreeDir, - activeDirectory: nestedDir, - fallbackDirectory: nestedDir, - }, - }); - expect(quotaOutput).toContain("82% left"); - expect(quotaOutput).not.toContain("17% left"); - - await resetQuotaStateForTests(); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client, - } as any, - sessionID: "session-layered-provider-override", - }); - - expect(panel.status).toBe("ready"); - const sidebarOutput = panel.lines.join("\n"); - expect(sidebarOutput).toContain("82%"); - expect(sidebarOutput).not.toContain("17%"); - expect(openaiProvider.fetch).toHaveBeenCalledTimes(1); - expect(syntheticProvider.fetch).not.toHaveBeenCalled(); - }); - - it("keeps synthetic grouped numeric parity between real /quota and real sidebar from shared snapshot storage", async () => { - const syntheticProvider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "Synthetic 5h", - group: "Synthetic", - label: "5h:", - percentRemaining: 44, - right: "44/100", - resetTimeIso: "2099-01-01T00:00:00.000Z", - }, - { - accounting: TEST_ACCOUNTING, - name: "Synthetic Weekly", - group: "Synthetic", - label: "Weekly:", - percentRemaining: 8, - right: "$22/$24", - resetTimeIso: "2099-01-08T00:00:00.000Z", - }, - ], - errors: [], - presentation: { - singleWindowShowRight: true, - }, - }), - }; - mocks.mockProviders.push(syntheticProvider); - - const sharedConfig = { - enabled: true, - enabledProviders: ["synthetic"], - formatStyle: "allWindows", - showOnQuestion: false, - showSessionTokens: false, - minIntervalMs: 60_000, - }; - - const client = createClient({ - config: sharedConfig, - sessionMeta: { modelID: "synthetic/default", providerID: "synthetic" }, - }); - - const quotaOutput = await buildQuotaDialogOutputText({ - client, - sessionID: "session-synthetic-parity", - roots: { - worktreeRoot: worktreeDir, - activeDirectory: nestedDir, - fallbackDirectory: nestedDir, - }, - }); - expect(quotaOutput).toContain("44% left"); - expect(quotaOutput).toContain("8% left"); - - // Force sidebar path to reuse persisted shared snapshot storage (not in-memory). - await resetQuotaStateForTests(); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client, - } as any, - sessionID: "session-synthetic-parity", - }); - - expect(panel.status).toBe("ready"); - expect(panel.linesExpanded).toBeDefined(); - const sidebarOutput = panel.linesExpanded!.join("\n"); - expect(sidebarOutput).toContain("44% left"); - expect(sidebarOutput).toContain("8% left"); - expect(syntheticProvider.fetch).toHaveBeenCalledTimes(1); - }); - - it("keeps intentional single-window-vs-all-windows non-parity while still sharing the same underlying snapshot", async () => { - const openaiProvider = { - id: "openai", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "OpenAI Pro 5h", - group: "OpenAI (Pro)", - label: "5h:", - percentRemaining: 95, - resetTimeIso: "2099-01-01T00:00:00.000Z", - }, - { - accounting: TEST_ACCOUNTING, - name: "OpenAI Pro Weekly", - group: "OpenAI (Pro)", - label: "Weekly:", - percentRemaining: 40, - resetTimeIso: "2099-01-08T00:00:00.000Z", - }, - ], - errors: [], - presentation: { - singleWindowDisplayName: "OpenAI Pro", - }, - }), - }; - mocks.mockProviders.push(openaiProvider); - - const config = { - enabled: true, - enabledProviders: ["openai"], - formatStyle: "singleWindow", - showOnQuestion: false, - showSessionTokens: false, - minIntervalMs: 60_000, - }; - - const client = createClient({ - config, - sessionMeta: { modelID: "openai/gpt-5", providerID: "openai" }, - }); - - const quotaOutput = await buildQuotaDialogOutputText({ - client, - sessionID: "session-openai-style-divergence", - roots: { - worktreeRoot: worktreeDir, - activeDirectory: nestedDir, - fallbackDirectory: nestedDir, - }, - }); - expect(quotaOutput).toContain("95% left"); - expect(quotaOutput).toContain("40% left"); - - // Ensure sidebar reads from shared persisted snapshot, then projects as single-window. - await resetQuotaStateForTests(); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client, - } as any, - sessionID: "session-openai-style-divergence", - }); - - expect(panel.status).toBe("ready"); - expect(panel.linesExpanded).toBeDefined(); - const sidebarOutput = panel.lines.join("\n"); - const expandedSidebarOutput = panel.linesExpanded!.join("\n"); - expect(sidebarOutput).toContain("40%"); - expect(sidebarOutput).not.toContain("95%"); - expect(expandedSidebarOutput).toContain("95% left"); - expect(expandedSidebarOutput).toContain("40% left"); - expect(openaiProvider.fetch).toHaveBeenCalledTimes(1); - }); -}); diff --git a/tests/test-stabilization-connected.test.ts b/tests/test-stabilization-connected.test.ts deleted file mode 100644 index 27cb6857..00000000 --- a/tests/test-stabilization-connected.test.ts +++ /dev/null @@ -1,1546 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { EventEmitter } from "node:events"; -import { - chmod, - copyFile, - lstat, - mkdir, - mkdtemp, - readdir, - readFile, - realpath, - rm, - symlink, - writeFile, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { - appendBoundedDiagnostic, - applyQuotaSurfaceSettings, - assertNoModelRequest, - buildConnectedChildEnv, - CAPTURED_PROCESS_STREAM_MAX_BYTES, - CONNECTED_CREDENTIAL_WARNING, - CONNECTED_TEMP_PREFIX, - CONNECTED_WEB_COMMANDS, - chooseConnectedWebModel, - classifyWebDiagnostic, - cleanupConnectedWorkspace, - connectedLaunchArgs, - createWebDiagnosticsFile, - DIAGNOSTIC_REDACTED_BODY_MARKER, - DIAGNOSTIC_REDACTED_CONFIG_MARKER, - DIAGNOSTIC_REDACTED_HEADER_MARKER, - DIAGNOSTIC_REDACTED_PRIVATE_MARKER, - DIAGNOSTIC_TRUNCATED_MARKER, - findOpenCodeExecutable, - formatConnectedWebModelGuidance, - formatWebDiagnostics, - formatWebDiagnosticsPath, - formatWebModelChoice, - getConnectedUsage, - OPENCODE_MODELS_ARGS, - parseConnectedArgs, - parseOpenCodeModelsOutput, - pluginFileUrls, - prepareConnectedWebModel, - prepareConnectedWorkspace, - readNewLogRecords, - redactConnectedDiagnostics, - resolveOpenCodeLogDirs, - rewritePluginList, - runCapturedProcess, - runConnected, - runProcess, - sliceLogFromOffset, - snapshotLogOffsets, - WEB_DIAGNOSTIC_LOG_MAX_BYTES, - WEB_DIAGNOSTIC_SOURCE_MAX_BYTES, - WEB_DIAGNOSTIC_TOTAL_MAX_BYTES, - WEB_FAILURE_HOOK_ENTRY_BUILD_INJECTION, - WEB_FAILURE_INVALID_MODEL_BEFORE_HOOK, - WEB_FAILURE_POST_INJECTION_BUSY_272, - WEB_FAILURE_UNKNOWN, - writeWebDiagnosticsFile, -} from "../scripts/test-stabilization-connected.mjs"; - -const scriptPath = fileURLToPath( - new URL("../scripts/test-stabilization-connected.mjs", import.meta.url), -); -const repoRoot = fileURLToPath(new URL("..", import.meta.url)); -const SECRET = "connected-config-secret-canary"; - -let tempDir: string | undefined; - -async function makeTemp(prefix: string): Promise { - const dir = await mkdtemp(path.join(tmpdir(), prefix)); - tempDir = dir; - return dir; -} - -afterEach(async () => { - if (!tempDir) return; - await rm(tempDir, { recursive: true, force: true }); - tempDir = undefined; -}); - -async function writeConfigTree(root: string) { - const realMain = path.join(root, "real-opencode.jsonc"); - await writeFile( - realMain, - `{ - // companion plugins stay first - "$schema": "https://opencode.ai/config.json", - "model": "test-model", - "plugin": [ - "opencode-gemini-auth", - ["@slkiser/opencode-quota", { "enabled": true }], - "opencode-agy-auth" - ] -} -`, - "utf8", - ); - await symlink(realMain, path.join(root, "opencode.jsonc")); - await writeFile( - path.join(root, "tui.jsonc"), - `{ - "plugin": ["@slkiser/opencode-quota"] -} -`, - "utf8", - ); - await mkdir(path.join(root, "opencode-quota"), { recursive: true }); - await writeFile( - path.join(root, "opencode-quota", "quota-toast.jsonc"), - `{ - "enabledProviders": "auto", - "tuiSidebarPanel": { - "opencodeGoPreferredWindow": "rolling" - }, - "enableToast": false, - "tuiCompactStatus": { "enabled": false }, - "tuiPromptBar": { "enabled": true } -} -`, - "utf8", - ); - await writeFile(path.join(root, "auth.json"), `${JSON.stringify({ token: SECRET })}\n`, "utf8"); -} - -async function writeJsonOnlyConfigTree(root: string) { - await writeFile( - path.join(root, "opencode.json"), - `${JSON.stringify({ - $schema: "https://opencode.ai/config.json", - model: "json-only-model", - plugin: ["@slkiser/opencode-quota"], - })}\n`, - "utf8", - ); - await writeFile( - path.join(root, "tui.json"), - `${JSON.stringify({ plugin: ["@slkiser/opencode-quota"] })}\n`, - "utf8", - ); - await mkdir(path.join(root, "opencode-quota"), { recursive: true }); - await writeFile( - path.join(root, "opencode-quota", "quota-toast.json"), - `${JSON.stringify({ enableToast: false })}\n`, - "utf8", - ); -} - -function runScript(args: string[], env: NodeJS.ProcessEnv) { - return spawnSync(process.execPath, [scriptPath, ...args], { - cwd: repoRoot, - encoding: "utf8", - env, - }); -} - -async function prefixDirs(dir: string): Promise { - return (await readdir(dir)).filter((name) => name.startsWith(CONNECTED_TEMP_PREFIX)); -} - -async function writeFakeWebHarness(root: string) { - const source = path.join(root, "source"); - const bin = path.join(root, "bin"); - const fakeRepo = path.join(root, "repo"); - const recordFile = path.join(root, "opencode-records.jsonl"); - const buildRecordFile = path.join(root, "build-record.json"); - const startedFile = path.join(root, "opencode-started.txt"); - await mkdir(source, { recursive: true }); - await mkdir(bin, { recursive: true }); - await mkdir(path.join(fakeRepo, "dist"), { recursive: true }); - await writeFile( - path.join(source, "opencode.jsonc"), - `{ - // keep this comment and plugin order - "model": "stale/provider-model", - "plugin": ["companion-before", "@slkiser/opencode-quota", "companion-after"] -} -`, - "utf8", - ); - await writeFile(path.join(fakeRepo, "dist", "index.js"), "export {};\n", "utf8"); - const fakePnpm = path.join(fakeRepo, "fake-pnpm.cjs"); - await writeFile( - fakePnpm, - `require("node:fs").writeFileSync(process.env.FAKE_BUILD_RECORD_FILE, JSON.stringify(process.argv.slice(2)));\n`, - "utf8", - ); - // Use a native executable rather than a shebang or .cmd shim, which spawn cannot - // launch without a shell on Windows. The preload runs only for this copied Node. - const fakeOpenCode = path.join(bin, process.platform === "win32" ? "opencode.exe" : "opencode"); - await copyFile(process.execPath, fakeOpenCode); - const fakeOpenCodePreload = path.join(root, "fake-opencode.mjs"); - await writeFile( - fakeOpenCodePreload, - `import { appendFileSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; -import path from "node:path"; -if (realpathSync(process.execPath) === realpathSync(${JSON.stringify(fakeOpenCode)})) { -// Node resolves its first argument as a script path before running --import. -const args = [path.relative(process.cwd(), process.argv[1]), ...process.argv.slice(2)]; -appendFileSync(process.env.FAKE_RECORD_FILE, JSON.stringify({ - args, - configDir: process.env.OPENCODE_CONFIG_DIR ?? null, - configFile: process.env.OPENCODE_CONFIG ?? null, - cwd: process.cwd(), - configContents: readFileSync(process.env.OPENCODE_CONFIG_DIR + "/opencode.jsonc", "utf8"), -}) + "\\n"); -if (args[0] === "models") { - if (process.env.FAKE_MODELS_HANG === "1") { - writeFileSync(process.env.FAKE_STARTED_FILE, "models\\n"); - process.on("SIGTERM", () => {}); - process.on("SIGINT", () => {}); - process.on("SIGHUP", () => {}); - setInterval(() => {}, 1000); - await new Promise(() => {}); - } else { - process.stdout.write(process.env.FAKE_MODELS_OUTPUT ?? ""); - process.stderr.write(process.env.FAKE_MODELS_STDERR ?? ""); - process.exit(Number(process.env.FAKE_MODELS_EXIT ?? "0")); - } -} else if (args[0] === "web") { - writeFileSync(process.env.FAKE_STARTED_FILE, "web\\n"); - process.stdout.write(process.env.FAKE_WEB_STDOUT ?? ""); - process.stderr.write(process.env.FAKE_WEB_STDERR ?? ""); - if (process.env.FAKE_WEB_HANG === "1") { - process.on("SIGTERM", () => {}); - process.on("SIGINT", () => {}); - process.on("SIGHUP", () => {}); - setInterval(() => {}, 1000); - await new Promise(() => {}); - } else { - process.exit(Number(process.env.FAKE_WEB_EXIT ?? "0")); - } -} else { - process.exit(64); -} -} -`, - "utf8", - ); - if (process.platform !== "win32") { - await chmod(fakeOpenCode, 0o755); - } - return { - source, - bin, - fakeRepo, - fakeOpenCode, - recordFile, - buildRecordFile, - startedFile, - env: { - ...process.env, - // Always run the fake build, even when pnpm test supplies npm_execpath. - npm_execpath: fakePnpm, - NODE_OPTIONS: `--import ${JSON.stringify(pathToFileURL(fakeOpenCodePreload).href)}`, - FAKE_BUILD_RECORD_FILE: buildRecordFile, - OPENCODE_CONFIG_DIR: source, - OPENCODE_CONFIG: path.join(source, "must-not-reach-child.json"), - PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}`, - FAKE_RECORD_FILE: recordFile, - FAKE_STARTED_FILE: startedFile, - }, - }; -} - -async function readFakeRecords(filePath: string) { - const content = await readFile(filePath, "utf8"); - return content - .trim() - .split("\n") - .filter(Boolean) - .map((line) => JSON.parse(line) as Record); -} - -async function waitForFile(filePath: string, expected: string) { - for (let index = 0; index < 200; index += 1) { - try { - if ((await readFile(filePath, "utf8")) === expected) return; - } catch { - // The fake executable has not reached the marker yet. - } - await new Promise((resolve) => setTimeout(resolve, 10)); - } - throw new Error(`Timed out waiting for ${filePath}`); -} - -function captureOutput(chunks: string[]) { - return { - write(chunk: string) { - chunks.push(String(chunk)); - return true; - }, - }; -} - -describe("test-stabilization-connected", () => { - it("parses TUI, web, prompt-bar, dry-run, and help flags", () => { - expect(parseConnectedArgs(["--help"])).toEqual({ - mode: null, - promptBar: false, - dryRun: false, - help: true, - }); - expect(parseConnectedArgs(["--tui", "--prompt-bar", "--dry-run"])).toEqual({ - mode: "tui", - promptBar: true, - dryRun: true, - help: false, - }); - expect(parseConnectedArgs(["--web"])).toMatchObject({ mode: "web", promptBar: false }); - expect(connectedLaunchArgs("web")).toEqual(["web"]); - expect(connectedLaunchArgs("tui")).toEqual([]); - expect(() => parseConnectedArgs([])).toThrow(/Specify --tui or --web/); - expect(() => parseConnectedArgs(["--tui", "--web"])).toThrow(/only one/); - expect(() => parseConnectedArgs(["--web", "--prompt-bar"])).toThrow(/only valid with --tui/); - expect(getConnectedUsage()).toContain("--tui"); - expect(getConnectedUsage()).toMatch(/fixed 12-cell bar/i); - expect(getConnectedUsage()).toMatch(/placement\/clipping/i); - }); - - it("rewrites only quota plugin entries and preserves companion order", () => { - const url = "file:///tmp/dist/index.js"; - expect( - rewritePluginList( - ["opencode-gemini-auth", "@slkiser/opencode-quota", "opencode-agy-auth"], - url, - ), - ).toEqual(["opencode-gemini-auth", url, "opencode-agy-auth"]); - expect(rewritePluginList([["@slkiser/opencode-quota", { enabled: true }]], url)).toEqual([ - [url, { enabled: true }], - ]); - expect(rewritePluginList(["opencode-agy-auth"], url)).toEqual(["opencode-agy-auth", url]); - }); - - it("enables sidebar, toast, and compact without dropping unrelated quota settings", () => { - const config = applyQuotaSurfaceSettings( - { - enabledProviders: "auto", - tuiSidebarPanel: { opencodeGoPreferredWindow: "rolling" }, - enableToast: false, - }, - { promptBarEnabled: false }, - ); - expect(config).toMatchObject({ - enabledProviders: "auto", - enableToast: true, - tuiSidebarPanel: { enabled: true, opencodeGoPreferredWindow: "rolling" }, - tuiCompactStatus: { enabled: true }, - tuiPromptBar: { enabled: false }, - }); - }); - - it("strips OPENCODE_CONFIG from the child environment", () => { - expect( - buildConnectedChildEnv( - { - OPENCODE_CONFIG: "/real/opencode.json", - OPENCODE_CONFIG_DIR: "/real/config", - PATH: "/bin", - }, - "/tmp/isolated-config", - ), - ).toEqual({ - OPENCODE_CONFIG_DIR: "/tmp/isolated-config", - PATH: "/bin", - }); - }); - - it("copies through a symlink, rewrites the temp files, and leaves the real config unchanged", async () => { - const root = await makeTemp("oq-connected-src-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeConfigTree(source); - const originalMain = await readFile(path.join(source, "real-opencode.jsonc"), "utf8"); - const originalAuth = await readFile(path.join(source, "auth.json"), "utf8"); - - const workspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - promptBarEnabled: false, - tmpdir: root, - }); - tempDir = root; - - const copiedMainStat = await lstat(workspace.mainPath); - expect(copiedMainStat.isSymbolicLink()).toBe(false); - expect(await realpath(workspace.mainPath)).not.toBe( - await realpath(path.join(source, "real-opencode.jsonc")), - ); - - const copiedMain = await readFile(workspace.mainPath, "utf8"); - const urls = pluginFileUrls(repoRoot); - expect(copiedMain).toContain("// companion plugins stay first"); - expect(copiedMain).toContain("opencode-gemini-auth"); - expect(copiedMain).toContain("opencode-agy-auth"); - expect(copiedMain).toContain(urls.server); - expect(copiedMain).not.toContain("@slkiser/opencode-quota"); - expect(copiedMain).toContain('"model": "test-model"'); - - const copiedTui = await readFile(workspace.tuiPath, "utf8"); - expect(copiedTui).toContain(urls.tui); - - const copiedQuota = JSON.parse(await readFile(workspace.quotaPath, "utf8")) as Record< - string, - unknown - >; - expect(copiedQuota).toMatchObject({ - enabledProviders: "auto", - enableToast: true, - tuiSidebarPanel: { enabled: true, opencodeGoPreferredWindow: "rolling" }, - tuiCompactStatus: { enabled: true }, - tuiPromptBar: { enabled: false }, - }); - - expect(await readFile(path.join(source, "real-opencode.jsonc"), "utf8")).toBe(originalMain); - expect(await readFile(path.join(source, "auth.json"), "utf8")).toBe(originalAuth); - expect(await readFile(path.join(workspace.configDir, "auth.json"), "utf8")).toContain(SECRET); - - if (process.platform !== "win32") { - expect((await lstat(workspace.tempRoot)).mode & 0o777).toBe(0o700); - expect((await lstat(workspace.configDir)).mode & 0o777).toBe(0o700); - expect((await lstat(workspace.projectDir)).mode & 0o777).toBe(0o700); - expect((await lstat(path.join(workspace.configDir, "auth.json"))).mode & 0o777).toBe(0o600); - expect((await lstat(workspace.mainPath)).mode & 0o777).toBe(0o600); - } - - const tempRoot = workspace.tempRoot; - await cleanupConnectedWorkspace(tempRoot, { tmpdir: root }); - await expect(lstat(tempRoot)).rejects.toThrow(); - expect(await readFile(path.join(source, "real-opencode.jsonc"), "utf8")).toBe(originalMain); - }); - - it("follows a symlinked config root with stat and still requires a directory", async () => { - const root = await makeTemp("oq-connected-link-"); - const realSource = path.join(root, "real-source"); - const linkedSource = path.join(root, "linked-source"); - await mkdir(realSource); - await writeConfigTree(realSource); - await symlink(realSource, linkedSource); - - const workspace = await prepareConnectedWorkspace({ - sourceConfigDir: linkedSource, - repoRoot, - promptBarEnabled: false, - tmpdir: root, - }); - tempDir = root; - - expect(path.basename(workspace.mainPath)).toBe("opencode.jsonc"); - expect((await lstat(workspace.mainPath)).isSymbolicLink()).toBe(false); - expect(await readFile(workspace.mainPath, "utf8")).toContain("test-model"); - await expect( - prepareConnectedWorkspace({ - sourceConfigDir: path.join(root, "missing-link"), - repoRoot, - tmpdir: root, - }), - ).rejects.toThrow(/config directory is missing/); - const fileSource = path.join(root, "file-source"); - await writeFile(fileSource, "{}\n", "utf8"); - await expect( - prepareConnectedWorkspace({ - sourceConfigDir: fileSource, - repoRoot, - tmpdir: root, - }), - ).rejects.toThrow(/config directory is missing/); - }); - - it("rewrites JSON-only configs without requiring jsonc files", async () => { - const root = await makeTemp("oq-connected-json-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeJsonOnlyConfigTree(source); - - const workspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - promptBarEnabled: false, - tmpdir: root, - }); - tempDir = root; - - expect(workspace.mainPath).toBe(path.join(workspace.configDir, "opencode.json")); - expect(workspace.tuiPath).toBe(path.join(workspace.configDir, "tui.json")); - expect(workspace.quotaPath).toBe( - path.join(workspace.configDir, "opencode-quota", "quota-toast.json"), - ); - const urls = pluginFileUrls(repoRoot); - expect(await readFile(workspace.mainPath, "utf8")).toContain(urls.server); - expect(await readFile(workspace.tuiPath, "utf8")).toContain(urls.tui); - }); - - it("rejects a config directory that is missing the main OpenCode config", async () => { - const root = await makeTemp("oq-connected-missing-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeFile( - path.join(source, "auth.json"), - `${JSON.stringify({ token: SECRET })}\n`, - "utf8", - ); - - await expect( - prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - tmpdir: root, - }), - ).rejects.toThrow(/missing opencode\.json or opencode\.jsonc/); - expect(await prefixDirs(root)).toEqual([]); - }); - - it("creates a TUI config for TUI runs and leaves it absent for web when missing", async () => { - const root = await makeTemp("oq-connected-tui-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeFile( - path.join(source, "opencode.jsonc"), - `${JSON.stringify({ plugin: ["@slkiser/opencode-quota"] })}\n`, - "utf8", - ); - - const tuiWorkspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - ensureTuiConfig: true, - tmpdir: root, - }); - expect(tuiWorkspace.tuiPath).toBe(path.join(tuiWorkspace.configDir, "tui.jsonc")); - expect(await readFile(tuiWorkspace.tuiPath, "utf8")).toContain(pluginFileUrls(repoRoot).tui); - await cleanupConnectedWorkspace(tuiWorkspace.tempRoot, { tmpdir: root }); - - const webWorkspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - ensureTuiConfig: false, - tmpdir: root, - }); - tempDir = root; - expect(webWorkspace.tuiPath).toBeNull(); - await expect(lstat(path.join(webWorkspace.configDir, "tui.jsonc"))).rejects.toThrow(); - await expect(lstat(path.join(webWorkspace.configDir, "tui.json"))).rejects.toThrow(); - }); - - it("deletes the temp workspace if copy or transform fails", async () => { - const root = await makeTemp("oq-connected-fail-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeFile(path.join(source, "opencode.jsonc"), "not-json {\n", "utf8"); - - await expect( - prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - tmpdir: root, - }), - ).rejects.toThrow(/Failed to parse copied opencode\.jsonc/); - expect(await prefixDirs(root)).toEqual([]); - }); - - it("refuses to delete a path outside the expected temp prefix", async () => { - const root = await makeTemp("oq-connected-guard-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeFile(path.join(source, "keep.txt"), "keep\n", "utf8"); - const tmp = path.join(root, "tmp"); - await mkdir(tmp); - - await expect(cleanupConnectedWorkspace(source, { tmpdir: tmp })).rejects.toThrow( - /Refusing to delete/, - ); - expect(await readFile(path.join(source, "keep.txt"), "utf8")).toBe("keep\n"); - - const decoy = path.join(tmp, `${CONNECTED_TEMP_PREFIX}decoy`); - await symlink(source, decoy); - await expect(cleanupConnectedWorkspace(decoy, { tmpdir: tmp })).rejects.toThrow( - /Refusing to delete/, - ); - expect(await readFile(path.join(source, "keep.txt"), "utf8")).toBe("keep\n"); - }); - - it("forwards SIGTERM to the child, waits for exit, then cleanup can run", async () => { - if (process.platform === "win32") return; - const root = await makeTemp("oq-connected-sig-"); - const childScript = path.join(root, "child.mjs"); - const marker = path.join(root, "marker.txt"); - await writeFile( - childScript, - `import { writeFileSync } from "node:fs"; -writeFileSync(process.argv[2], "started\\n"); -process.on("SIGTERM", () => { - writeFileSync(process.argv[2], "exited\\n"); - process.exit(0); -}); -setInterval(() => {}, 1000); -`, - "utf8", - ); - - const signalSource = new EventEmitter(); - const events: string[] = []; - const running = runProcess(process.execPath, [childScript, marker], { - cwd: root, - stdio: "ignore", - signalSource, - }).then((result: { code: number | null; signal: NodeJS.Signals | null }) => { - events.push("child-exit"); - return result; - }); - - for (let i = 0; i < 100; i++) { - try { - if ((await readFile(marker, "utf8")) === "started\n") break; - } catch { - // The child has not created the marker yet. - } - await new Promise((resolve) => setTimeout(resolve, 20)); - } - expect(await readFile(marker, "utf8")).toBe("started\n"); - signalSource.emit("SIGTERM"); - const result = await running; - events.push("cleanup"); - expect(await readFile(marker, "utf8")).toBe("exited\n"); - expect(result.code).toBe(0); - expect(events).toEqual(["child-exit", "cleanup"]); - }); - - it("escalates an ignored SIGTERM to SIGKILL after the grace period, then cleanup can run", async () => { - if (process.platform === "win32") return; - const root = await makeTemp("oq-connected-kill-"); - const childScript = path.join(root, "ignore.mjs"); - const marker = path.join(root, "marker.txt"); - const pidsFile = path.join(root, "pids.json"); - const grandchildSource = - "process.on('SIGTERM', () => {}); process.on('SIGINT', () => {}); process.on('SIGHUP', () => {}); setInterval(() => {}, 1000);"; - await writeFile( - childScript, - `import { writeFileSync } from "node:fs"; -import { spawn } from "node:child_process"; -process.on("SIGTERM", () => { - writeFileSync(process.argv[2], "ignored\\n"); -}); -process.on("SIGINT", () => {}); -process.on("SIGHUP", () => {}); -const grandchild = spawn(process.execPath, ["-e", ${JSON.stringify(grandchildSource)}], { - detached: false, - stdio: "ignore", -}); -writeFileSync(process.argv[3], JSON.stringify({ parent: process.pid, grandchild: grandchild.pid })); -writeFileSync(process.argv[2], "started\\n"); -setInterval(() => {}, 1000); -`, - "utf8", - ); - - const signalSource = new EventEmitter(); - const events: string[] = []; - const graceMs = 80; - const running = runProcess(process.execPath, [childScript, marker, pidsFile], { - cwd: root, - stdio: "ignore", - signalSource, - signalGraceMs: graceMs, - }).then((result: { code: number | null; signal: NodeJS.Signals | null }) => { - events.push("child-exit"); - return result; - }); - - for (let i = 0; i < 100; i++) { - try { - if ((await readFile(marker, "utf8")) === "started\n") break; - } catch { - // The child has not created the marker yet. - } - await new Promise((resolve) => setTimeout(resolve, 20)); - } - expect(await readFile(marker, "utf8")).toBe("started\n"); - const pids = JSON.parse(await readFile(pidsFile, "utf8")) as { - parent: number; - grandchild: number; - }; - const signaledAt = Date.now(); - signalSource.emit("SIGTERM"); - const result = await running; - const elapsed = Date.now() - signaledAt; - events.push("cleanup"); - expect(await readFile(marker, "utf8")).toBe("ignored\n"); - expect(result.signal).toBe("SIGKILL"); - expect(result.code).toBeNull(); - expect(elapsed).toBeGreaterThanOrEqual(graceMs); - expect(elapsed).toBeLessThan(graceMs + 2000); - expect(events).toEqual(["child-exit", "cleanup"]); - expect(() => process.kill(pids.parent, 0)).toThrow(); - expect(() => process.kill(pids.grandchild, 0)).toThrow(); - }); - - it("prints help without requiring config or OpenCode", async () => { - const logs: string[] = []; - const code = await runConnected(["--help"], { - stdout: { - write(chunk: string) { - logs.push(chunk); - return true; - }, - }, - stderr: { - write() { - return true; - }, - }, - env: { PATH: "" }, - }); - expect(code).toBe(0); - expect(logs.join("")).toContain("--tui"); - expect(logs.join("")).not.toContain(SECRET); - }); - - it("fails dry-run clearly when config or OpenCode is missing", () => { - const missingConfig = runScript(["--dry-run", "--tui"], { - ...process.env, - OPENCODE_CONFIG_DIR: path.join(tmpdir(), "missing-opencode-config-dir"), - PATH: "", - }); - expect(missingConfig.status).toBe(1); - expect(`${missingConfig.stdout}${missingConfig.stderr}`).toMatch( - /config directory is missing/i, - ); - expect(`${missingConfig.stdout}${missingConfig.stderr}`).not.toContain(SECRET); - }); - - it("dry-run warns about real credentials and does not print copied secrets", async () => { - const root = await makeTemp("oq-connected-dry-"); - const source = path.join(root, "source"); - const bin = path.join(root, "bin"); - await mkdir(source); - await mkdir(bin); - await writeConfigTree(source); - const opencodePath = path.join(bin, process.platform === "win32" ? "opencode.cmd" : "opencode"); - await writeFile(opencodePath, process.platform === "win32" ? "@echo off\n" : "#!/bin/sh\n"); - if (process.platform !== "win32") await chmod(opencodePath, 0o755); - - expect(findOpenCodeExecutable({ PATH: bin })).toBe(opencodePath); - - const result = runScript(["--dry-run", "--tui"], { - ...process.env, - OPENCODE_CONFIG_DIR: source, - PATH: bin, - }); - const output = `${result.stdout}${result.stderr}`; - expect(result.status).toBe(0); - expect(output).toContain(CONNECTED_CREDENTIAL_WARNING); - expect(output).toContain("Dry run"); - expect(output).not.toContain(SECRET); - expect(output).not.toContain("test-model"); - expect(output).not.toContain("companion plugins stay first"); - - const web = runScript(["--dry-run", "--web"], { - ...process.env, - OPENCODE_CONFIG_DIR: source, - PATH: bin, - }); - const webOutput = `${web.stdout}${web.stderr}`; - expect(web.status).toBe(0); - expect(webOutput).toContain("Mode: web"); - expect(webOutput).not.toContain(SECRET); - }); - - it("keeps a valid temp model and never sends a model request", async () => { - const root = await makeTemp("oq-connected-valid-model-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeFile( - path.join(source, "opencode.jsonc"), - `{ - "model": "github-copilot/gpt-4.1", - "plugin": ["opencode-gemini-auth", "@slkiser/opencode-quota", "opencode-agy-auth"] -} -`, - "utf8", - ); - const originalMain = await readFile(path.join(source, "opencode.jsonc"), "utf8"); - const workspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - promptBarEnabled: false, - ensureTuiConfig: false, - tmpdir: root, - }); - tempDir = root; - const spawned: string[][] = []; - const result = await prepareConnectedWebModel({ - opencodeBin: "opencode", - configDir: workspace.configDir, - projectDir: workspace.projectDir, - env: { PATH: "/bin" }, - mainPath: workspace.mainPath, - async runCaptured(_command: string, args: string[]) { - spawned.push(args); - return { - code: 0, - signal: null, - stdout: "openai/gpt-5\ngithub-copilot/gpt-4.1\n", - stderr: "", - }; - }, - }); - expect(spawned).toEqual([OPENCODE_MODELS_ARGS]); - expect(result).toMatchObject({ - ok: true, - reason: "valid-model", - originalModel: "github-copilot/gpt-4.1", - temporaryModel: "github-copilot/gpt-4.1", - replaced: false, - }); - expect(result.spawnedArgs).toEqual(["models"]); - expect(result.spawnedArgs).not.toContain("run"); - expect(await readFile(path.join(source, "opencode.jsonc"), "utf8")).toBe(originalMain); - expect(await readFile(workspace.mainPath, "utf8")).toContain( - '"model": "github-copilot/gpt-4.1"', - ); - expect(formatWebModelChoice(result)).toContain("Copied default model: github-copilot/gpt-4.1"); - expect(formatWebModelChoice(result)).toContain("Temporary Web model: github-copilot/gpt-4.1"); - }); - - it("rewrites only the temp copy when the default model is stale", async () => { - const root = await makeTemp("oq-connected-stale-model-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeFile( - path.join(source, "opencode.jsonc"), - `{ - "model": "github-copilot/claude-sonnet-4.6", - "plugin": ["opencode-gemini-auth", "@slkiser/opencode-quota", "opencode-agy-auth"] -} -`, - "utf8", - ); - const originalMain = await readFile(path.join(source, "opencode.jsonc"), "utf8"); - const workspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - promptBarEnabled: false, - ensureTuiConfig: false, - tmpdir: root, - }); - tempDir = root; - const result = await prepareConnectedWebModel({ - opencodeBin: "opencode", - configDir: workspace.configDir, - projectDir: workspace.projectDir, - env: { PATH: "/bin" }, - mainPath: workspace.mainPath, - async runCaptured(_command: string, args: string[]) { - expect(args).toEqual(["models"]); - expect(args).not.toContain("run"); - return { - code: 0, - signal: null, - stdout: "openai/gpt-5\ngithub-copilot/gpt-4o\ngithub-copilot/gpt-4.1\n", - stderr: "", - }; - }, - }); - expect(result).toMatchObject({ - ok: true, - reason: "stale-model", - originalModel: "github-copilot/claude-sonnet-4.6", - temporaryModel: "github-copilot/gpt-4.1", - replaced: true, - }); - expect( - chooseConnectedWebModel("github-copilot/claude-sonnet-4.6", [ - "openai/gpt-5", - "github-copilot/gpt-4o", - "github-copilot/gpt-4.1", - ]).model, - ).toBe("github-copilot/gpt-4.1"); - expect(await readFile(path.join(source, "opencode.jsonc"), "utf8")).toBe(originalMain); - const copied = await readFile(workspace.mainPath, "utf8"); - expect(copied).toContain('"model": "github-copilot/gpt-4.1"'); - expect(copied).not.toContain("claude-sonnet-4.6"); - expect(copied).toContain("opencode-gemini-auth"); - expect(copied).toContain("opencode-agy-auth"); - expect(formatWebModelChoice(result)).toContain( - "Copied default model: github-copilot/claude-sonnet-4.6", - ); - expect(formatWebModelChoice(result)).toContain("Temporary Web model: github-copilot/gpt-4.1"); - }); - - it("fails before launch when the catalog is empty and leaves the real config unchanged", async () => { - const root = await makeTemp("oq-connected-empty-catalog-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeConfigTree(source); - const originalMain = await readFile(path.join(source, "real-opencode.jsonc"), "utf8"); - const workspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - promptBarEnabled: false, - ensureTuiConfig: false, - tmpdir: root, - }); - tempDir = root; - const result = await prepareConnectedWebModel({ - opencodeBin: "opencode", - configDir: workspace.configDir, - projectDir: workspace.projectDir, - env: { PATH: "/bin" }, - mainPath: workspace.mainPath, - async runCaptured() { - return { code: 0, signal: null, stdout: "No models available\n", stderr: "" }; - }, - }); - expect(result.ok).toBe(false); - expect(result.reason).toBe("empty-catalog"); - expect(result.replaced).toBe(false); - expect(result.guidance).toContain("/quota"); - expect(result.guidance).toContain("/quota_status"); - expect(result.guidance).toContain("Click the model name"); - expect(result.guidance).toContain("The real OpenCode config was not edited."); - expect(await readFile(path.join(source, "real-opencode.jsonc"), "utf8")).toBe(originalMain); - expect(await readFile(workspace.mainPath, "utf8")).toContain('"model": "test-model"'); - }); - - it("fails before launch when opencode models fails and does not send a model request", async () => { - const root = await makeTemp("oq-connected-models-fail-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeConfigTree(source); - const originalMain = await readFile(path.join(source, "real-opencode.jsonc"), "utf8"); - const workspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - promptBarEnabled: false, - ensureTuiConfig: false, - tmpdir: root, - }); - tempDir = root; - const result = await prepareConnectedWebModel({ - opencodeBin: "opencode", - configDir: workspace.configDir, - projectDir: workspace.projectDir, - env: { PATH: "/bin" }, - mainPath: workspace.mainPath, - async runCaptured(_command: string, args: string[]) { - expect(args).toEqual(["models"]); - expect(args).not.toContain("run"); - return { - code: 1, - signal: null, - stdout: "", - stderr: "models failed token=super-secret-token", - }; - }, - }); - expect(result.ok).toBe(false); - expect(result.reason).toBe("models-command-failed"); - expect(result.spawnedArgs).toEqual(OPENCODE_MODELS_ARGS); - expect(result.guidance).toContain("/quota"); - expect(result.guidance).toContain("/quota_status"); - expect(result.guidance).not.toContain("super-secret-token"); - expect(result.guidance).toContain("[REDACTED]"); - expect(await readFile(path.join(source, "real-opencode.jsonc"), "utf8")).toBe(originalMain); - expect(() => assertNoModelRequest(["run", "prompt"])).toThrow(/model request/); - expect(assertNoModelRequest(["models"])).toEqual(["models"]); - }); - - it("bounds and conservatively redacts diagnostic sources", async () => { - const redacted = redactConnectedDiagnostics( - [ - "Authorization: Bearer abc.def", - "Cookie: session=browser-secret", - "api_key=sk-live-123", - "token=super-secret-token", - '{"access_token":"xyz","authorization":"Bearer abc"}', - 'request body: {"prompt":"private request"}', - 'response body: {"choices":[{"text":"private reply"}]}', - 'serialized config: {"plugin":["private-plugin"],"token":"hidden"}', - "request body:", - "{", - ' "prompt": "multiline-request"', - "}", - '"config": {', - ' "private": "multiline-config"', - "}", - '"headers": {', - ' "X-Custom": "multiline-header"', - "}", - "private content=private-message", - "-----BEGIN PRIVATE KEY-----\nprivate-key\n-----END PRIVATE KEY-----", - ].join("\n"), - ); - expect(redacted).toContain("[REDACTED]"); - expect(redacted).toContain(DIAGNOSTIC_REDACTED_HEADER_MARKER); - expect(redacted).toContain(DIAGNOSTIC_REDACTED_BODY_MARKER); - expect(redacted).toContain(DIAGNOSTIC_REDACTED_CONFIG_MARKER); - expect(redacted).toContain(DIAGNOSTIC_REDACTED_PRIVATE_MARKER); - for (const secret of [ - "abc.def", - "browser-secret", - "sk-live-123", - "super-secret-token", - "private request", - "private reply", - "private-plugin", - "multiline-request", - "multiline-config", - "multiline-header", - "private-message", - "private-key", - ]) { - expect(redacted).not.toContain(secret); - } - - const huge = "x".repeat(WEB_DIAGNOSTIC_TOTAL_MAX_BYTES * 2); - const formatted = formatWebDiagnostics({ runner: huge, server: huge, log: huge }); - expect(Buffer.byteLength(formatted)).toBeLessThanOrEqual(WEB_DIAGNOSTIC_TOTAL_MAX_BYTES); - expect(formatted).toContain(DIAGNOSTIC_TRUNCATED_MARKER.trim()); - expect( - Buffer.byteLength(appendBoundedDiagnostic("", huge, WEB_DIAGNOSTIC_SOURCE_MAX_BYTES)), - ).toBeLessThanOrEqual(WEB_DIAGNOSTIC_SOURCE_MAX_BYTES); - expect( - Buffer.byteLength( - appendBoundedDiagnostic("", "é".repeat(WEB_DIAGNOSTIC_SOURCE_MAX_BYTES), 101), - ), - ).toBeLessThanOrEqual(101); - - const captured = await runCapturedProcess( - process.execPath, - ["-e", `process.stdout.write("x".repeat(${CAPTURED_PROCESS_STREAM_MAX_BYTES * 2}))`], - { cwd: repoRoot, env: process.env }, - ); - expect(Buffer.byteLength(captured.stdout)).toBeLessThanOrEqual( - CAPTURED_PROCESS_STREAM_MAX_BYTES, - ); - expect(captured.stdout).toContain(DIAGNOSTIC_TRUNCATED_MARKER.trim()); - }); - - it("redacts adversarial multiline bodies, header objects, and quoted secrets in retained files", async () => { - const root = await makeTemp("oq-connected-adversarial-redaction-"); - tempDir = root; - const diagnostics = await createWebDiagnosticsFile({ tmpdir: root }); - const requestSecret = "plain request secret with spaces"; - const responseSecret = "plain response secret with spaces"; - const headerSecret = "header secret after a quoted } brace"; - const passwordSecret = "single quoted password with spaces"; - const content = formatWebDiagnostics({ - runner: `request body:\n${requestSecret}\nrequest continuation`, - server: `response body:\n${responseSecret}\nresponse continuation`, - log: [ - "headers={", - ' "X-Decoy": "quoted } brace",', - ` "X-Private": "${headerSecret}"`, - "}", - `password='${passwordSecret}'`, - ].join("\n"), - }); - await writeWebDiagnosticsFile(diagnostics.filePath, content); - - const retained = await readFile(diagnostics.filePath, "utf8"); - expect(retained).toContain(DIAGNOSTIC_REDACTED_BODY_MARKER); - expect(retained).toContain(DIAGNOSTIC_REDACTED_HEADER_MARKER); - expect(retained).toContain("password='[REDACTED]'"); - for (const secret of [requestSecret, responseSecret, headerSecret, passwordSecret]) { - expect(retained).not.toContain(secret); - } - }); - - it("redacts prefixed blocks, quoted body keys, and escaped passwords in retained files", async () => { - const root = await makeTemp("oq-connected-prefixed-redaction-"); - tempDir = root; - const diagnostics = await createWebDiagnosticsFile({ tmpdir: root }); - const content = formatWebDiagnostics({ - runner: [ - "2026-09-22T12:34:56Z INFO headers={", - ' "X-Private": "prefixed-header-secret"', - "}", - "level=debug config={", - ' "private": "prefixed-config-secret"', - "}", - "runner-after-blocks", - ].join("\n"), - server: [ - '{"response body":{', - ' "text": "quoted-body-secret"', - "}}", - "server-after-body", - ].join("\n"), - log: [ - String.raw`password="double-prefix\"double-tail"`, - String.raw`password='single-prefix\'single-tail'`, - "log-after-passwords", - ].join("\n"), - }); - await writeWebDiagnosticsFile(diagnostics.filePath, content); - const retained = await readFile(diagnostics.filePath, "utf8"); - for (const secret of [ - "prefixed-header-secret", - "prefixed-config-secret", - "quoted-body-secret", - "double-prefix", - "double-tail", - "single-prefix", - "single-tail", - ]) { - expect(retained).not.toContain(secret); - } - for (const marker of [ - DIAGNOSTIC_REDACTED_HEADER_MARKER, - DIAGNOSTIC_REDACTED_CONFIG_MARKER, - DIAGNOSTIC_REDACTED_BODY_MARKER, - 'password="[REDACTED]"', - "password='[REDACTED]'", - "runner-after-blocks", - "server-after-body", - "log-after-passwords", - ]) { - expect(retained).toContain(marker); - } - }); - - it("slices OpenCode log records from the starting offset", async () => { - const prefix = "historical-log-line\n"; - const next = "ProviderModelNotFoundError: Model not found: github-copilot/claude-sonnet-4.6\n"; - expect(sliceLogFromOffset(`${prefix}${next}`, Buffer.byteLength(prefix))).toBe(next); - expect(sliceLogFromOffset(`${prefix}${next}`, Buffer.byteLength(`${prefix}${next}`))).toBe(""); - - const root = await makeTemp("oq-connected-log-slice-"); - const logDir = path.join(root, "log"); - await mkdir(logDir); - const logFile = path.join(logDir, "opencode.log"); - await writeFile(logFile, prefix, "utf8"); - const offsets = await snapshotLogOffsets([logDir]); - await writeFile(logFile, `${prefix}${next}`, "utf8"); - await writeFile( - logFile, - `${prefix}${next}${"x".repeat(WEB_DIAGNOSTIC_LOG_MAX_BYTES * 2)}`, - "utf8", - ); - const sliced = await readNewLogRecords([logDir], offsets); - expect(sliced).toContain(next.trim()); - expect(sliced).not.toContain("historical-log-line"); - expect(Buffer.byteLength(sliced)).toBeLessThanOrEqual(WEB_DIAGNOSTIC_LOG_MAX_BYTES); - expect(sliced).toContain(DIAGNOSTIC_TRUNCATED_MARKER.trim()); - }); - - it("keeps each decoded log slice within 65,536 bytes at a split UTF-8 character", async () => { - const root = await makeTemp("oq-connected-log-utf8-cap-"); - tempDir = root; - const logDir = path.join(root, "log"); - await mkdir(logDir); - const logFile = path.join(logDir, "multibyte.log"); - const contentLimit = - WEB_DIAGNOSTIC_SOURCE_MAX_BYTES - Buffer.byteLength(DIAGNOSTIC_TRUNCATED_MARKER); - await writeFile( - logFile, - `${"x".repeat(contentLimit - 1)}é${"z".repeat(WEB_DIAGNOSTIC_SOURCE_MAX_BYTES)}`, - "utf8", - ); - - const records = await readNewLogRecords([logDir], new Map()); - const heading = "=== multibyte.log ===\n"; - expect(records.startsWith(heading)).toBe(true); - const decodedSlice = records.slice(heading.length, -1); - expect(Buffer.byteLength(decodedSlice)).toBeLessThanOrEqual(WEB_DIAGNOSTIC_SOURCE_MAX_BYTES); - expect(decodedSlice).toContain(DIAGNOSTIC_TRUNCATED_MARKER.trim()); - }); - - it("resolves injectable Linux and macOS OpenCode log paths", () => { - expect(resolveOpenCodeLogDirs({}, "/home/tester", "linux")).toEqual([ - "/home/tester/.local/share/opencode/log", - ]); - expect(resolveOpenCodeLogDirs({ XDG_DATA_HOME: "/xdg" }, "/home/tester", "linux")).toEqual([ - "/xdg/opencode/log", - ]); - expect(resolveOpenCodeLogDirs({}, "/Users/tester", "darwin")).toEqual([ - "/Users/tester/Library/Application Support/opencode/log", - ]); - expect(resolveOpenCodeLogDirs({ XDG_DATA_HOME: "/custom" }, "/Users/tester", "darwin")).toEqual( - ["/custom/opencode/log", "/Users/tester/Library/Application Support/opencode/log"], - ); - }); - - it("resolves injectable Windows OpenCode log paths on any host", () => { - expect(resolveOpenCodeLogDirs({}, "C:\\Users\\tester", "win32")).toEqual([ - "C:\\Users\\tester\\.local\\share\\opencode\\log", - ]); - expect( - resolveOpenCodeLogDirs({ XDG_DATA_HOME: " D:/data/../xdg/ " }, "C:\\Users\\tester", "win32"), - ).toEqual(["D:\\xdg\\opencode\\log"]); - }); - - it.each([ - ["linux", "/home/tester", "/home/tester/.local/share/opencode/log"], - ["darwin", "/Users/tester", "/Users/tester/Library/Application Support/opencode/log"], - ["win32", "C:\\Users\\tester", "C:\\Users\\tester\\.local\\share\\opencode\\log"], - ])("uses the %s fallback for blank XDG_DATA_HOME on any host", (platform, home, expected) => { - expect(resolveOpenCodeLogDirs({ XDG_DATA_HOME: " " }, home, platform)).toEqual([expected]); - }); - - it("deduplicates normalized macOS XDG and Application Support log paths", () => { - expect( - resolveOpenCodeLogDirs( - { XDG_DATA_HOME: " /Users/tester/Library/../Library/Application Support/ " }, - "/Users/tester", - "darwin", - ), - ).toEqual(["/Users/tester/Library/Application Support/opencode/log"]); - }); - - it("deletes the temp workspace while retaining only sanitized diagnostics", async () => { - const root = await makeTemp("oq-connected-diag-cleanup-"); - const source = path.join(root, "source"); - await mkdir(source); - await writeConfigTree(source); - const workspace = await prepareConnectedWorkspace({ - sourceConfigDir: source, - repoRoot, - promptBarEnabled: false, - ensureTuiConfig: false, - tmpdir: root, - }); - const diagnostics = await createWebDiagnosticsFile({ tmpdir: root }); - const content = formatWebDiagnostics({ - originalModel: "github-copilot/claude-sonnet-4.6", - temporaryModel: "github-copilot/gpt-4.1", - runner: `token=${SECRET}\nplugin initialized\n`, - server: "Authorization: Bearer abc.def\n", - log: "ProviderModelNotFoundError: Model not found: github-copilot/claude-sonnet-4.6\n", - }); - await writeWebDiagnosticsFile(diagnostics.filePath, content); - const tempRoot = workspace.tempRoot; - await cleanupConnectedWorkspace(tempRoot, { tmpdir: root }); - await expect(lstat(tempRoot)).rejects.toThrow(); - expect(await readFile(diagnostics.filePath, "utf8")).toContain( - WEB_FAILURE_INVALID_MODEL_BEFORE_HOOK, - ); - expect(await readFile(diagnostics.filePath, "utf8")).not.toContain(SECRET); - expect(await readFile(diagnostics.filePath, "utf8")).not.toContain("abc.def"); - expect(formatWebDiagnosticsPath(diagnostics.filePath)).toContain(diagnostics.filePath); - expect(await prefixDirs(root)).toEqual([]); - }); - - it("does not reinterpret malformed structured catalog output as plain model IDs", () => { - for (const text of [ - '"p/model', - '"p/model" trailing', - 'warning: cached catalog unavailable\n"p/model', - '"p/model\nopenai/gpt-5', - '{"id":"openai/gpt-5"', - '[{"id":"openai/gpt-5"}', - ]) { - expect(parseOpenCodeModelsOutput(text)).toEqual([]); - } - expect(parseOpenCodeModelsOutput('[{"id":"openai/gpt-5"}] trailing')).toEqual([]); - expect( - parseOpenCodeModelsOutput("warning: cached catalog unavailable\nopenai/gpt-5\n"), - ).toMatchObject([{ id: "openai/gpt-5", usability: "usable", source: "plain" }]); - }); - - it("retains catalog usability metadata and fails closed for aliases or unknown entries", () => { - const plain = parseOpenCodeModelsOutput( - "warning: cache is stale\ngithub-copilot/gpt-4.1\nhttps://example.com/x\n", - ); - expect(plain).toMatchObject([ - { id: "github-copilot/gpt-4.1", usability: "usable", source: "plain" }, - ]); - expect( - parseOpenCodeModelsOutput( - "openai/ready status=ready\nopenai/alias alias\nopenai/disabled disabled\nopenai/unknown tier=premium\n", - ).map((model) => [model.id, model.usability]), - ).toEqual([ - ["openai/ready", "usable"], - ["openai/alias", "rejected"], - ["openai/disabled", "rejected"], - ["openai/unknown", "unknown"], - ]); - - const structured = parseOpenCodeModelsOutput( - `warning: using cached provider metadata -${JSON.stringify({ - models: [ - { providerID: "openai", id: "gpt-5", available: true, context: 400000 }, - { providerID: "openai", id: "gpt-alias", available: true, aliasOf: "gpt-5" }, - { providerID: "openai", id: "gpt-hidden", available: true, hidden: true }, - { providerID: "openai", id: "gpt-offline", status: "unavailable" }, - { providerID: "openai", id: "gpt-unknown", context: 128000 }, - { providerID: "openai", id: "gpt-conflict", status: "active", availability: "unavailable" }, - { providerID: "openai", id: "gpt-disabled-active", status: "active", disabled: true }, - { providerID: "openai", id: "gpt-hidden-active", status: "active", hidden: true }, - { providerID: "openai", id: "gpt-alias-active", status: "active", alias: true }, - { providerID: "openai", id: "gpt-ambiguous", status: "active", availability: "sometimes" }, - { providerID: "github-copilot", id: "gpt-4.1", status: "ready" }, - ], -})}`, - ); - expect(structured.map((model) => [model.id, model.usability])).toEqual([ - ["openai/gpt-5", "usable"], - ["openai/gpt-alias", "rejected"], - ["openai/gpt-hidden", "rejected"], - ["openai/gpt-offline", "rejected"], - ["openai/gpt-unknown", "unknown"], - ["openai/gpt-conflict", "rejected"], - ["openai/gpt-disabled-active", "rejected"], - ["openai/gpt-hidden-active", "rejected"], - ["openai/gpt-alias-active", "rejected"], - ["openai/gpt-ambiguous", "unknown"], - ["github-copilot/gpt-4.1", "usable"], - ]); - expect((structured[0]?.metadata as { context?: number }).context).toBe(400000); - expect(chooseConnectedWebModel("openai/stale", structured)).toEqual({ - status: "replace", - model: "openai/gpt-5", - }); - expect( - chooseConnectedWebModel( - "openai/stale", - structured.filter((model) => model.usability !== "usable"), - ), - ).toEqual({ status: "empty-catalog", model: null }); - expect( - chooseConnectedWebModel("openai/stale", ["z/model", "a/model", "openai/z", "openai/a"]), - ).toEqual({ status: "replace", model: "openai/a" }); - }); - - it("requires affirmative assignments for both output and busy evidence", () => { - for (const output of [ - "raw output injected", - "injected raw output", - "quota output ready", - "quota output rendered", - "quota output written", - "slash command output complete", - ]) { - for (const busy of ["session busy", "session is busy", "session-busy"]) { - for (const assignment of ["=false", ": false", "=0", "=unknown", '="false"']) { - expect( - classifyWebDiagnostic(`command.execute.before\n${output}${assignment}\n${busy}`), - ).toBe(WEB_FAILURE_UNKNOWN); - expect( - classifyWebDiagnostic(`command.execute.before\n${output}\n${busy}${assignment}`), - ).toBe(WEB_FAILURE_UNKNOWN); - } - expect(classifyWebDiagnostic(`command.execute.before\n${output}=true\n${busy}=true`)).toBe( - WEB_FAILURE_POST_INJECTION_BUSY_272, - ); - expect(classifyWebDiagnostic(`command.execute.before\n${output}\nnot ${busy}`)).toBe( - WEB_FAILURE_UNKNOWN, - ); - } - } - }); - - it("prints click/select guidance and requires ordered positive diagnostic evidence", () => { - const guidance = formatConnectedWebModelGuidance({ - originalModel: "github-copilot/claude-sonnet-4.6", - catalog: ["github-copilot/gpt-4.1", "openai/gpt-5"], - reason: "empty-catalog", - }); - expect(guidance).toContain(CONNECTED_WEB_COMMANDS[0]); - expect(guidance).toContain(CONNECTED_WEB_COMMANDS[1]); - expect(guidance).toContain("Click the model name in the OpenCode Web session header"); - expect(guidance).toContain("github-copilot/gpt-4.1"); - expect(getConnectedUsage()).toContain("opencode models"); - expect(getConnectedUsage()).toMatch(/bounded, sanitized diagnostics/i); - expect(chooseConnectedWebModel("gpt-4.1", ["github-copilot/gpt-4.1", "openai/gpt-5"])).toEqual({ - status: "replace", - model: "github-copilot/gpt-4.1", - }); - expect( - classifyWebDiagnostic( - "plugin initialized\nProviderModelNotFoundError: Model not found: github-copilot/claude-sonnet-4.6", - ), - ).toBe(WEB_FAILURE_INVALID_MODEL_BEFORE_HOOK); - expect( - classifyWebDiagnostic( - "ProviderModelNotFoundError\ncommand.execute.before\nFailed to send command", - ), - ).toBe(WEB_FAILURE_UNKNOWN); - expect(classifyWebDiagnostic("command.execute.before\nFailed to inject raw output")).toBe( - WEB_FAILURE_HOOK_ENTRY_BUILD_INJECTION, - ); - expect( - classifyWebDiagnostic( - "command.execute.before\nquota output rendered\nFailed to send command: network disconnected", - ), - ).toBe(WEB_FAILURE_UNKNOWN); - expect( - classifyWebDiagnostic("command.execute.before\nnot raw output injected\nsession is busy"), - ).toBe(WEB_FAILURE_UNKNOWN); - expect( - classifyWebDiagnostic( - "command.execute.before\nquota output rendered\nFailed to send command: session is busy", - ), - ).toBe(WEB_FAILURE_POST_INJECTION_BUSY_272); - expect(classifyWebDiagnostic("session is busy")).toBe(WEB_FAILURE_UNKNOWN); - expect( - formatWebDiagnostics({ - runner: "ProviderModelNotFoundError", - server: "command.execute.before\nFailed to send command", - }), - ).toContain(`Failure class: ${WEB_FAILURE_UNKNOWN}`); - }); - - it.each([ - { - label: "empty catalog", - env: { FAKE_MODELS_OUTPUT: "No models available\n" }, - }, - { - label: "failed catalog command", - env: { - FAKE_MODELS_EXIT: "1", - FAKE_MODELS_STDERR: "Authorization: Bearer catalog-secret\n", - }, - }, - ])("uses a fake executable to stop before Web launch for $label", async ({ env }) => { - if (process.platform === "win32") return; - const root = await makeTemp("oq-connected-web-catalog-e2e-"); - const harness = await writeFakeWebHarness(root); - const originalConfig = await readFile(path.join(harness.source, "opencode.jsonc"), "utf8"); - const output: string[] = []; - const code = await runConnected(["--web"], { - repoRoot: harness.fakeRepo, - tmpdir: root, - env: { ...harness.env, ...env }, - stdout: captureOutput(output), - stderr: captureOutput(output), - }); - expect(code).toBe(1); - expect((await readFakeRecords(harness.recordFile)).map((record) => record.args)).toEqual([ - ["models"], - ]); - expect(output.join("")).toContain("refused to launch"); - expect(output.join("")).not.toContain("catalog-secret"); - expect(await readFile(path.join(harness.source, "opencode.jsonc"), "utf8")).toBe( - originalConfig, - ); - expect(await prefixDirs(root)).toEqual([]); - }); - - it.each([ - undefined, - process.execPath, - ])("runs fake Web with isolated env and preserves exit after a diagnostic retry (inherited npm_execpath: %s)", async (npmExecPath) => { - // An existing non-pnpm path makes accidental inheritance fail even in focused runs. - vi.stubEnv("npm_execpath", npmExecPath); - const root = await makeTemp("oq-connected-web-run-e2e with spaces-"); - const harness = await writeFakeWebHarness(root); - const originalConfig = await readFile(path.join(harness.source, "opencode.jsonc"), "utf8"); - const output: string[] = []; - let writes = 0; - const code = await runConnected(["--web"], { - repoRoot: harness.fakeRepo, - tmpdir: root, - env: { - ...harness.env, - FAKE_MODELS_OUTPUT: "provider/model\n", - FAKE_WEB_EXIT: "7", - FAKE_WEB_STDOUT: 'response body: {"private":"reply"}\n', - FAKE_WEB_STDERR: "Cookie: session=browser-secret\n", - }, - stdout: captureOutput(output), - stderr: captureOutput(output), - async writeWebDiagnosticsFile(filePath: string, content: string) { - writes += 1; - if (writes === 1) throw new Error("injected diagnostic write failure"); - return writeWebDiagnosticsFile(filePath, content); - }, - }); - expect(code).toBe(7); - expect(JSON.parse(await readFile(harness.buildRecordFile, "utf8"))).toEqual(["run", "build"]); - expect(writes).toBe(2); - const records = await readFakeRecords(harness.recordFile); - expect(records.map((record) => record.args)).toEqual([["models"], ["web"]]); - expect(records[0]?.configDir).toBe(records[1]?.configDir); - expect(records[0]?.configFile).toBeNull(); - expect(records[1]?.configFile).toBeNull(); - expect(records[0]?.cwd).toBe(records[1]?.cwd); - expect(records[1]?.configContents).toContain("// keep this comment and plugin order"); - expect(records[1]?.configContents).toMatch( - /companion-before[\s\S]+dist\/index\.js[\s\S]+companion-after/, - ); - expect(records[1]?.configContents).toContain('"model": "provider/model"'); - await expect(lstat(String(records[0]?.configDir))).rejects.toThrow(); - expect(await readFile(path.join(harness.source, "opencode.jsonc"), "utf8")).toBe( - originalConfig, - ); - expect(await prefixDirs(root)).toEqual([]); - - const combined = output.join(""); - expect(combined).toContain("injected diagnostic write failure"); - const pathMatches = [ - ...combined.matchAll(/Web diagnostics \(sanitized, this run only\): (.+)/g), - ]; - expect(pathMatches).toHaveLength(1); - const diagnosticPath = pathMatches[0]?.[1]?.trim(); - expect(diagnosticPath).toBeTruthy(); - expect(diagnosticPath).not.toContain(String(records[0]?.configDir)); - const diagnostic = await readFile(diagnosticPath as string, "utf8"); - expect(diagnostic).toContain(DIAGNOSTIC_REDACTED_BODY_MARKER); - expect(diagnostic).toContain(DIAGNOSTIC_REDACTED_HEADER_MARKER); - expect(diagnostic).not.toContain("browser-secret"); - expect(diagnostic).not.toContain('"private":"reply"'); - if (process.platform !== "win32") { - expect((await lstat(path.dirname(diagnosticPath as string))).mode & 0o777).toBe(0o700); - expect((await lstat(diagnosticPath as string)).mode & 0o777).toBe(0o600); - } - }); - - it("preserves the original preflight signal after escalation and prints no catalog guidance", async () => { - if (process.platform === "win32") return; - const root = await makeTemp("oq-connected-web-signal-e2e-"); - const harness = await writeFakeWebHarness(root); - const output: string[] = []; - const signalSource = new EventEmitter(); - const running = runConnected(["--web"], { - repoRoot: harness.fakeRepo, - tmpdir: root, - signalGraceMs: 60, - signalSource, - env: { ...harness.env, FAKE_MODELS_HANG: "1" }, - stdout: captureOutput(output), - stderr: captureOutput(output), - }); - await waitForFile(harness.startedFile, "models\n"); - signalSource.emit("SIGTERM"); - expect(await running).toBe(143); - expect((await readFakeRecords(harness.recordFile)).map((record) => record.args)).toEqual([ - ["models"], - ]); - expect(output.join("")).not.toContain("Connected Web preflight refused to launch"); - expect(await prefixDirs(root)).toEqual([]); - }); -}); diff --git a/tests/test-stabilization-fixtures.test.ts b/tests/test-stabilization-fixtures.test.ts deleted file mode 100644 index 521d8bc5..00000000 --- a/tests/test-stabilization-fixtures.test.ts +++ /dev/null @@ -1,495 +0,0 @@ -import { spawn, spawnSync } from "node:child_process"; -import { EventEmitter } from "node:events"; -import { chmod, lstat, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { - assertFixtureContentsAreIsolated, - assertFrozenFixtureFiles, - buildFixtureChildEnv, - buildVitestArgs, - cleanupFixtureSandbox, - FIXTURE_STRIP_ENV_VARS, - FIXTURE_TEMP_PREFIX, - FIXTURE_TEST_FILES, - getFixturesUsage, - parseFixturesArgs, - prepareFixtureSandbox, - resolveVitestExecutable, - runFixtures, -} from "../scripts/test-stabilization-fixtures.mjs"; - -const scriptPath = fileURLToPath( - new URL("../scripts/test-stabilization-fixtures.mjs", import.meta.url), -); -const repoRoot = fileURLToPath(new URL("..", import.meta.url)); -const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as { - scripts?: Record; -}; - -let tempDir: string | undefined; - -async function makeTemp(prefix: string): Promise { - const dir = await mkdtemp(path.join(tmpdir(), prefix)); - tempDir = dir; - return dir; -} - -afterEach(async () => { - if (!tempDir) return; - await rm(tempDir, { recursive: true, force: true }); - tempDir = undefined; -}); - -function runScript(args: string[]) { - return spawnSync(process.execPath, [scriptPath, ...args], { - cwd: repoRoot, - encoding: "utf8", - env: process.env, - }); -} - -async function prefixDirs(dir: string): Promise { - return (await readdir(dir)).filter((name) => name.startsWith(FIXTURE_TEMP_PREFIX)); -} - -async function waitForFile(filePath: string, expected?: string): Promise { - for (let i = 0; i < 100; i++) { - try { - const contents = await readFile(filePath, "utf8"); - if (expected === undefined || contents === expected) return contents; - } catch { - // The child has not created the file yet. - } - await new Promise((resolve) => setTimeout(resolve, 20)); - } - return await readFile(filePath, "utf8"); -} - -function captureIo() { - const logs: string[] = []; - return { - logs, - stdout: { - write(chunk: string) { - logs.push(chunk); - return true; - }, - }, - stderr: { - write(chunk: string) { - logs.push(chunk); - return true; - }, - }, - }; -} - -describe("test-stabilization-fixtures", () => { - it("keeps a frozen fake-data file list and package command", () => { - expect(FIXTURE_TEST_FILES).toEqual([ - "tests/lib.config-symlink-write.test.ts", - "tests/lib.config-write-target.test.ts", - "tests/lib.atomic-json.test.ts", - "tests/lib.opencode-go.test.ts", - "tests/providers.opencode-go.test.ts", - "tests/providers.opencode-go.surfaces.test.ts", - "tests/providers.synthetic.test.ts", - "tests/providers.synthetic.surfaces.test.ts", - "tests/lib.openrouter.test.ts", - "tests/providers.openrouter.test.ts", - "tests/providers.openrouter.surfaces.test.ts", - "tests/lib.alibaba-token-plan.test.ts", - "tests/providers.alibaba-token-plan.test.ts", - "tests/lib.quota-status.test.ts", - "tests/quota-render-data.test.ts", - "tests/tui-runtime.test.ts", - "tests/tui-prompt-bar-format.test.ts", - "tests/lib.quota-export.test.ts", - "tests/lib.quota-export.production-policy.test.ts", - "tests/lib.api-key-provider-queries.test.ts", - "tests/lib.provider-api-key-configs.test.ts", - "tests/lib.api-key-resolver.test.ts", - "tests/contribution-guidance.test.ts", - ]); - expect(pkg.scripts?.["test:stabilization:fixtures"]).toBe( - "node scripts/test-stabilization-fixtures.mjs", - ); - expect(pkg.scripts?.["test:stabilization:tui"]).toBe( - "node scripts/test-stabilization-connected.mjs --tui", - ); - expect(pkg.scripts?.["test:stabilization:tui:prompt-bar"]).toBe( - "node scripts/test-stabilization-connected.mjs --tui --prompt-bar", - ); - expect(pkg.scripts?.["test:stabilization:web"]).toBe( - "node scripts/test-stabilization-connected.mjs --web", - ); - expect(parseFixturesArgs(["--dry-run"])).toEqual({ dryRun: true, help: false }); - expect(getFixturesUsage()).toContain("bl executable"); - expect(FIXTURE_STRIP_ENV_VARS).toContain("OPENCODE_CONFIG"); - expect(FIXTURE_STRIP_ENV_VARS).toContain("OPENCODE_CONFIG_DIR"); - expect(FIXTURE_STRIP_ENV_VARS).toContain("OPENROUTER_API_KEY"); - }); - - it("builds a no-shell Vitest command with the repo executable and fixed args", () => { - const vitestCli = resolveVitestExecutable(repoRoot); - expect(vitestCli).toBe(path.join(repoRoot, "node_modules", "vitest", "vitest.mjs")); - expect(buildVitestArgs()).toEqual(["run", ...FIXTURE_TEST_FILES]); - assertFrozenFixtureFiles([...FIXTURE_TEST_FILES]); - expect(() => assertFrozenFixtureFiles(["tests/http.test.ts"])).toThrow(/allowlist/); - expect(() => assertFrozenFixtureFiles([...FIXTURE_TEST_FILES, "tests/http.test.ts"])).toThrow( - /allowlist/, - ); - }); - - it("rejects selected tests that would escape mocks into bl, a shell, or live HTTP", () => { - const isolated: Record = Object.fromEntries( - FIXTURE_TEST_FILES.map((file) => [file, "describe('ok', () => {})\n"]), - ); - expect(() => assertFixtureContentsAreIsolated(isolated)).not.toThrow(); - - expect(() => - assertFixtureContentsAreIsolated({ - ...isolated, - "tests/lib.alibaba-token-plan.test.ts": 'spawn("bl", ["usage"]);\n', - }), - ).toThrow(/real bl executable/); - - expect(() => - assertFixtureContentsAreIsolated({ - ...isolated, - "tests/lib.openrouter.test.ts": "spawn(process.execPath, ['x'], { shell: true });\n", - }), - ).toThrow(/shell/); - - expect(() => - assertFixtureContentsAreIsolated({ - ...isolated, - "tests/providers.openrouter.test.ts": 'await fetch("https://openrouter.ai/api/v1/key");\n', - }), - ).toThrow(/live OpenRouter/); - - expect(() => - assertFixtureContentsAreIsolated({ - ...isolated, - extra: "ok", - } as Record), - ).toThrow(/outside the frozen fake-data allowlist/); - }); - - it("prints help and dry-run without launching Vitest or creating a sandbox", async () => { - const help = runScript(["--help"]); - expect(help.status).toBe(0); - expect(help.stdout).toContain("frozen fake-data"); - - const root = await makeTemp("oq-fixtures-dry-"); - const io = captureIo(); - const code = await runFixtures(["--dry-run"], { - stdout: io.stdout, - stderr: io.stderr, - tmpdir: root, - }); - const output = io.logs.join(""); - expect(code).toBe(0); - expect(output).toContain("no real credentials, network provider calls, or real bl executable"); - expect(output).toContain(process.execPath); - expect(output).toContain(path.join(repoRoot, "node_modules", "vitest", "vitest.mjs")); - expect(output).toContain("tests/quota-render-data.test.ts"); - expect(output).toContain("Dry run"); - expect(output).not.toMatch(/\bshell\s+true\b/); - expect(await prefixDirs(root)).toEqual([]); - }); - - it("rejects unknown CLI arguments", () => { - const result = runScript(["--network"]); - expect(result.status).toBe(2); - expect(`${result.stdout}${result.stderr}`).toContain("Unknown argument"); - }); - - it("remaps HOME and XDG dirs into the sandbox and strips credential env vars", async () => { - const root = await makeTemp("oq-fixtures-env-"); - const realHome = path.join(root, "real-home"); - await mkdir(realHome); - await writeFile(path.join(realHome, "secret.txt"), "profile-secret\n", "utf8"); - const sandbox = await prepareFixtureSandbox({ tmpdir: root }); - tempDir = root; - - if (process.platform !== "win32") { - expect((await lstat(sandbox.tempRoot)).mode & 0o777).toBe(0o700); - expect((await lstat(sandbox.home)).mode & 0o777).toBe(0o700); - expect((await lstat(sandbox.config)).mode & 0o777).toBe(0o700); - } - - const childEnv = buildFixtureChildEnv( - { - HOME: realHome, - USERPROFILE: realHome, - XDG_CONFIG_HOME: path.join(realHome, ".config"), - XDG_DATA_HOME: path.join(realHome, ".local", "share"), - XDG_STATE_HOME: path.join(realHome, ".local", "state"), - XDG_CACHE_HOME: path.join(realHome, ".cache"), - APPDATA: path.join(realHome, "AppData", "Roaming"), - LOCALAPPDATA: path.join(realHome, "AppData", "Local"), - OPENCODE_CONFIG: path.join(realHome, "opencode.json"), - OPENCODE_CONFIG_DIR: path.join(realHome, ".config", "opencode"), - OPENROUTER_API_KEY: "real-openrouter-secret", - SYNTHETIC_API_KEY: "real-synthetic-secret", - MIMO_USAGE_COOKIE: "real-mimo-cookie", - CURSOR_ACP_HOME_DIR: realHome, - PATH: "/usr/bin", - }, - sandbox, - ); - - expect(childEnv.HOME).toBe(sandbox.home); - expect(childEnv.USERPROFILE).toBe(sandbox.home); - expect(childEnv.XDG_CONFIG_HOME).toBe(sandbox.config); - expect(childEnv.XDG_DATA_HOME).toBe(sandbox.data); - expect(childEnv.XDG_STATE_HOME).toBe(sandbox.state); - expect(childEnv.XDG_CACHE_HOME).toBe(sandbox.cache); - expect(childEnv.HOME).not.toBe(realHome); - expect(childEnv.XDG_CONFIG_HOME).not.toBe(path.join(realHome, ".config")); - expect(childEnv.OPENCODE_CONFIG).toBeUndefined(); - expect(childEnv.OPENCODE_CONFIG_DIR).toBeUndefined(); - expect(childEnv.OPENROUTER_API_KEY).toBeUndefined(); - expect(childEnv.SYNTHETIC_API_KEY).toBeUndefined(); - expect(childEnv.MIMO_USAGE_COOKIE).toBeUndefined(); - expect(childEnv.CURSOR_ACP_HOME_DIR).toBeUndefined(); - expect(childEnv.PATH).toBe("/usr/bin"); - - await cleanupFixtureSandbox(sandbox.tempRoot, { tmpdir: root }); - await expect(lstat(sandbox.tempRoot)).rejects.toThrow(); - expect(await readFile(path.join(realHome, "secret.txt"), "utf8")).toBe("profile-secret\n"); - }); - - it("spawns a child that cannot reach the real profile through HOME or XDG", async () => { - const root = await makeTemp("oq-fixtures-child-"); - const realHome = path.join(root, "real-home"); - await mkdir(realHome); - await writeFile(path.join(realHome, "secret.txt"), "profile-secret\n", "utf8"); - const sandbox = await prepareFixtureSandbox({ tmpdir: root }); - tempDir = root; - - const dumpPath = path.join(root, "dump.json"); - const childEnv = buildFixtureChildEnv( - { - ...process.env, - HOME: realHome, - USERPROFILE: realHome, - XDG_CONFIG_HOME: path.join(realHome, ".config"), - XDG_DATA_HOME: path.join(realHome, ".local", "share"), - XDG_STATE_HOME: path.join(realHome, ".local", "state"), - XDG_CACHE_HOME: path.join(realHome, ".cache"), - OPENCODE_CONFIG: path.join(realHome, "opencode.json"), - OPENCODE_CONFIG_DIR: path.join(realHome, ".config", "opencode"), - OPENROUTER_API_KEY: "real-openrouter-secret", - SYNTHETIC_API_KEY: "real-synthetic-secret", - }, - sandbox, - ); - - const script = ` -import { existsSync, writeFileSync } from "node:fs"; -import { homedir } from "node:os"; -import path from "node:path"; -writeFileSync(process.argv[1], JSON.stringify({ - HOME: process.env.HOME, - USERPROFILE: process.env.USERPROFILE, - XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, - XDG_DATA_HOME: process.env.XDG_DATA_HOME, - XDG_STATE_HOME: process.env.XDG_STATE_HOME, - XDG_CACHE_HOME: process.env.XDG_CACHE_HOME, - OPENCODE_CONFIG: process.env.OPENCODE_CONFIG, - OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR, - OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY, - SYNTHETIC_API_KEY: process.env.SYNTHETIC_API_KEY, - homedir: homedir(), - homeSecretExists: existsSync(path.join(process.env.HOME ?? "", "secret.txt")), -})); -`; - const result = spawnSync(process.execPath, ["--input-type=module", "-e", script, dumpPath], { - cwd: root, - encoding: "utf8", - env: childEnv, - }); - expect(result.status).toBe(0); - const dump = JSON.parse(await readFile(dumpPath, "utf8")) as { - HOME: string; - XDG_CONFIG_HOME: string; - OPENCODE_CONFIG?: string; - OPENCODE_CONFIG_DIR?: string; - OPENROUTER_API_KEY?: string; - SYNTHETIC_API_KEY?: string; - homedir: string; - homeSecretExists: boolean; - }; - expect(dump.HOME).toBe(sandbox.home); - expect(dump.homedir).toBe(sandbox.home); - expect(dump.XDG_CONFIG_HOME).toBe(sandbox.config); - expect(dump.HOME).not.toBe(realHome); - expect(dump.OPENCODE_CONFIG).toBeUndefined(); - expect(dump.OPENCODE_CONFIG_DIR).toBeUndefined(); - expect(dump.OPENROUTER_API_KEY).toBeUndefined(); - expect(dump.SYNTHETIC_API_KEY).toBeUndefined(); - expect(dump.homeSecretExists).toBe(false); - expect(await readFile(path.join(realHome, "secret.txt"), "utf8")).toBe("profile-secret\n"); - }); - - it.runIf(process.platform !== "win32")( - "keeps PATH so a fake executable still runs in the isolated env", - async () => { - const root = await makeTemp("oq-fixtures-bl-"); - const bin = path.join(root, "bin"); - await mkdir(bin); - const fakeBl = path.join(bin, "bl"); - await writeFile(fakeBl, "#!/bin/sh\necho fake-bl-ok\n", "utf8"); - await chmod(fakeBl, 0o755); - const sandbox = await prepareFixtureSandbox({ tmpdir: root }); - tempDir = root; - const childEnv = buildFixtureChildEnv( - { - ...process.env, - PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}`, - OPENROUTER_API_KEY: "must-not-leak", - }, - sandbox, - ); - const result = spawnSync("bl", [], { cwd: root, encoding: "utf8", env: childEnv }); - expect(result.status).toBe(0); - expect(result.stdout).toContain("fake-bl-ok"); - expect(childEnv.OPENROUTER_API_KEY).toBeUndefined(); - }, - ); - - it("creates a 0700 sandbox for Vitest, isolates the child env, and deletes it on success", async () => { - const root = await makeTemp("oq-fixtures-run-"); - const realHome = path.join(root, "real-home"); - await mkdir(realHome); - await writeFile(path.join(realHome, "secret.txt"), "profile-secret\n", "utf8"); - let capturedEnv: NodeJS.ProcessEnv | undefined; - let capturedSandboxHome: string | undefined; - const io = captureIo(); - const code = await runFixtures([], { - stdout: io.stdout, - stderr: io.stderr, - tmpdir: root, - stdio: "ignore", - env: { - ...process.env, - HOME: realHome, - USERPROFILE: realHome, - OPENCODE_CONFIG: path.join(realHome, "opencode.json"), - OPENCODE_CONFIG_DIR: path.join(realHome, ".config", "opencode"), - OPENROUTER_API_KEY: "real-openrouter-secret", - }, - spawn(_command, _args, options) { - capturedEnv = options?.env as NodeJS.ProcessEnv; - capturedSandboxHome = capturedEnv.HOME; - return spawn(process.execPath, ["-e", "process.exit(0)"], { - cwd: options?.cwd, - env: options?.env, - stdio: "ignore", - shell: false, - }); - }, - }); - expect(code).toBe(0); - expect(capturedEnv).toBeDefined(); - expect(capturedEnv?.HOME).toBeDefined(); - expect(capturedEnv?.HOME).not.toBe(realHome); - expect(capturedEnv?.HOME).toContain(FIXTURE_TEMP_PREFIX); - expect(capturedEnv?.XDG_CONFIG_HOME).toContain(FIXTURE_TEMP_PREFIX); - expect(capturedEnv?.XDG_DATA_HOME).toContain(FIXTURE_TEMP_PREFIX); - expect(capturedEnv?.XDG_STATE_HOME).toContain(FIXTURE_TEMP_PREFIX); - expect(capturedEnv?.XDG_CACHE_HOME).toContain(FIXTURE_TEMP_PREFIX); - expect(capturedEnv?.OPENCODE_CONFIG).toBeUndefined(); - expect(capturedEnv?.OPENCODE_CONFIG_DIR).toBeUndefined(); - expect(capturedEnv?.OPENROUTER_API_KEY).toBeUndefined(); - expect(await prefixDirs(root)).toEqual([]); - if (capturedSandboxHome) { - await expect(lstat(capturedSandboxHome)).rejects.toThrow(); - } - expect(await readFile(path.join(realHome, "secret.txt"), "utf8")).toBe("profile-secret\n"); - }); - - it("deletes the sandbox when the child exits with an error", async () => { - const root = await makeTemp("oq-fixtures-fail-"); - const io = captureIo(); - const code = await runFixtures([], { - stdout: io.stdout, - stderr: io.stderr, - tmpdir: root, - stdio: "ignore", - spawn(_command, _args, options) { - return spawn(process.execPath, ["-e", "process.exit(7)"], { - cwd: options?.cwd, - env: options?.env, - stdio: "ignore", - shell: false, - }); - }, - }); - expect(code).toBe(7); - expect(await prefixDirs(root)).toEqual([]); - }); - - it.runIf(process.platform !== "win32")( - "forwards SIGTERM, waits for exit, then deletes the sandbox", - async () => { - const root = await makeTemp("oq-fixtures-sig-"); - const marker = path.join(root, "marker.txt"); - const signalSource = new EventEmitter(); - const io = captureIo(); - const running = runFixtures([], { - stdout: io.stdout, - stderr: io.stderr, - tmpdir: root, - stdio: "ignore", - signalSource, - spawn(_command, _args, options) { - return spawn( - process.execPath, - [ - "-e", - `const { writeFileSync } = require("node:fs"); -writeFileSync(${JSON.stringify(marker)}, "started\\n"); -process.on("SIGTERM", () => { - writeFileSync(${JSON.stringify(marker)}, "exited\\n"); - process.exit(0); -}); -setInterval(() => {}, 1000); -`, - ], - { - cwd: options?.cwd, - env: options?.env, - stdio: "ignore", - shell: false, - }, - ); - }, - }); - expect(await waitForFile(marker, "started\n")).toBe("started\n"); - expect(await prefixDirs(root)).toHaveLength(1); - signalSource.emit("SIGTERM"); - const code = await running; - expect(await readFile(marker, "utf8")).toBe("exited\n"); - expect(code).toBe(128 + 15); - expect(await prefixDirs(root)).toEqual([]); - }, - ); - - it("refuses to delete a path outside the expected fixture temp prefix", async () => { - const root = await makeTemp("oq-fixtures-guard-"); - const keep = path.join(root, "keep.txt"); - await writeFile(keep, "keep\n", "utf8"); - await expect(cleanupFixtureSandbox(root, { tmpdir: root })).rejects.toThrow( - /Refusing to delete/, - ); - expect(await readFile(keep, "utf8")).toBe("keep\n"); - }); -}); diff --git a/tests/tui-config-diagnostics.test.ts b/tests/tui-config-diagnostics.test.ts index 1ac59422..deb8b1e1 100644 --- a/tests/tui-config-diagnostics.test.ts +++ b/tests/tui-config-diagnostics.test.ts @@ -77,12 +77,12 @@ describe("inspectTuiConfig", () => { expect(diagnostics.quotaPluginConfigPaths).toEqual([join(projectDir, "tui.jsonc")]); }); - it("supports local file plugin specs that point at dist/tui.tsx", async () => { + it("supports local file plugin specs that point at dist/tui.js", async () => { writeFileSync( join(projectDir, "tui.json"), JSON.stringify({ plugin: [ - ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.tsx", { debug: true }], + ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.js", { debug: true }], ], }), "utf8", diff --git a/tests/tui-dist-packaging.test.ts b/tests/tui-dist-packaging.test.ts index eeea3778..932261a4 100644 --- a/tests/tui-dist-packaging.test.ts +++ b/tests/tui-dist-packaging.test.ts @@ -43,7 +43,7 @@ describe("tui dist packaging", () => { expect(typeof local.default.setup).toBe("function"); }); - it("ships the precompiled TUI entry and removes stale jsx artifacts", async () => { + it("ships the precompiled TUI entry without jsx artifacts", async () => { const distTui = new URL("../dist/tui.js", import.meta.url); const distJsx = new URL("../dist/tui.jsx", import.meta.url); const distJsxMap = new URL("../dist/tui.jsx.map", import.meta.url); diff --git a/tests/tui-first-quota-lifecycle.test.ts b/tests/tui-first-quota-lifecycle.test.ts deleted file mode 100644 index 5636c587..00000000 --- a/tests/tui-first-quota-lifecycle.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createTuiRefreshLifecycle } from "../src/lib/tui-refresh-lifecycle.js"; -import { - FIRST_QUOTA_RECORDED_SAMPLES, - type FirstQuotaScenario, - type FirstQuotaScenarioResult, - formatFirstQuotaReport, - runFirstQuotaScenarioSamples, -} from "./helpers/tui-first-quota-timing.js"; - -const RECOVERY_DELAYS_MS = [500, 1_500, 4_000] as const; -const EVENT_REFRESH_DELAYS_MS = [150, 600] as const; - -const scenarios: FirstQuotaScenario[] = [ - { - id: "S0_IMMEDIATE", - description: "Immediate success", - loaderSteps: [{ afterMs: 0, outcome: "resolve" }], - }, - { - id: "S1_BEFORE_150", - description: "Initial success before the first event refresh", - loaderSteps: [{ afterMs: 100, outcome: "resolve" }], - eventAtMs: 0, - eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS, - }, - { - id: "S2_AFTER_150", - description: "Initial success after the first event refresh", - loaderSteps: [ - { afterMs: 200, outcome: "resolve" }, - { afterMs: 40, outcome: "resolve" }, - ], - eventAtMs: 0, - eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS, - }, - { - id: "S3_AFTER_500", - description: "Initial success after the first mount recovery", - loaderSteps: [ - { afterMs: 700, outcome: "resolve" }, - { afterMs: 40, outcome: "resolve" }, - ], - recoveryDelaysMs: RECOVERY_DELAYS_MS, - }, - { - id: "S4_OVERLAP", - description: "Initial success after overlapping event and recovery refreshes", - loaderSteps: [ - { afterMs: 700, outcome: "resolve" }, - { afterMs: 100, outcome: "resolve" }, - ], - eventAtMs: 0, - eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS, - recoveryDelaysMs: RECOVERY_DELAYS_MS, - }, - { - id: "S5_REJECT_RECOVER", - description: "Initial rejection followed by queued recovery success", - loaderSteps: [ - { afterMs: 700, outcome: "reject" }, - { afterMs: 50, outcome: "resolve" }, - ], - recoveryDelaysMs: RECOVERY_DELAYS_MS, - }, - { - id: "S6_DISPOSED", - description: "Final disposal before the initial loader settles", - loaderSteps: [{ afterMs: 200, outcome: "resolve" }], - disposeAtMs: 100, - }, - { - id: "S7_SUCCESSOR_LATE", - description: "Successor applies before a disposed predecessor settles late", - loaderSteps: [], - successor: { - createAtMs: 100, - predecessorSettlesAtMs: 200, - successorLatencyMs: 50, - }, - }, -]; - -const expected = new Map< - string, - { firstApplyMs: number | null; completions: number | null; starts: number | null } ->([ - ["S0_IMMEDIATE", { firstApplyMs: 0, completions: 1, starts: 1 }], - ["S1_BEFORE_150", { firstApplyMs: 100, completions: 1, starts: 1 }], - ["S2_AFTER_150", { firstApplyMs: 200, completions: 1, starts: 1 }], - ["S3_AFTER_500", { firstApplyMs: 700, completions: 1, starts: 1 }], - ["S4_OVERLAP", { firstApplyMs: 700, completions: 1, starts: 1 }], - ["S5_REJECT_RECOVER", { firstApplyMs: 750, completions: 2, starts: 2 }], - ["S6_DISPOSED", { firstApplyMs: null, completions: null, starts: null }], - ["S7_SUCCESSOR_LATE", { firstApplyMs: 50, completions: 1, starts: 1 }], -]); - -const queuedSuccessScenarios = new Set(["S2_AFTER_150", "S3_AFTER_500", "S4_OVERLAP"]); - -const results: FirstQuotaScenarioResult[] = []; - -function expectedAggregate(value: number | null) { - if (value === null) { - return { - min: null, - median: null, - p95: null, - max: null, - censored: FIRST_QUOTA_RECORDED_SAMPLES, - }; - } - - return { min: value, median: value, p95: value, max: value, censored: 0 }; -} - -function flushPromises(): Promise { - return Promise.resolve() - .then(() => undefined) - .then(() => undefined) - .then(() => undefined); -} - -beforeEach(() => { - vi.useFakeTimers(); -}); - -afterEach(() => { - vi.clearAllTimers(); -}); - -afterAll(() => { - if (process.env.OPENCODE_QUOTA_TUI_FIRST_QUOTA_REPORT === "1") { - console.log(`\n${formatFirstQuotaReport(results)}`); - } - vi.useRealTimers(); -}); - -describe("TUI first accepted quota lifecycle baseline", () => { - for (const scenario of scenarios) { - it(`${scenario.id}: ${scenario.description}`, async () => { - const result = await runFirstQuotaScenarioSamples(createTuiRefreshLifecycle, scenario); - const scenarioExpected = expected.get(scenario.id); - if (!scenarioExpected) throw new Error(`Missing expectation for ${scenario.id}`); - - expect(result.firstApplyMs).toEqual(expectedAggregate(scenarioExpected.firstApplyMs)); - expect(result.loaderCompletions).toEqual(expectedAggregate(scenarioExpected.completions)); - expect(result.loaderStarts).toEqual(expectedAggregate(scenarioExpected.starts)); - - for (const sample of result.samples) { - if (scenario.id === "S6_DISPOSED") { - expect(sample).toMatchObject({ - firstApplyMs: null, - loaderStartsAtFirstApply: null, - loaderCompletionsAtFirstApply: null, - acceptedLoaderOrdinal: null, - applyCount: 0, - afterApplyCount: 0, - loaderCompletionsTotal: 1, - unsubscribeCount: 1, - disposeCount: 1, - predecessorApplyCount: 0, - sinkValue: null, - censored: true, - }); - continue; - } - - const expectedOrdinal = scenarioExpected.starts; - const queuedSuccess = queuedSuccessScenarios.has(scenario.id); - expect(sample).toMatchObject({ - firstApplyMs: scenarioExpected.firstApplyMs, - loaderStartsAtFirstApply: scenarioExpected.starts, - loaderCompletionsAtFirstApply: scenarioExpected.completions, - acceptedLoaderOrdinal: expectedOrdinal, - applyCount: queuedSuccess ? 2 : 1, - afterApplyCount: queuedSuccess ? 2 : 1, - predecessorApplyCount: 0, - sinkValue: - scenario.id === "S7_SUCCESSOR_LATE" - ? "successor" - : `loader-${queuedSuccess ? 2 : expectedOrdinal}`, - censored: false, - }); - expect(sample.unsubscribeCount).toBe(scenario.id === "S7_SUCCESSOR_LATE" ? 2 : 1); - expect(sample.disposeCount).toBe(scenario.id === "S7_SUCCESSOR_LATE" ? 2 : 1); - } - - results.push(result); - }); - } - - it("keeps the active result when the queued follow-up rejects and recovers later", async () => { - let resolveInitial: ((value: string) => void) | undefined; - let rejectQueued: ((error: Error) => void) | undefined; - let resolveLater: ((value: string) => void) | undefined; - let loadCall = 0; - let sink: string | undefined; - const load = vi.fn(() => { - loadCall += 1; - if (loadCall === 1) { - return new Promise((resolve) => { - resolveInitial = resolve; - }); - } - if (loadCall === 2) { - return new Promise((_resolve, reject) => { - rejectQueued = reject; - }); - } - return new Promise((resolve) => { - resolveLater = resolve; - }); - }); - const apply = vi.fn((value: string) => { - sink = value; - }); - const afterApply = vi.fn(); - const lifecycle = createTuiRefreshLifecycle({ - load, - apply, - afterApply, - intervalMs: 60_000, - eventRefreshDelaysMs: [], - subscribe: () => [], - onDispose: vi.fn(), - }); - lifecycle.retain(); - - lifecycle.reload(); - resolveInitial?.("initial"); - await flushPromises(); - - expect(load).toHaveBeenCalledTimes(2); - expect(sink).toBe("initial"); - expect(apply).toHaveBeenCalledOnce(); - expect(afterApply).toHaveBeenCalledOnce(); - - rejectQueued?.(new Error("queued refresh failed")); - await flushPromises(); - - expect(sink).toBe("initial"); - expect(apply).toHaveBeenCalledOnce(); - expect(afterApply).toHaveBeenCalledOnce(); - - lifecycle.reload(); - expect(load).toHaveBeenCalledTimes(3); - resolveLater?.("recovered"); - await flushPromises(); - - expect(sink).toBe("recovered"); - expect(apply).toHaveBeenCalledTimes(2); - expect(afterApply).toHaveBeenCalledTimes(2); - lifecycle.release(); - }); - - it("starts immediately, polls at 60000 ms, and stops polling after final release", async () => { - const load = vi.fn(async () => "quota"); - const apply = vi.fn(); - const unsubscribe = vi.fn(); - const onDispose = vi.fn(); - const lifecycle = createTuiRefreshLifecycle({ - load, - apply, - intervalMs: 60_000, - eventRefreshDelaysMs: [], - subscribe: () => [unsubscribe], - onDispose, - }); - lifecycle.retain(); - - expect(load).toHaveBeenCalledOnce(); - await flushPromises(); - expect(apply).toHaveBeenCalledOnce(); - - await vi.advanceTimersByTimeAsync(59_999); - expect(load).toHaveBeenCalledOnce(); - await vi.advanceTimersByTimeAsync(1); - expect(load).toHaveBeenCalledTimes(2); - - lifecycle.release(); - await vi.advanceTimersByTimeAsync(60_000); - expect(load).toHaveBeenCalledTimes(2); - expect(unsubscribe).toHaveBeenCalledOnce(); - expect(onDispose).toHaveBeenCalledOnce(); - }); - - it("keeps a retained lifecycle alive and suppresses late work after final release", async () => { - let scheduleRefresh: (() => void) | undefined; - let resolveLoad: ((value: string) => void) | undefined; - const load = vi.fn( - () => - new Promise((resolve) => { - resolveLoad = resolve; - }), - ); - const apply = vi.fn(); - const afterApply = vi.fn(); - const unsubscribe = vi.fn(); - const onDispose = vi.fn(); - const lifecycle = createTuiRefreshLifecycle({ - load, - apply, - afterApply, - intervalMs: 60_000, - eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS, - recoveryDelaysMs: RECOVERY_DELAYS_MS, - subscribe: (receivedScheduleRefresh) => { - scheduleRefresh = receivedScheduleRefresh; - return [unsubscribe]; - }, - onDispose, - }); - lifecycle.retain(); - lifecycle.retain(); - - lifecycle.release(); - scheduleRefresh?.(); - await vi.advanceTimersByTimeAsync(600); - expect(load).toHaveBeenCalledOnce(); - expect(unsubscribe).not.toHaveBeenCalled(); - - lifecycle.release(); - resolveLoad?.("late quota"); - await flushPromises(); - await vi.advanceTimersByTimeAsync(60_000); - - expect(load).toHaveBeenCalledOnce(); - expect(apply).not.toHaveBeenCalled(); - expect(afterApply).not.toHaveBeenCalled(); - expect(unsubscribe).toHaveBeenCalledOnce(); - expect(onDispose).toHaveBeenCalledOnce(); - }); -}); diff --git a/tests/tui-line-style.test.ts b/tests/tui-line-style.test.ts deleted file mode 100644 index e341fb03..00000000 --- a/tests/tui-line-style.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { SESSION_TOKEN_SECTION_HEADING } from "../src/lib/session-tokens-format.js"; -import { getSidebarBodyLineColor } from "../src/lib/tui-line-style.js"; - -describe("getSidebarBodyLineColor", () => { - const theme = { - text: "white", - textMuted: "gray", - }; - - it("uses normal text color for the sidebar session-token heading", () => { - expect(getSidebarBodyLineColor(SESSION_TOKEN_SECTION_HEADING, theme)).toBe("white"); - }); - - it("keeps the heading highlighted when the rendered sidebar heading is width-clamped", () => { - expect(getSidebarBodyLineColor(SESSION_TOKEN_SECTION_HEADING.slice(0, 18), theme)).toBe( - "white", - ); - }); - - it("keeps non-heading sidebar lines muted", () => { - expect(getSidebarBodyLineColor("Unavailable", theme)).toBe("gray"); - }); -}); diff --git a/tests/tui-provider-availability-timing.test.ts b/tests/tui-provider-availability-timing.test.ts deleted file mode 100644 index f02894c9..00000000 --- a/tests/tui-provider-availability-timing.test.ts +++ /dev/null @@ -1,596 +0,0 @@ -import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createLoadConfigMeta } from "../src/lib/config.js"; -import { resolveRuntimeContextRoots } from "../src/lib/config-file-utils.js"; -import type { QuotaProvider, QuotaProviderResult } from "../src/lib/entries.js"; -import type { MaintainerAnnouncement } from "../src/lib/maintainer-announcements.js"; -import { __resetQuotaStateForTests } from "../src/lib/quota-state.js"; -import { loadTuiHomeBottomStatus, type TuiInitialRuntimeSeed } from "../src/lib/tui-runtime.js"; -import { DEFAULT_CONFIG, type QuotaToastConfig } from "../src/lib/types.js"; -import { - formatProviderAvailabilityTimingReport, - getProviderAvailabilityJitterOffsetMs, - type ProviderAvailabilityTimingResult, - type ProviderAvailabilityTimingSample, - runProviderAvailabilityTimingSamples, - TUI_PROVIDER_AVAILABILITY_HORIZON_MS, -} from "./helpers/tui-provider-availability-timing.js"; - -const ORDERED_PROVIDER_IDS = ["cursor", "synthetic", "alibaba-coding-plan"] as const; -const AVAILABLE_PROVIDER_IDS = ["cursor", "synthetic"] as const; -const NOTICE_TEXT = "Notice: Maintainer announcement available. Run /quota_announcements."; -const COMPACT_TEXT = "Cursor timing 80% | Synthetic timing 60% | +1 issue"; -const COMBINED_OUTPUT = JSON.stringify({ - status: "ready", - announcementText: NOTICE_TEXT, - compact: { status: "ready", text: COMPACT_TEXT }, -}); -const COMPACT_ONLY_OUTPUT = JSON.stringify({ - status: "ready", - compact: { status: "ready", text: COMPACT_TEXT }, -}); -const ANNOUNCEMENT_ONLY_OUTPUT = JSON.stringify({ - status: "ready", - announcementText: NOTICE_TEXT, - compact: { status: "disabled" }, -}); - -const ANNOUNCEMENTS: readonly MaintainerAnnouncement[] = [ - { - id: "timing-available-provider", - message: "Timing fixture for an available provider.", - providerIds: ["cursor"], - }, - { - id: "timing-unavailable-provider", - message: "Timing fixture for an unavailable provider.", - providerIds: ["alibaba-coding-plan"], - }, -]; -const UNTARGETED_ANNOUNCEMENTS: readonly MaintainerAnnouncement[] = [ - { - id: "timing-untargeted", - message: "Timing fixture independent of provider availability.", - }, -]; -const MIXED_DELAY_ANNOUNCEMENTS: readonly MaintainerAnnouncement[] = [ - { - id: "timing-fast-target", - message: "Timing fixture for a fast targeted provider.", - providerIds: ["cursor"], - }, -]; - -const reportResults: ProviderAvailabilityTimingResult[] = []; - -function createConfig(params: { - announcementEnabled: boolean; - compactEnabled: boolean; -}): QuotaToastConfig { - return { - ...DEFAULT_CONFIG, - enabledProviders: [...ORDERED_PROVIDER_IDS], - minIntervalMs: 0, - showSessionTokens: false, - tuiSidebarPanel: { ...DEFAULT_CONFIG.tuiSidebarPanel }, - tuiCompactStatus: { - ...DEFAULT_CONFIG.tuiCompactStatus, - enabled: params.compactEnabled, - homeBottom: true, - suppressWhenNativeProviderQuota: false, - maxWidth: 96, - }, - maintainerAnnouncements: { - ...DEFAULT_CONFIG.maintainerAnnouncements, - enabled: params.announcementEnabled, - home: true, - }, - export: { ...DEFAULT_CONFIG.export, enabled: false }, - telemetry: { ...DEFAULT_CONFIG.telemetry, enabled: false }, - layout: { ...DEFAULT_CONFIG.layout }, - opencodeGoWindows: [...DEFAULT_CONFIG.opencodeGoWindows], - quotaProviders: [], - }; -} - -function createProviderResult(name: string, percentRemaining: number): QuotaProviderResult { - return { - attempted: true, - entries: [ - { - name, - percentRemaining, - accounting: { - resultType: "quota", - acquisitionMethod: "local_runtime_accounting", - ownership: "maintained", - authority: "provider_reported", - }, - }, - ], - errors: [], - }; -} - -function createProviders(params: { - controlledDelayMs: number; - controlledDelayByProvider?: Readonly>; - callsByProvider: Record; - fetchesByProvider: Record; -}): QuotaProvider[] { - const results: Record<(typeof AVAILABLE_PROVIDER_IDS)[number], QuotaProviderResult> = { - cursor: createProviderResult("Cursor timing", 80), - synthetic: createProviderResult("Synthetic timing", 60), - }; - - return ORDERED_PROVIDER_IDS.map( - (providerId): QuotaProvider => ({ - id: providerId, - isAvailable: async () => { - params.callsByProvider[providerId] = (params.callsByProvider[providerId] ?? 0) + 1; - await new Promise((resolve, reject) => { - setTimeout(() => { - if (providerId === "alibaba-coding-plan") { - reject(new Error("deterministic availability failure")); - return; - } - resolve(); - }, params.controlledDelayByProvider?.[providerId] ?? params.controlledDelayMs); - }); - return true; - }, - fetch: async () => { - params.fetchesByProvider[providerId] = (params.fetchesByProvider[providerId] ?? 0) + 1; - const result = results[providerId as (typeof AVAILABLE_PROVIDER_IDS)[number]]; - if (!result) throw new Error(`Unexpected fetch for unavailable provider ${providerId}`); - return result; - }, - }), - ); -} - -function createApi(providerIds: readonly string[] = ORDERED_PROVIDER_IDS) { - return { - state: { - provider: providerIds.map((id) => ({ id })), - path: { - worktree: process.cwd(), - directory: process.cwd(), - }, - }, - client: { - config: { - providers: vi.fn().mockResolvedValue({ - data: { providers: providerIds.map((id) => ({ id })) }, - }), - get: vi.fn().mockResolvedValue({ data: {} }), - }, - }, - }; -} - -async function runSample(params: { - scenario: string; - controlledDelayMs: number; - controlledDelayByProvider?: Readonly>; - announcementEnabled: boolean; - compactEnabled: boolean; - announcements?: readonly MaintainerAnnouncement[]; -}): Promise { - vi.clearAllTimers(); - vi.setSystemTime(0); - __resetQuotaStateForTests(); - - const callsByProvider: Record = {}; - const fetchesByProvider: Record = {}; - const providers = createProviders({ - controlledDelayMs: params.controlledDelayMs, - controlledDelayByProvider: params.controlledDelayByProvider, - callsByProvider, - fetchesByProvider, - }); - const api = createApi(); - const initialRuntimeSeed: TuiInitialRuntimeSeed = { - roots: resolveRuntimeContextRoots({ - worktreeRoot: process.cwd(), - activeDirectory: process.cwd(), - fallbackDirectory: process.cwd(), - }), - config: createConfig(params), - configMeta: createLoadConfigMeta(), - providers, - }; - - const startedAtMs = Date.now(); - const loadPromise = loadTuiHomeBottomStatus({ - api: api as never, - nowMs: 0, - announcements: params.announcements ?? ANNOUNCEMENTS, - initialRuntimeSeed, - }); - await vi.runAllTimersAsync(); - const state = await loadPromise; - const elapsedMs = Date.now() - startedAtMs; - const reachedHorizon = elapsedMs > TUI_PROVIDER_AVAILABILITY_HORIZON_MS; - const readyCompactMs = !reachedHorizon && state.compact.status === "ready" ? elapsedMs : null; - const homeCompletionMs = reachedHorizon ? null : elapsedMs; - - return { - scenario: params.scenario, - controlledDelayMs: params.controlledDelayMs, - readyCompactMs, - homeCompletionMs, - callsByProvider: { ...callsByProvider }, - totalAvailabilityCalls: Object.values(callsByProvider).reduce((sum, count) => sum + count, 0), - fetchesByProvider: { ...fetchesByProvider }, - totalFetches: Object.values(fetchesByProvider).reduce((sum, count) => sum + count, 0), - outputSignature: JSON.stringify(state), - censored: reachedHorizon, - }; -} - -function expectInvariantVectors(params: { - result: ProviderAvailabilityTimingResult; - callsByProvider: Readonly>; - fetchesByProvider: Readonly>; - outputSignature: string; -}): void { - expect(params.result.distinctCallCountVectors).toEqual([ - [ - ...ORDERED_PROVIDER_IDS.map( - (providerId) => `${providerId}=${params.callsByProvider[providerId] ?? 0}`, - ), - `total=${Object.values(params.callsByProvider).reduce((sum, count) => sum + count, 0)}`, - ].join(","), - ]); - expect(params.result.distinctFetchCountVectors).toEqual([ - [ - ...ORDERED_PROVIDER_IDS.map( - (providerId) => `${providerId}=${params.fetchesByProvider[providerId] ?? 0}`, - ), - `total=${Object.values(params.fetchesByProvider).reduce((sum, count) => sum + count, 0)}`, - ].join(","), - ]); - expect(params.result.distinctOutputSignatures).toEqual([params.outputSignature]); - expect(params.result.samples.every((sample) => !sample.censored)).toBe(true); -} - -beforeEach(() => { - vi.useFakeTimers(); -}); - -afterEach(() => { - vi.clearAllTimers(); - __resetQuotaStateForTests(); -}); - -afterAll(() => { - if (process.env.OPENCODE_QUOTA_TUI_PROVIDER_AVAILABILITY_REPORT === "1") { - console.log(`\n${formatProviderAvailabilityTimingReport(reportResults)}`); - } - vi.useRealTimers(); -}); - -describe("TUI home provider availability timing", () => { - it("measures the narrowed targeted path, controls, and deterministic jitter", async () => { - for (const controlledDelayMs of [25, 100]) { - const scenario = `combined-${controlledDelayMs}`; - const result = await runProviderAvailabilityTimingSamples({ - scenario, - delayDescription: `${controlledDelayMs} ms fixed`, - orderedProviderIds: ORDERED_PROVIDER_IDS, - runSample: () => - runSample({ - scenario, - controlledDelayMs, - announcementEnabled: true, - compactEnabled: true, - }), - }); - reportResults.push(result); - - expect(result.readyCompact).toEqual({ - min: controlledDelayMs * 2, - median: controlledDelayMs * 2, - p95: controlledDelayMs * 2, - max: controlledDelayMs * 2, - censored: 0, - }); - expect(result.homeCompletion).toEqual(result.readyCompact); - expectInvariantVectors({ - result, - callsByProvider: { cursor: 2, synthetic: 1, "alibaba-coding-plan": 2 }, - fetchesByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 0 }, - outputSignature: COMBINED_OUTPUT, - }); - } - - const jitterResult = await runProviderAvailabilityTimingSamples({ - scenario: "combined-jitter", - delayDescription: "25 + [0,3,7,1,5] ms", - orderedProviderIds: ORDERED_PROVIDER_IDS, - runSample: (sampleIndex) => { - const controlledDelayMs = 25 + getProviderAvailabilityJitterOffsetMs(sampleIndex); - return runSample({ - scenario: "combined-jitter", - controlledDelayMs, - announcementEnabled: true, - compactEnabled: true, - }); - }, - }); - reportResults.push(jitterResult); - expect(jitterResult.readyCompact).toEqual({ - min: 50, - median: 56, - p95: 64, - max: 64, - censored: 0, - }); - expectInvariantVectors({ - result: jitterResult, - callsByProvider: { cursor: 2, synthetic: 1, "alibaba-coding-plan": 2 }, - fetchesByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 0 }, - outputSignature: COMBINED_OUTPUT, - }); - - const compactOnlyResult = await runProviderAvailabilityTimingSamples({ - scenario: "compact-only", - delayDescription: "25 ms fixed", - orderedProviderIds: ORDERED_PROVIDER_IDS, - runSample: () => - runSample({ - scenario: "compact-only", - controlledDelayMs: 25, - announcementEnabled: false, - compactEnabled: true, - }), - }); - reportResults.push(compactOnlyResult); - expect(compactOnlyResult.readyCompact).toEqual({ - min: 25, - median: 25, - p95: 25, - max: 25, - censored: 0, - }); - expectInvariantVectors({ - result: compactOnlyResult, - callsByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 1 }, - fetchesByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 0 }, - outputSignature: COMPACT_ONLY_OUTPUT, - }); - - const announcementOnlyResult = await runProviderAvailabilityTimingSamples({ - scenario: "announcement-only", - delayDescription: "25 ms fixed", - orderedProviderIds: ORDERED_PROVIDER_IDS, - runSample: () => - runSample({ - scenario: "announcement-only", - controlledDelayMs: 25, - announcementEnabled: true, - compactEnabled: false, - }), - }); - reportResults.push(announcementOnlyResult); - expect(announcementOnlyResult.readyCompact).toEqual({ - min: null, - median: null, - p95: null, - max: null, - censored: 30, - }); - expect(announcementOnlyResult.homeCompletion).toEqual({ - min: 25, - median: 25, - p95: 25, - max: 25, - censored: 0, - }); - expectInvariantVectors({ - result: announcementOnlyResult, - callsByProvider: { cursor: 1, synthetic: 0, "alibaba-coding-plan": 1 }, - fetchesByProvider: { cursor: 0, synthetic: 0, "alibaba-coding-plan": 0 }, - outputSignature: ANNOUNCEMENT_ONLY_OUTPUT, - }); - }); - - it("measures untargeted and mixed-delay combined baselines", async () => { - const untargetedFixedResult = await runProviderAvailabilityTimingSamples({ - scenario: "untargeted-combined-25", - delayDescription: "25 ms fixed", - orderedProviderIds: ORDERED_PROVIDER_IDS, - runSample: () => - runSample({ - scenario: "untargeted-combined-25", - controlledDelayMs: 25, - announcementEnabled: true, - compactEnabled: true, - announcements: UNTARGETED_ANNOUNCEMENTS, - }), - }); - reportResults.push(untargetedFixedResult); - expect(untargetedFixedResult.readyCompact).toEqual({ - min: 25, - median: 25, - p95: 25, - max: 25, - censored: 0, - }); - expectInvariantVectors({ - result: untargetedFixedResult, - callsByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 1 }, - fetchesByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 0 }, - outputSignature: COMBINED_OUTPUT, - }); - - const untargetedJitterResult = await runProviderAvailabilityTimingSamples({ - scenario: "untargeted-combined-jitter", - delayDescription: "25 + [0,3,7,1,5] ms", - orderedProviderIds: ORDERED_PROVIDER_IDS, - runSample: (sampleIndex) => { - const controlledDelayMs = 25 + getProviderAvailabilityJitterOffsetMs(sampleIndex); - return runSample({ - scenario: "untargeted-combined-jitter", - controlledDelayMs, - announcementEnabled: true, - compactEnabled: true, - announcements: UNTARGETED_ANNOUNCEMENTS, - }); - }, - }); - reportResults.push(untargetedJitterResult); - expect(untargetedJitterResult.readyCompact).toEqual({ - min: 25, - median: 28, - p95: 32, - max: 32, - censored: 0, - }); - expectInvariantVectors({ - result: untargetedJitterResult, - callsByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 1 }, - fetchesByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 0 }, - outputSignature: COMBINED_OUTPUT, - }); - - const mixedDelayResult = await runProviderAvailabilityTimingSamples({ - scenario: "mixed-delay-combined", - delayDescription: "cursor=10,synthetic=100,alibaba-coding-plan=40 ms", - orderedProviderIds: ORDERED_PROVIDER_IDS, - runSample: () => - runSample({ - scenario: "mixed-delay-combined", - controlledDelayMs: 100, - controlledDelayByProvider: { - cursor: 10, - synthetic: 100, - "alibaba-coding-plan": 40, - }, - announcementEnabled: true, - compactEnabled: true, - announcements: MIXED_DELAY_ANNOUNCEMENTS, - }), - }); - reportResults.push(mixedDelayResult); - expect(mixedDelayResult.readyCompact).toEqual({ - min: 110, - median: 110, - p95: 110, - max: 110, - censored: 0, - }); - expectInvariantVectors({ - result: mixedDelayResult, - callsByProvider: { cursor: 2, synthetic: 1, "alibaba-coding-plan": 1 }, - fetchesByProvider: { cursor: 1, synthetic: 1, "alibaba-coding-plan": 0 }, - outputSignature: COMBINED_OUTPUT, - }); - }); - - it("narrows announcement probes in runtime order and performs fresh checks on later loads", async () => { - const probeOrder: string[] = []; - const providers: QuotaProvider[] = ORDERED_PROVIDER_IDS.map((providerId) => ({ - id: providerId, - isAvailable: async () => { - probeOrder.push(providerId); - if (providerId === "alibaba-coding-plan") { - throw new Error("deterministic announcement availability failure"); - } - return true; - }, - fetch: async () => { - throw new Error("announcement-only loads must not fetch quota"); - }, - })); - const api = createApi(); - const initialRuntimeSeed: TuiInitialRuntimeSeed = { - roots: resolveRuntimeContextRoots({ - worktreeRoot: process.cwd(), - activeDirectory: process.cwd(), - fallbackDirectory: process.cwd(), - }), - config: createConfig({ announcementEnabled: true, compactEnabled: false }), - configMeta: createLoadConfigMeta(), - providers, - }; - const announcements: readonly MaintainerAnnouncement[] = [ - { - id: "reverse-target-order", - message: "Provider order must follow runtime order.", - providerIds: ["alibaba-coding-plan", "cursor"], - }, - ]; - - const first = await loadTuiHomeBottomStatus({ - api: api as never, - announcements, - initialRuntimeSeed, - }); - const second = await loadTuiHomeBottomStatus({ - api: api as never, - announcements, - initialRuntimeSeed, - }); - - expect(first).toEqual({ - status: "ready", - announcementText: NOTICE_TEXT, - compact: { status: "disabled" }, - }); - expect(second).toEqual(first); - expect(probeOrder).toEqual(["cursor", "alibaba-coding-plan", "cursor", "alibaba-coding-plan"]); - }); - - it("keeps legal quota-providers announcement and compact observations context-sensitive", async () => { - const observedOnlyCurrentModel: boolean[] = []; - let fetchCount = 0; - const provider: QuotaProvider = { - id: "quota-providers", - isAvailable: async (ctx) => { - observedOnlyCurrentModel.push(ctx.config.onlyCurrentModel); - return !ctx.config.onlyCurrentModel; - }, - fetch: async () => { - fetchCount += 1; - return createProviderResult("Custom timing", 70); - }, - }; - const config: QuotaToastConfig = { - ...createConfig({ announcementEnabled: true, compactEnabled: true }), - enabledProviders: ["quota-providers"], - onlyCurrentModel: true, - }; - const api = createApi(["quota-providers"]); - const initialRuntimeSeed: TuiInitialRuntimeSeed = { - roots: resolveRuntimeContextRoots({ - worktreeRoot: process.cwd(), - activeDirectory: process.cwd(), - fallbackDirectory: process.cwd(), - }), - config, - configMeta: createLoadConfigMeta(), - providers: [provider], - }; - - const state = await loadTuiHomeBottomStatus({ - api: api as never, - announcements: [ - { - id: "custom-provider-context", - message: "Context-sensitive aggregate provider announcement.", - providerIds: ["quota-providers"], - }, - ], - initialRuntimeSeed, - }); - - expect(observedOnlyCurrentModel).toEqual([true, false]); - expect(fetchCount).toBe(1); - expect(state.announcementText).toBeUndefined(); - expect(state).toMatchObject({ - status: "ready", - compact: { status: "ready" }, - }); - }); -}); diff --git a/tests/tui-runtime.test.ts b/tests/tui-runtime.test.ts deleted file mode 100644 index 2f08632c..00000000 --- a/tests/tui-runtime.test.ts +++ /dev/null @@ -1,3153 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const { collectQuotaRenderData, buildCompactQuotaStatusLine, buildSidebarQuotaPanelLines } = - vi.hoisted(() => ({ - collectQuotaRenderData: vi.fn(), - buildCompactQuotaStatusLine: vi.fn(), - buildSidebarQuotaPanelLines: vi.fn(), - })); - -const { buildQuotaExport: mockBuildQuotaExport, writeQuotaExport: mockWriteQuotaExport } = - vi.hoisted(() => ({ - buildQuotaExport: vi.fn(), - writeQuotaExport: vi.fn(), - })); - -vi.mock("../src/lib/quota-render-data.js", async () => { - const actual = await vi.importActual( - "../src/lib/quota-render-data.js", - ); - return { - ...actual, - collectQuotaRenderData, - }; -}); - -vi.mock("../src/lib/tui-sidebar-format.js", async () => { - const actual = await vi.importActual( - "../src/lib/tui-sidebar-format.js", - ); - return { - ...actual, - buildSidebarQuotaPanelLines, - }; -}); - -vi.mock("../src/lib/tui-compact-format.js", async () => { - const actual = await vi.importActual( - "../src/lib/tui-compact-format.js", - ); - return { - ...actual, - buildCompactQuotaStatusLine, - }; -}); - -vi.mock("../src/lib/quota-export.js", async () => { - const actual = await vi.importActual( - "../src/lib/quota-export.js", - ); - return { - ...actual, - buildQuotaExport: mockBuildQuotaExport, - writeQuotaExport: mockWriteQuotaExport, - }; -}); - -import { - getTuiSessionModelMeta, - loadTuiHomeBottomStatus, - loadTuiHomeCompactStatus, - loadTuiSessionQuotaSurfaces, - normalizeTuiSessionID, - resolveTuiSurfaceRegistration, - resolveWorkspaceDir, - type TuiInitialRuntimeSeed, - writeTuiQuotaExportIfEnabled, -} from "../src/lib/tui-runtime.js"; - -async function loadSidebarPanel(params: Parameters[0]) { - return (await loadTuiSessionQuotaSurfaces(params)).sidebar; -} - -describe("tui runtime helpers", () => { - const originalCwd = process.cwd(); - const originalEnv = { ...process.env }; - let tempDir: string; - let worktreeDir: string; - let nestedDir: string; - let xdgConfigHome: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-tui-")); - worktreeDir = join(tempDir, "worktree"); - nestedDir = join(worktreeDir, "packages", "feature"); - xdgConfigHome = join(tempDir, "xdg-config"); - - mkdirSync(nestedDir, { recursive: true }); - mkdirSync(join(xdgConfigHome, "opencode"), { recursive: true }); - - process.env.HOME = tempDir; - process.env.XDG_CONFIG_HOME = xdgConfigHome; - process.env.XDG_DATA_HOME = join(tempDir, "xdg-data"); - process.env.XDG_CACHE_HOME = join(tempDir, "xdg-cache"); - process.env.XDG_STATE_HOME = join(tempDir, "xdg-state"); - delete process.env.OPENCODE_CONFIG_DIR; - - collectQuotaRenderData.mockReset(); - buildCompactQuotaStatusLine.mockReset(); - buildSidebarQuotaPanelLines.mockReset(); - }); - - afterEach(() => { - process.chdir(originalCwd); - process.env = originalEnv; - rmSync(tempDir, { recursive: true, force: true }); - }); - - it("prefers the worktree root over the active directory for config lookup", () => { - expect( - resolveWorkspaceDir({ - state: { - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - }, - } as any), - ).toBe(worktreeDir); - }); - - it("still uses worktree root when process.cwd() differs from the active nested directory", async () => { - process.chdir(tempDir); - - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: false, - }, - }, - }), - "utf8", - ); - - writeFileSync( - join(nestedDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - }, - }, - }), - "utf8", - ); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "session-worktree-over-cwd", - }); - - expect(panel).toEqual({ status: "disabled", lines: [] }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("falls back to the active directory when no worktree root is available", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - }, - }, - }), - "utf8", - ); - - writeFileSync( - join(nestedDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: false, - }, - }, - }), - "utf8", - ); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: undefined, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "session-no-worktree", - }); - - expect(panel).toEqual({ status: "disabled", lines: [] }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("loads sidebar config from the worktree root when the active directory is nested", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: false, - }, - }, - }), - "utf8", - ); - - writeFileSync( - join(nestedDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - }, - }, - }), - "utf8", - ); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "session-1", - }); - - expect(panel).toEqual({ status: "disabled", lines: [] }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("honors sdk-backed quota config fallback when no config files are present", async () => { - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - config: { - get: vi.fn().mockResolvedValue({ - data: { - experimental: { - quotaToast: { - enabled: false, - }, - }, - }, - }), - }, - }, - } as any, - sessionID: "session-sdk-fallback", - }); - - expect(panel).toEqual({ status: "disabled", lines: [] }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("preserves sdk-backed quota config fields when no config files are present", async () => { - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: { - entries: [], - errors: [], - sessionTokens: undefined, - }, - }); - buildSidebarQuotaPanelLines.mockReturnValue(["Quota line"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - config: { - get: vi.fn().mockResolvedValue({ - data: { - experimental: { - quotaToast: { - enabled: true, - formatStyle: "grouped", - percentDisplayMode: "used", - onlyCurrentModel: true, - }, - }, - }, - }), - }, - session: { - get: vi.fn().mockResolvedValue({ - data: { - model: { providerID: "copilot", id: "gpt-4.1" }, - }, - }), - }, - }, - } as any, - sessionID: "session-sdk-fields", - }); - - expect(panel).toEqual({ status: "ready", lines: ["Quota line"] }); - expect(collectQuotaRenderData).toHaveBeenCalledWith( - expect.objectContaining({ - config: expect.objectContaining({ - formatStyle: "allWindows", - percentDisplayMode: "used", - onlyCurrentModel: true, - }), - formatStyle: "allWindows", - request: expect.objectContaining({ - sessionMeta: { - providerID: "copilot", - modelID: "gpt-4.1", - }, - }), - }), - ); - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith({ - data: { - entries: [], - errors: [], - sessionTokens: undefined, - }, - config: expect.objectContaining({ - formatStyle: "allWindows", - percentDisplayMode: "used", - onlyCurrentModel: true, - }), - }); - }); - - it("keeps the sidebar enabled when enableToast is false", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - enableToast: false, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: { - entries: [], - errors: [], - sessionTokens: undefined, - }, - }); - buildSidebarQuotaPanelLines.mockReturnValue(["Quota line"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "session-2", - }); - - expect(panel).toEqual({ status: "ready", lines: ["Quota line"] }); - expect(collectQuotaRenderData).toHaveBeenCalledOnce(); - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledOnce(); - }); - - it("shows sidebar loading instead of bare unavailable while onlyCurrentModel waits for session metadata", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - enabledProviders: ["copilot"], - onlyCurrentModel: true, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ - selection: { - waitingForCurrentSelection: true, - }, - active: [], - data: null, - }); - buildSidebarQuotaPanelLines.mockReturnValue(["Quota line"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - session: { - get: vi.fn().mockResolvedValue({ data: {} }), - }, - }, - } as any, - sessionID: "fresh-session", - }); - - expect(panel).toEqual({ status: "loading", lines: [] }); - expect(collectQuotaRenderData).toHaveBeenCalledWith( - expect.objectContaining({ - request: expect.objectContaining({ - sessionMeta: {}, - }), - }), - ); - expect(buildSidebarQuotaPanelLines).not.toHaveBeenCalled(); - }); - - it("preserves canonical all-window formatStyle through sidebar runtime collection and formatting", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "allWindows", - }, - }, - }), - "utf8", - ); - - const data = { - entries: [ - { - name: "Copilot", - group: "Copilot (business)", - label: "Usage:", - kind: "value", - value: "9 used | 2026-01 | org=acme-corp", - resetTimeIso: "2026-01-16T00:00:00.000Z", - }, - ], - errors: [], - sessionTokens: undefined, - }; - - collectQuotaRenderData.mockResolvedValue({ active: [], data }); - buildSidebarQuotaPanelLines.mockReturnValue(["[Copilot] (business)"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "session-grouped", - }); - - expect(panel).toEqual({ status: "ready", lines: ["[Copilot] (business)"], providerCount: 1 }); - expect(collectQuotaRenderData).toHaveBeenCalledWith( - expect.objectContaining({ - formatStyle: "allWindows", - }), - ); - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith({ - data, - config: expect.objectContaining({ - formatStyle: "allWindows", - }), - }); - }); - - it("forwards weekly grouped row data unchanged from render-data to sidebar formatter", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "allWindows", - percentDisplayMode: "used", - }, - }, - }), - "utf8", - ); - - const weeklyData = { - entries: [ - { - name: "Synthetic Weekly", - group: "Synthetic", - label: "Weekly:", - percentRemaining: 8, - right: "$22/$24", - resetTimeIso: "2099-01-01T00:00:00.000Z", - }, - ], - errors: [], - sessionTokens: undefined, - }; - - collectQuotaRenderData.mockResolvedValue({ active: [], data: weeklyData }); - buildSidebarQuotaPanelLines.mockReturnValue(["[Synthetic]", "Weekly window"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "session-weekly-grouped", - }); - - expect(panel).toEqual({ - status: "ready", - lines: ["[Synthetic]", "Weekly window"], - providerCount: 1, - }); - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith({ - data: weeklyData, - config: expect.objectContaining({ - formatStyle: "allWindows", - percentDisplayMode: "used", - }), - }); - }); - - it("prefers api.client.config.providers over sidebar state providers", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - }, - }, - }), - "utf8", - ); - - const runtimeProviders = vi.fn().mockResolvedValue({ - data: { providers: [{ id: "copilot" }, { id: "openai" }] }, - }); - - collectQuotaRenderData.mockImplementation(async ({ client }) => { - const response = await client.config.providers(); - expect(response).toEqual({ - data: { providers: [{ id: "copilot" }, { id: "openai" }] }, - }); - return { - active: [], - data: { - entries: [], - errors: [], - sessionTokens: undefined, - }, - }; - }); - buildSidebarQuotaPanelLines.mockReturnValue(["Quota line"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [{ id: "stale-state-provider" }], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - config: { - providers: runtimeProviders, - }, - }, - } as any, - sessionID: "session-2b", - }); - - expect(panel).toEqual({ status: "ready", lines: ["Quota line"] }); - expect(runtimeProviders).toHaveBeenCalledOnce(); - }); - - it("normalizes placeholder TUI session route params as unavailable", () => { - expect(normalizeTuiSessionID("%7BsessionID%7D")).toBeUndefined(); - expect(normalizeTuiSessionID("{sessionID}")).toBeUndefined(); - expect(normalizeTuiSessionID(" session-3 ")).toBe("session-3"); - }); - - it("uses the TUI session.get parameter shape for session model metadata", async () => { - const sessionGet = vi.fn().mockResolvedValue({ - data: { model: { providerID: "openai", id: "gpt-5" } }, - }); - const messages = vi.fn(() => [ - { role: "assistant", providerID: "cursor", modelID: "claude-3.7-sonnet" }, - ]); - - const meta = await getTuiSessionModelMeta( - { - client: { - session: { - get: sessionGet, - }, - }, - state: { - session: { - messages, - }, - }, - } as any, - "session-3", - ); - - expect(sessionGet).toHaveBeenCalledWith({ sessionID: "session-3" }); - expect(messages).not.toHaveBeenCalled(); - expect(meta).toEqual({ - providerID: "openai", - modelID: "gpt-5", - }); - }); - - it("falls back to the client lookup when the TUI state accessor throws", async () => { - const stateGet = vi.fn(() => { - throw new Error("state is not hydrated"); - }); - const sessionGet = vi.fn().mockResolvedValue({ - data: { model: { providerID: "alibaba-coding-plan", id: "qwen3-coder-plus" } }, - }); - const messages = vi.fn(() => []); - - const meta = await getTuiSessionModelMeta( - { - client: { session: { get: sessionGet } }, - state: { session: { get: stateGet, messages } }, - } as any, - "session-throwing-state", - ); - - expect(stateGet).toHaveBeenCalledWith("session-throwing-state"); - expect(sessionGet).toHaveBeenCalledWith({ sessionID: "session-throwing-state" }); - expect(messages).not.toHaveBeenCalled(); - expect(meta).toEqual({ - providerID: "alibaba-coding-plan", - modelID: "qwen3-coder-plus", - }); - }); - - it("does not read obsolete top-level session model fields", async () => { - const sessionGet = vi.fn().mockResolvedValue({ - data: { providerID: "legacy-provider", modelID: "legacy-model" }, - }); - - const meta = await getTuiSessionModelMeta( - { - client: { session: { get: sessionGet } }, - state: { session: { messages: () => [] } }, - } as any, - "session-legacy", - ); - - expect(meta).toEqual({}); - }); - - it("does not call session APIs for placeholder TUI session IDs", async () => { - const stateGet = vi.fn(() => { - throw new Error("should not be called"); - }); - const sessionGet = vi.fn().mockRejectedValue(new Error("should not be called")); - const messages = vi.fn(() => []); - - const meta = await getTuiSessionModelMeta( - { - client: { - session: { - get: sessionGet, - }, - }, - state: { - session: { - get: stateGet, - messages, - }, - }, - } as any, - "%7BsessionID%7D", - ); - - expect(meta).toEqual({}); - expect(stateGet).not.toHaveBeenCalled(); - expect(sessionGet).not.toHaveBeenCalled(); - expect(messages).not.toHaveBeenCalled(); - }); - - it("falls back to session messages when session.get fails under onlyCurrentModel", async () => { - const sessionGet = vi.fn().mockRejectedValue(new Error("boom")); - - const meta = await getTuiSessionModelMeta( - { - client: { - session: { - get: sessionGet, - }, - }, - state: { - session: { - messages: () => [ - { role: "assistant", providerID: "openai", modelID: "gpt-4.1" }, - { - role: "user", - model: { providerID: "cursor", modelID: "claude-3.7-sonnet" }, - }, - ], - }, - }, - } as any, - "session-3", - ); - - expect(sessionGet).toHaveBeenCalledWith({ sessionID: "session-3" }); - expect(meta).toEqual({ - providerID: "cursor", - modelID: "claude-3.7-sonnet", - }); - }); - - it("keeps surface registration pending until deferred SDK config resolves", async () => { - let resolveSdkConfig!: (value: { data: Record }) => void; - const sdkConfig = new Promise<{ data: Record }>((resolve) => { - resolveSdkConfig = resolve; - }); - const getConfig = vi.fn(() => sdkConfig); - - let settled = false; - const registrationPromise = resolveTuiSurfaceRegistration({ - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - config: { - get: getConfig, - }, - }, - } as any); - void registrationPromise.then(() => { - settled = true; - }); - - await Promise.resolve(); - await Promise.resolve(); - - expect(getConfig).toHaveBeenCalledOnce(); - expect(settled).toBe(false); - - resolveSdkConfig({ - data: { - experimental: { - quotaToast: { - enabled: true, - tuiCommandDisplay: "dialog", - tuiSidebarPanel: { - enabled: true, - }, - tuiCompactStatus: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - suppressWhenNativeProviderQuota: true, - }, - tuiPromptBar: { enabled: true }, - maintainerAnnouncements: { - home: false, - }, - }, - }, - }, - }); - - await expect(registrationPromise).resolves.toEqual({ - commandDisplay: "dialog", - sidebar: { - enabled: true, - }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { - enabled: true, - }, - announcements: { - homeBottom: false, - }, - homeBottom: true, - }); - expect(settled).toBe(true); - }); - - it("resolves compact registration and suppresses native provider quota clients", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - suppressWhenNativeProviderQuota: true, - }, - }, - }, - }), - "utf8", - ); - - const registration = ( - await resolveTuiSurfaceRegistration({ - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - experimental: { - providerQuota: {}, - }, - }, - } as any) - ).compact; - - expect(registration).toEqual({ - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: true, - suppressedByNativeProviderQuota: true, - }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("resolves sidebar independently from compact native-provider suppression", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCommandDisplay: "dialog", - tuiSidebarPanel: { - enabled: true, - }, - tuiCompactStatus: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - suppressWhenNativeProviderQuota: true, - }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - const registration = await resolveTuiSurfaceRegistration({ - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - experimental: { - providerQuota: {}, - }, - }, - } as any); - - expect(registration).toEqual({ - commandDisplay: "dialog", - sidebar: { - enabled: true, - }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: true, - suppressedByNativeProviderQuota: true, - }, - promptBar: { - enabled: true, - }, - announcements: { - homeBottom: true, - }, - homeBottom: true, - }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("registers home bottom when export is enabled even without visible home content", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - export: { enabled: true }, - maintainerAnnouncements: { home: false }, - tuiCompactStatus: { enabled: false, homeBottom: false }, - }, - }, - }), - "utf8", - ); - - const registration = await resolveTuiSurfaceRegistration({ - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any); - - expect(registration.homeBottom).toBe(true); - expect(registration.compact.homeBottom).toBe(false); - expect(registration.announcements.homeBottom).toBe(false); - }); - - it("keeps the shared registration snapshot unchanged across concurrent initial consumers", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - onlyCurrentModel: true, - percentDisplayMode: "used", - requestTimeoutMs: 12_345, - maintainerAnnouncements: { enabled: false, home: false }, - tuiSidebarPanel: { enabled: true }, - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - homeBottom: true, - maxWidth: 41, - }, - }, - }, - }), - "utf8", - ); - const sessionGet = vi.fn().mockResolvedValue({ - data: { model: { providerID: "copilot", id: "gpt-4.1" } }, - }); - const api = { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: { session: { get: sessionGet } }, - } as any; - let initialRuntimeSeed: TuiInitialRuntimeSeed | undefined; - - await resolveTuiSurfaceRegistration(api, { - captureInitialRuntime(seed) { - initialRuntimeSeed = seed; - }, - }); - expect(initialRuntimeSeed).toBeDefined(); - if (!initialRuntimeSeed) - throw new Error("registration did not capture an initial runtime seed"); - - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - onlyCurrentModel: false, - percentDisplayMode: "remaining", - tuiSidebarPanel: { enabled: true }, - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - homeBottom: true, - maxWidth: 42, - }, - }, - }, - }), - "utf8", - ); - const data = { entries: [], errors: [], sessionTokens: undefined }; - collectQuotaRenderData.mockResolvedValue({ active: [], data }); - buildSidebarQuotaPanelLines.mockReturnValue(["Sidebar quota"]); - buildCompactQuotaStatusLine.mockReturnValue("Compact quota"); - - const configBefore = initialRuntimeSeed.config; - const configMetaBefore = initialRuntimeSeed.configMeta; - const providersBefore = initialRuntimeSeed.providers; - const serializedSeedBefore = JSON.stringify(initialRuntimeSeed); - - await Promise.all([ - loadTuiSessionQuotaSurfaces({ - api, - sessionID: "seeded-session", - initialRuntimeSeed, - }), - loadTuiHomeBottomStatus({ api, initialRuntimeSeed }), - ]); - - expect(initialRuntimeSeed.config).toBe(configBefore); - expect(initialRuntimeSeed.configMeta).toBe(configMetaBefore); - expect(initialRuntimeSeed.providers).toBe(providersBefore); - expect(JSON.stringify(initialRuntimeSeed)).toBe(serializedSeedBefore); - expect(sessionGet).toHaveBeenCalledWith({ sessionID: "seeded-session" }); - - const runtimeCalls = collectQuotaRenderData.mock.calls.map(([params]) => params); - const sessionRuntimeCall = runtimeCalls.find( - (params) => params.request?.sessionID === "seeded-session", - ); - const homeRuntimeCall = runtimeCalls.find((params) => params.request?.sessionID === undefined); - expect(sessionRuntimeCall).toEqual( - expect.objectContaining({ - config: configBefore, - configMeta: configMetaBefore, - providers: providersBefore, - request: { - sessionID: "seeded-session", - sessionMeta: { providerID: "copilot", modelID: "gpt-4.1" }, - }, - }), - ); - expect(sessionRuntimeCall?.config).toBe(configBefore); - expect(sessionRuntimeCall?.configMeta).toBe(configMetaBefore); - expect(sessionRuntimeCall?.providers).toBe(providersBefore); - expect(sessionRuntimeCall?.config).toMatchObject({ - onlyCurrentModel: true, - percentDisplayMode: "used", - requestTimeoutMs: 12_345, - }); - expect(homeRuntimeCall?.configMeta).toBe(configMetaBefore); - expect(homeRuntimeCall?.providers).toBe(providersBefore); - expect(homeRuntimeCall).toEqual( - expect.objectContaining({ - config: expect.objectContaining({ - onlyCurrentModel: false, - showSessionTokens: false, - percentDisplayMode: "used", - }), - configMeta: configMetaBefore, - providers: providersBefore, - request: { sessionID: undefined, sessionMeta: undefined }, - }), - ); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledTimes(2); - for (const [params] of buildCompactQuotaStatusLine.mock.calls) { - expect(params).toEqual(expect.objectContaining({ maxWidth: 41 })); - } - }); - - it("discards a registration snapshot when current runtime roots differ", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - onlyCurrentModel: true, - tuiSidebarPanel: { enabled: true }, - tuiCompactStatus: { enabled: true, sessionPrompt: true, maxWidth: 41 }, - }, - }, - }), - "utf8", - ); - const sessionGet = vi.fn().mockResolvedValue({ data: {} }); - const api = { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: { session: { get: sessionGet } }, - } as any; - let initialRuntimeSeed: TuiInitialRuntimeSeed | undefined; - await resolveTuiSurfaceRegistration(api, { - captureInitialRuntime(seed) { - initialRuntimeSeed = seed; - }, - }); - if (!initialRuntimeSeed) - throw new Error("registration did not capture an initial runtime seed"); - - const nextWorktreeDir = join(tempDir, "next-worktree"); - mkdirSync(nextWorktreeDir, { recursive: true }); - writeFileSync( - join(nextWorktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - onlyCurrentModel: false, - percentDisplayMode: "remaining", - tuiSidebarPanel: { enabled: true }, - tuiCompactStatus: { enabled: true, sessionPrompt: true, maxWidth: 77 }, - }, - }, - }), - "utf8", - ); - api.state.path = { worktree: nextWorktreeDir, directory: nextWorktreeDir }; - const data = { entries: [], errors: [], sessionTokens: undefined }; - collectQuotaRenderData.mockResolvedValue({ active: [], data }); - buildSidebarQuotaPanelLines.mockReturnValue(["Sidebar quota"]); - buildCompactQuotaStatusLine.mockReturnValue("Compact quota"); - - await loadTuiSessionQuotaSurfaces({ - api, - sessionID: "root-mismatch", - initialRuntimeSeed, - }); - - expect(sessionGet).not.toHaveBeenCalled(); - expect(collectQuotaRenderData).toHaveBeenCalledWith( - expect.objectContaining({ - config: expect.objectContaining({ - onlyCurrentModel: false, - percentDisplayMode: "remaining", - }), - }), - ); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith( - expect.objectContaining({ maxWidth: 77 }), - ); - }); - - it("loads compact session surface while returning disabled sidebar when sidebar config is off", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - percentDisplayMode: "used", - tuiSidebarPanel: { - enabled: false, - }, - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - maxWidth: 42, - }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - const data = { - entries: [ - { - name: "Copilot 5h", - percentRemaining: 18, - }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ active: [], data }); - buildSidebarQuotaPanelLines.mockReturnValue(["Sidebar quota"]); - buildCompactQuotaStatusLine.mockReturnValue("Compact quota"); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "compact-sidebar-off", - }); - - expect(surfaces).toEqual({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "ready", text: "Compact quota" }, - promptBar: { - status: "ready", - entry: { identityLabel: "Copilot 5h", name: "Copilot 5h", percentRemaining: 18 }, - percentDisplayMode: "used", - resetTimeDecimals: undefined, - resetTimeSpaced: true, - }, - }); - expect(collectQuotaRenderData).toHaveBeenCalledOnce(); - expect(buildSidebarQuotaPanelLines).not.toHaveBeenCalled(); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith({ - data, - percentDisplayMode: "used", - accountingDetail: "summary", - resetTimeSpaced: true, - maxWidth: 42, - }); - }); - - it("skips session quota collection when sidebar and session compact are disabled", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiSidebarPanel: { - enabled: false, - }, - tuiCompactStatus: { - enabled: true, - sessionPrompt: false, - }, - tuiPromptBar: { - enabled: false, - }, - }, - }, - }), - "utf8", - ); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "all-session-surfaces-off", - }); - - expect(surfaces).toEqual({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { status: "disabled" }, - }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - expect(buildSidebarQuotaPanelLines).not.toHaveBeenCalled(); - expect(buildCompactQuotaStatusLine).not.toHaveBeenCalled(); - }); - - it("builds an opt-in prompt bar from the five-hour all-windows entry", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - percentDisplayMode: "used", - resetTimeDecimals: 2, - tuiSidebarPanel: { enabled: false }, - tuiCompactStatus: { enabled: false }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: { - entries: [{ name: "Copilot weekly", percentRemaining: 4 }], - errors: [], - sessionTokens: undefined, - }, - allWindowsData: { - entries: [ - { name: "Copilot weekly", percentRemaining: 4 }, - { name: "Copilot 5h", percentRemaining: 72, resetTimeIso: "2026-08-09T12:00:00Z" }, - ], - errors: [], - sessionTokens: undefined, - }, - }); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "prompt-bar-five-hour", - }); - - expect(surfaces).toEqual({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { - identityLabel: "Copilot 5h", - name: "Copilot 5h", - percentRemaining: 72, - resetTimeIso: "2026-08-09T12:00:00Z", - }, - percentDisplayMode: "used", - resetTimeDecimals: 2, - resetTimeSpaced: true, - }, - }); - expect(collectQuotaRenderData).toHaveBeenCalledOnce(); - }); - - it("falls back to the lowest finite prompt-bar percentage", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiSidebarPanel: { enabled: false }, - tuiCompactStatus: { enabled: false }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: { - entries: [ - { name: "Balance", value: "$10" }, - { name: "Copilot weekly", percentRemaining: 44 }, - { name: "Copilot monthly", percentRemaining: 18 }, - ], - errors: [], - sessionTokens: undefined, - }, - }); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "prompt-bar-lowest", - }); - - expect(surfaces.promptBar).toEqual({ - status: "ready", - entry: { - identityLabel: "Copilot Monthly", - name: "Copilot monthly", - percentRemaining: 18, - }, - percentDisplayMode: "remaining", - resetTimeDecimals: undefined, - resetTimeSpaced: true, - }); - }); - - it("prefers OpenCode Go 5h on the prompt bar when a sibling window is exhausted", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiSidebarPanel: { enabled: false }, - tuiCompactStatus: { enabled: false }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: { - entries: [ - { - name: "OpenCode Go 5h", - group: "OpenCode Go", - label: "5h:", - percentRemaining: 83, - }, - { - name: "OpenCode Go Weekly", - group: "OpenCode Go", - label: "Weekly:", - percentRemaining: 0, - }, - { - name: "OpenCode Go Monthly", - group: "OpenCode Go", - label: "Monthly:", - percentRemaining: 9, - }, - ], - errors: [], - sessionTokens: undefined, - }, - }); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "prompt-bar-opencode-go-exhausted", - }); - - expect(surfaces.promptBar).toEqual({ - status: "ready", - entry: { - identityLabel: "OpenCode Go 5h", - name: "OpenCode Go 5h", - group: "OpenCode Go", - label: "5h:", - percentRemaining: 83, - }, - percentDisplayMode: "remaining", - resetTimeDecimals: undefined, - resetTimeSpaced: true, - }); - }); - - it("selects the exhausted OpenCode Go window on the prompt bar when 5h is filtered out", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiSidebarPanel: { enabled: false }, - tuiCompactStatus: { enabled: false }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: { - entries: [ - { - name: "OpenCode Go Weekly", - group: "OpenCode Go", - label: "Weekly:", - percentRemaining: 0, - }, - { - name: "OpenCode Go Monthly", - group: "OpenCode Go", - label: "Monthly:", - percentRemaining: 9, - }, - ], - errors: [], - sessionTokens: undefined, - }, - }); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "prompt-bar-opencode-go-selected", - }); - - expect(surfaces.promptBar).toEqual({ - status: "ready", - entry: { - identityLabel: "OpenCode Go Weekly", - name: "OpenCode Go Weekly", - group: "OpenCode Go", - label: "Weekly:", - percentRemaining: 0, - }, - percentDisplayMode: "remaining", - resetTimeDecimals: undefined, - resetTimeSpaced: true, - }); - }); - - it.each([ - { - sessionID: "prompt-bar-balance", - accounting: { - resultType: "balance", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", - }, - metric: { kind: "component", component: "current_balance" }, - name: "account-balance", - group: "NanoGPT", - expected: "NanoGPT: Current balance USD 12.50", - }, - { - sessionID: "prompt-bar-spend", - accounting: { - resultType: "spend", - acquisitionMethod: "local_runtime_accounting", - ownership: "maintained", - authority: "locally_derived", - }, - metric: { kind: "named", name: "Known API" }, - name: "known-api-spend", - group: "Cursor", - expected: "Cursor: Known API spend USD 12.50", - }, - ] as const)("publishes a primary structured value for $sessionID", async (fixture) => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - accountingDetail: "detailed", - tuiSidebarPanel: { enabled: false }, - tuiCompactStatus: { enabled: false }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: { - entries: [ - { - kind: "quantity", - accounting: fixture.accounting, - name: fixture.name, - group: fixture.group, - semantic: { metric: fixture.metric, prominence: "primary" }, - quantity: { decimal: "12.5", unit: { kind: "currency", code: "USD" } }, - }, - ], - errors: [], - sessionTokens: undefined, - }, - }); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: fixture.sessionID, - }); - - expect(surfaces.promptBar).toEqual({ - status: "ready", - entry: { semanticSegment: fixture.expected }, - percentDisplayMode: "remaining", - resetTimeDecimals: undefined, - resetTimeSpaced: true, - }); - }); - - it("loads sidebar and compact session surfaces from one collection", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - percentDisplayMode: "used", - onlyCurrentModel: true, - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - maxWidth: 42, - }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - const data = { - entries: [ - { - name: "Copilot 5h", - percentRemaining: 18, - }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ active: [], data }); - buildSidebarQuotaPanelLines.mockReturnValue(["Sidebar quota"]); - buildCompactQuotaStatusLine.mockReturnValue("Compact quota"); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - session: { - get: vi.fn().mockResolvedValue({ - data: { - model: { providerID: "copilot", id: "gpt-4.1" }, - }, - }), - }, - }, - } as any, - sessionID: "compact-session", - }); - - expect(surfaces).toEqual({ - sidebar: { status: "ready", lines: ["Sidebar quota"], providerCount: 1 }, - compact: { status: "ready", text: "Compact quota" }, - promptBar: { - status: "ready", - entry: { identityLabel: "Copilot 5h", name: "Copilot 5h", percentRemaining: 18 }, - percentDisplayMode: "used", - resetTimeDecimals: undefined, - resetTimeSpaced: true, - }, - }); - expect(collectQuotaRenderData).toHaveBeenCalledOnce(); - expect(collectQuotaRenderData).toHaveBeenCalledWith( - expect.objectContaining({ - surfaceExplicitProviderIssues: true, - config: expect.objectContaining({ - onlyCurrentModel: true, - percentDisplayMode: "used", - }), - request: expect.objectContaining({ - sessionID: "compact-session", - sessionMeta: { - providerID: "copilot", - modelID: "gpt-4.1", - }, - }), - }), - ); - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith({ - data, - config: expect.objectContaining({ - percentDisplayMode: "used", - }), - }); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith({ - data, - percentDisplayMode: "used", - accountingDetail: "summary", - resetTimeSpaced: true, - maxWidth: 42, - }); - }); - - it.each([ - { label: "default spaced resets", resetTimeSpaced: undefined, expectedResetTimeSpaced: true }, - { - label: "the explicit dense-reset opt-out", - resetTimeSpaced: false, - expectedResetTimeSpaced: false, - }, - ])("forwards $label and bare labels to TUI quota displays", async ({ - resetTimeSpaced, - expectedResetTimeSpaced, - }) => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - percentDisplayMode: "used", - percentLabelStyle: "bare", - ...(resetTimeSpaced === undefined ? {} : { resetTimeSpaced }), - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - maxWidth: 42, - }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - const data = { - entries: [ - { - name: "Copilot 5h", - percentRemaining: 18, - resetTimeIso: "2026-01-15T13:45:00.000Z", - }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ active: [], data }); - buildSidebarQuotaPanelLines.mockReturnValue(["Sidebar quota"]); - buildCompactQuotaStatusLine.mockReturnValue("Compact quota"); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: `spacing-${expectedResetTimeSpaced}`, - }); - - expect(surfaces.sidebar).toEqual({ - status: "ready", - lines: ["Sidebar quota"], - headerPercentMode: "used", - providerCount: 1, - }); - expect(surfaces.promptBar).toMatchObject({ - status: "ready", - percentDisplayMode: "used", - resetTimeSpaced: expectedResetTimeSpaced, - }); - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith({ - data, - config: expect.objectContaining({ - percentLabelStyle: "bare", - resetTimeSpaced: expectedResetTimeSpaced, - }), - }); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith({ - data, - percentDisplayMode: "used", - accountingDetail: "summary", - resetTimeSpaced: expectedResetTimeSpaced, - maxWidth: 42, - }); - }); - - it("uses compact fallback text when session collection has no data", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ active: [], data: null }); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - sessionID: "compact-no-data", - }); - - expect(surfaces).toEqual({ - sidebar: { status: "ready", lines: [] }, - compact: { status: "ready", text: "Quota unavailable" }, - promptBar: { - status: "ready", - percentDisplayMode: "remaining", - resetTimeDecimals: undefined, - resetTimeSpaced: true, - }, - }); - expect(buildCompactQuotaStatusLine).not.toHaveBeenCalled(); - }); - - it("marks both session surfaces loading while waiting for current selection", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - onlyCurrentModel: true, - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - }, - tuiPromptBar: { enabled: true }, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderData.mockResolvedValue({ - selection: { - waitingForCurrentSelection: true, - }, - active: [], - data: null, - }); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: { - session: { - get: vi.fn().mockResolvedValue({ data: {} }), - }, - }, - } as any, - sessionID: "waiting-session", - }); - - expect(surfaces).toEqual({ - sidebar: { status: "loading", lines: [] }, - compact: { status: "loading" }, - promptBar: { status: "loading" }, - }); - expect(buildSidebarQuotaPanelLines).not.toHaveBeenCalled(); - expect(buildCompactQuotaStatusLine).not.toHaveBeenCalled(); - }); - - it("uses compact fallback text when home compact formatting returns empty", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: true, - homeBottom: true, - }, - }, - }, - }), - "utf8", - ); - - const data = { - entries: [], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ active: [], data }); - buildCompactQuotaStatusLine.mockReturnValue(""); - - const compact = await loadTuiHomeCompactStatus({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - }); - - expect(compact).toEqual({ status: "ready", text: "Quota unavailable" }); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith({ - data, - percentDisplayMode: "remaining", - accountingDetail: "summary", - resetTimeSpaced: true, - maxWidth: 96, - }); - }); - - it("loads announcement-only home bottom without fetching quota data", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: false, - homeBottom: false, - }, - maintainerAnnouncements: { - enabled: true, - home: true, - }, - }, - }, - }), - "utf8", - ); - - const bottom = await loadTuiHomeBottomStatus({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - nowMs: Date.parse("2026-05-21T12:00:00.000Z"), - announcements: [ - { - id: "copilot-credits", - message: "If you use Copilot, GitHub billing is moving to AI Credits.", - }, - ], - }); - - expect(bottom).toEqual({ - status: "ready", - announcementText: "Notice: Maintainer announcement available. Run /quota_announcements.", - compact: { status: "disabled" }, - }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("loads provider-targeted announcement-only home bottom for detected providers", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: false, - homeBottom: false, - }, - maintainerAnnouncements: { - enabled: true, - home: true, - }, - }, - }, - }), - "utf8", - ); - - const bottom = await loadTuiHomeBottomStatus({ - api: { - state: { - provider: [{ id: "copilot" }], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - nowMs: Date.parse("2026-05-21T12:00:00.000Z"), - announcements: [ - { - id: "copilot-credits", - message: "If you use Copilot, GitHub billing is moving to AI Credits.", - providerIds: ["copilot"], - }, - ], - }); - - expect(bottom).toEqual({ - status: "ready", - announcementText: "Notice: Maintainer announcement available. Run /quota_announcements.", - compact: { status: "disabled" }, - }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("does not render provider-targeted announcement-only home bottom without a detected provider", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: false, - homeBottom: false, - }, - maintainerAnnouncements: { - enabled: true, - home: true, - }, - }, - }, - }), - "utf8", - ); - - const bottom = await loadTuiHomeBottomStatus({ - api: { - state: { - provider: [{ id: "openai" }], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - nowMs: Date.parse("2026-05-21T12:00:00.000Z"), - announcements: [ - { - id: "copilot-credits", - message: "If you use Copilot, GitHub billing is moving to AI Credits.", - providerIds: ["copilot"], - }, - ], - }); - - expect(bottom).toEqual({ status: "disabled", compact: { status: "disabled" } }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("keeps the July 14 bundled Copilot announcement hidden for a nonmatching provider", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: false, - homeBottom: false, - }, - maintainerAnnouncements: { - enabled: true, - home: true, - }, - }, - }, - }), - "utf8", - ); - - const bottom = await loadTuiHomeBottomStatus({ - api: { - state: { - provider: [{ id: "openai" }], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - nowMs: Date.parse("2026-07-14T12:00:00.000Z"), - }); - - expect(bottom).toEqual({ status: "disabled", compact: { status: "disabled" } }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("loads announcement and compact quota in one home bottom state", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: true, - homeBottom: true, - }, - maintainerAnnouncements: { - enabled: true, - home: true, - }, - }, - }, - }), - "utf8", - ); - - const data = { - entries: [], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ active: [], data }); - buildCompactQuotaStatusLine.mockReturnValue("Home compact quota"); - - const bottom = await loadTuiHomeBottomStatus({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - nowMs: Date.parse("2026-05-21T12:00:00.000Z"), - announcements: [ - { - id: "copilot-credits", - message: "If you use Copilot, GitHub billing is moving to AI Credits.", - }, - ], - }); - - expect(bottom).toEqual({ - status: "ready", - announcementText: "Notice: Maintainer announcement available. Run /quota_announcements.", - compact: { status: "ready", text: "Home compact quota" }, - }); - expect(collectQuotaRenderData).toHaveBeenCalledOnce(); - }); - - it("does not render inactive announcement-only home bottom", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiCompactStatus: { - enabled: false, - homeBottom: false, - }, - maintainerAnnouncements: { - enabled: true, - home: true, - }, - }, - }, - }), - "utf8", - ); - - const bottom = await loadTuiHomeBottomStatus({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - nowMs: Date.parse("2026-05-21T12:00:00.000Z"), - announcements: [ - { - id: "copilot-credits", - message: "If you use Copilot, GitHub billing is moving to AI Credits.", - startsAt: "2026-06-01T00:00:00.000Z", - }, - ], - }); - - expect(bottom).toEqual({ status: "disabled", compact: { status: "disabled" } }); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); - }); - - it("loads home compact with an onlyCurrentModel false config copy", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - onlyCurrentModel: true, - showSessionTokens: true, - percentDisplayMode: "used", - tuiCompactStatus: { - enabled: true, - homeBottom: true, - maxWidth: 40, - }, - }, - }, - }), - "utf8", - ); - - const data = { - entries: [ - { - name: "Copilot 5h", - percentRemaining: 25, - }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockImplementation(async ({ config, request }) => { - expect(config).toEqual( - expect.objectContaining({ - onlyCurrentModel: false, - showSessionTokens: false, - percentDisplayMode: "used", - }), - ); - expect(request).toEqual({ - sessionID: undefined, - sessionMeta: undefined, - }); - return { data }; - }); - buildCompactQuotaStatusLine.mockReturnValue("Home compact quota"); - - const compact = await loadTuiHomeCompactStatus({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - }); - - expect(compact).toEqual({ status: "ready", text: "Home compact quota" }); - expect(collectQuotaRenderData).toHaveBeenCalledOnce(); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith({ - data, - percentDisplayMode: "used", - accountingDetail: "summary", - resetTimeSpaced: true, - maxWidth: 40, - }); - expect(buildSidebarQuotaPanelLines).not.toHaveBeenCalled(); - }); - - it("uses tuiCompactStatus.formatStyle override for home compact status", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "singleWindow", - tuiCompactStatus: { - enabled: true, - homeBottom: true, - formatStyle: "allWindows", - }, - }, - }, - }), - "utf8", - ); - - const singleWindowData = { - entries: [{ name: "Copilot", percentRemaining: 50 }], - errors: [], - sessionTokens: undefined, - }; - const allWindowsData = { - entries: [ - { name: "Copilot 5h", percentRemaining: 50 }, - { name: "Copilot Weekly", percentRemaining: 80 }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: singleWindowData, - allWindowsData, - }); - buildCompactQuotaStatusLine.mockReturnValue("50% 80%"); - - const compact = await loadTuiHomeCompactStatus({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - }); - - expect(compact).toEqual({ status: "ready", text: "50% 80%" }); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith( - expect.objectContaining({ data: allWindowsData }), - ); - }); - - it("uses tuiCompactStatus.formatStyle override for home bottom compact status", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "singleWindow", - tuiCompactStatus: { - enabled: true, - homeBottom: true, - formatStyle: "allWindows", - }, - maintainerAnnouncements: { - enabled: false, - }, - }, - }, - }), - "utf8", - ); - - const singleWindowData = { - entries: [{ name: "Copilot", percentRemaining: 50 }], - errors: [], - sessionTokens: undefined, - }; - const allWindowsData = { - entries: [ - { name: "Copilot 5h", percentRemaining: 50 }, - { name: "Copilot Weekly", percentRemaining: 80 }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: singleWindowData, - allWindowsData, - }); - buildCompactQuotaStatusLine.mockReturnValue("50% 80%"); - - const bottom = await loadTuiHomeBottomStatus({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - }); - - expect(bottom).toEqual({ - status: "ready", - announcementText: undefined, - compact: { status: "ready", text: "50% 80%" }, - }); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith( - expect.objectContaining({ data: allWindowsData }), - ); - }); - - it("uses tuiSidebarPanel.formatStyle override instead of root formatStyle for sidebar", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "singleWindow", - tuiSidebarPanel: { - enabled: true, - formatStyle: "allWindows", - }, - }, - }, - }), - "utf8", - ); - - const singleWindowData = { - entries: [{ name: "Copilot", percentRemaining: 50 }], - errors: [], - sessionTokens: undefined, - }; - const allWindowsData = { - entries: [ - { name: "Copilot 5h", percentRemaining: 50 }, - { name: "Copilot Weekly", percentRemaining: 80 }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: singleWindowData, - allWindowsData, - }); - buildCompactQuotaStatusLine.mockReturnValue("Copilot 50%"); - buildSidebarQuotaPanelLines.mockReturnValueOnce(["[Copilot]", "5h line", "Weekly line"]); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "session-sidebar-override", - }); - - // sidebar keeps compact text collapsed and all-windows lines expanded. - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledTimes(1); - expect(buildSidebarQuotaPanelLines).toHaveBeenNthCalledWith(1, { - data: allWindowsData, - config: expect.objectContaining({ formatStyle: "allWindows" }), - }); - expect(surfaces.sidebar).toEqual({ - status: "ready", - lines: ["Copilot 50%"], - linesExpanded: ["[Copilot]", "5h line", "Weekly line"], - providerCount: 2, - }); - // collect still used root formatStyle=singleWindow - expect(collectQuotaRenderData).toHaveBeenCalledWith( - expect.objectContaining({ formatStyle: "singleWindow" }), - ); - }); - - it("applies the OpenCode Go preference only to collapsed sidebar data", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "singleWindow", - tuiSidebarPanel: { - enabled: true, - formatStyle: "allWindows", - opencodeGoPreferredWindow: "rolling", - }, - }, - }, - }), - "utf8", - ); - - const accounting = { - resultType: "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", - } as const; - const openCodeGoResult = { - attempted: true, - errors: [], - entries: [ - { - accounting, - name: "OpenCode Go Five-hour", - group: "OpenCode Go", - percentRemaining: 98, - semantic: { metric: { kind: "window", window: "five_hour" }, prominence: "primary" }, - }, - { - accounting, - name: "OpenCode Go Monthly", - group: "OpenCode Go", - percentRemaining: 33, - semantic: { metric: { kind: "window", window: "month" }, prominence: "primary" }, - }, - ], - } as const; - const otherResult = { - attempted: true, - errors: [], - entries: [ - { - accounting, - name: "Other Weekly", - group: "Other", - percentRemaining: 80, - semantic: { metric: { kind: "window", window: "week" }, prominence: "primary" }, - }, - { - accounting, - name: "Other Monthly", - group: "Other", - percentRemaining: 10, - semantic: { metric: { kind: "window", window: "month" }, prominence: "primary" }, - }, - ], - } as const; - const singleWindowData = { - entries: [openCodeGoResult.entries[1], otherResult.entries[1]], - errors: [], - sessionTokens: undefined, - }; - const allWindowsData = { - entries: [...openCodeGoResult.entries, ...otherResult.entries], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ - active: [{ id: "opencode-go" }, { id: "other" }], - data: singleWindowData, - allWindowsData, - providerResults: [ - { providerId: "opencode-go", result: openCodeGoResult }, - { providerId: "other", result: otherResult }, - ], - }); - buildCompactQuotaStatusLine.mockReturnValue("OpenCode Go 98% | Other 10%"); - buildSidebarQuotaPanelLines.mockReturnValue(["all configured windows"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "session-opencode-go-preference", - }); - - expect(panel).toEqual({ - status: "ready", - lines: ["OpenCode Go 98% | Other 10%"], - linesExpanded: ["all configured windows"], - providerCount: 2, - }); - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - entries: expect.arrayContaining([ - expect.objectContaining({ - semantic: { metric: { kind: "window", window: "five_hour" }, prominence: "primary" }, - }), - expect.objectContaining({ - semantic: { metric: { kind: "window", window: "month" }, prominence: "primary" }, - }), - ]), - }), - }), - ); - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith({ - data: allWindowsData, - config: expect.objectContaining({ formatStyle: "allWindows" }), - }); - }); - - it.each([ - { name: "inherited default singleWindow", sidebarFormatStyle: undefined }, - { name: "explicit sidebar singleWindow", sidebarFormatStyle: "singleWindow" as const }, - ])("applies the OpenCode Go preference for $name", async ({ sidebarFormatStyle }) => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - tuiSidebarPanel: { - enabled: true, - formatStyle: sidebarFormatStyle, - opencodeGoPreferredWindow: "rolling", - }, - }, - }, - }), - "utf8", - ); - - const accounting = { - resultType: "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", - } as const; - const openCodeGoResult = { - attempted: true, - errors: [], - entries: [ - { - accounting, - name: "OpenCode Go Five-hour", - group: "OpenCode Go", - percentRemaining: 98, - semantic: { metric: { kind: "window", window: "five_hour" }, prominence: "primary" }, - }, - { - accounting, - name: "OpenCode Go Monthly", - group: "OpenCode Go", - percentRemaining: 33, - semantic: { metric: { kind: "window", window: "month" }, prominence: "primary" }, - }, - ], - } as const; - const singleWindowData = { - entries: [openCodeGoResult.entries[1]], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ - active: [{ id: "opencode-go" }], - data: singleWindowData, - singleWindowData, - providerResults: [{ providerId: "opencode-go", result: openCodeGoResult }], - }); - buildSidebarQuotaPanelLines.mockReturnValue(["preferred rolling window"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: `session-opencode-go-${sidebarFormatStyle ?? "inherited"}`, - }); - - expect(panel).toEqual({ - status: "ready", - lines: ["preferred rolling window"], - providerCount: 1, - }); - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledTimes(1); - const collapsedCall = buildSidebarQuotaPanelLines.mock.calls[0]?.[0]; - expect(collapsedCall?.config).toEqual(expect.objectContaining({ formatStyle: "singleWindow" })); - expect( - collapsedCall?.data.entries.map((entry: (typeof openCodeGoResult.entries)[number]) => - entry.semantic.metric.kind === "window" ? entry.semantic.metric.window : undefined, - ), - ).toEqual(["five_hour"]); - }); - - it("uses tuiCompactStatus.formatStyle override instead of root formatStyle for compact", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "singleWindow", - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - formatStyle: "allWindows", - }, - }, - }, - }), - "utf8", - ); - - const singleWindowData = { - entries: [{ name: "Copilot", percentRemaining: 50 }], - errors: [], - sessionTokens: undefined, - }; - const allWindowsData = { - entries: [ - { name: "Copilot 5h", percentRemaining: 50 }, - { name: "Copilot Weekly", percentRemaining: 80 }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: singleWindowData, - allWindowsData, - }); - buildCompactQuotaStatusLine.mockReturnValue("50% 80%"); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "session-compact-override", - }); - - // compact uses allWindowsData (per compact.formatStyle=allWindows) - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith( - expect.objectContaining({ data: allWindowsData }), - ); - expect(surfaces.compact).toEqual({ status: "ready", text: "50% 80%" }); - }); - - it("independent formatStyle overrides: sidebar allWindows, compact singleWindow", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "singleWindow", - tuiSidebarPanel: { - enabled: true, - formatStyle: "allWindows", - }, - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - formatStyle: "singleWindow", - }, - }, - }, - }), - "utf8", - ); - - const singleWindowData = { - entries: [{ name: "Copilot", percentRemaining: 50 }], - errors: [], - sessionTokens: undefined, - }; - const allWindowsData = { - entries: [ - { name: "Copilot 5h", percentRemaining: 50 }, - { name: "Copilot Weekly", percentRemaining: 80 }, - ], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: singleWindowData, - allWindowsData, - }); - buildSidebarQuotaPanelLines.mockReturnValue(["[Copilot]", "5h", "Weekly"]); - buildCompactQuotaStatusLine.mockReturnValue("50%"); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "session-independent-overrides", - }); - - // sidebar → allWindowsData - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith({ - data: allWindowsData, - config: expect.objectContaining({ formatStyle: "allWindows" }), - }); - // compact → singleWindowData (formatStyle=singleWindow falls back to result.data) - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith( - expect.objectContaining({ data: singleWindowData }), - ); - }); - - it("uses singleWindowData for compact when root=allWindows and compact.formatStyle=singleWindow", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "allWindows", - tuiCompactStatus: { - enabled: true, - sessionPrompt: true, - formatStyle: "singleWindow", - }, - }, - }, - }), - "utf8", - ); - - // root=allWindows → result.data is allWindows-projected - const allWindowsData = { - entries: [ - { name: "Copilot 5h", percentRemaining: 50 }, - { name: "Copilot Weekly", percentRemaining: 80 }, - ], - errors: [], - sessionTokens: undefined, - }; - // singleWindowData pre-computed by collectQuotaRenderData when root=allWindows - const singleWindowData = { - entries: [{ name: "Copilot", percentRemaining: 50 }], - errors: [], - sessionTokens: undefined, - }; - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: allWindowsData, - allWindowsData, - singleWindowData, - }); - buildCompactQuotaStatusLine.mockReturnValue("50%"); - buildSidebarQuotaPanelLines.mockReturnValue([]); - - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "session-compact-singlewindow-root-allwindows", - }); - - // compact must use singleWindowData, not allWindowsData - expect(buildCompactQuotaStatusLine).toHaveBeenCalledWith( - expect.objectContaining({ data: singleWindowData }), - ); - expect(surfaces.compact).toEqual({ status: "ready", text: "50%" }); - }); - - describe("writeTuiQuotaExportIfEnabled", () => { - beforeEach(() => { - mockBuildQuotaExport.mockReset(); - mockWriteQuotaExport.mockReset(); - }); - - it("does not write when config.export.enabled is false", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - export: { enabled: false }, - }, - }, - }), - "utf8", - ); - - await writeTuiQuotaExportIfEnabled({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - }); - - expect(mockBuildQuotaExport).not.toHaveBeenCalled(); - expect(mockWriteQuotaExport).not.toHaveBeenCalled(); - }); - - it("writes the export through writeQuotaExport at the resolved path when enabled", async () => { - mockBuildQuotaExport.mockResolvedValue({ - version: 1, - exportedAt: 0, - fromCache: true, - cacheAgeSeconds: 0, - providers: {}, - }); - - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - export: { enabled: true, path: "/tmp/test-tui-export.json" }, - }, - }, - }), - "utf8", - ); - - await writeTuiQuotaExportIfEnabled({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: nestedDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - }); - - expect(mockBuildQuotaExport).toHaveBeenCalledOnce(); - expect(mockWriteQuotaExport).toHaveBeenCalledOnce(); - expect(mockWriteQuotaExport.mock.calls[0][1]).toBe("/tmp/test-tui-export.json"); - }); - - it("propagates writeQuotaExport errors to caller", async () => { - mockBuildQuotaExport.mockResolvedValue({ - version: 1, - exportedAt: 0, - fromCache: true, - cacheAgeSeconds: 0, - providers: {}, - }); - mockWriteQuotaExport.mockRejectedValueOnce(new Error("write rejected")); - - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - export: { enabled: true, path: "/tmp/test-tui-export.json" }, - }, - }, - }), - "utf8", - ); - - await expect( - writeTuiQuotaExportIfEnabled({ - api: { - state: { - provider: [], - path: { - worktree: worktreeDir, - directory: nestedDir, - }, - session: { - messages: () => [], - }, - }, - client: {}, - } as any, - }), - ).rejects.toThrow("write rejected"); - }); - }); -}); diff --git a/tests/tui-sidebar-collapse.test.ts b/tests/tui-sidebar-collapse.test.ts deleted file mode 100644 index 4196f366..00000000 --- a/tests/tui-sidebar-collapse.test.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import { - getSidebarPanelLines, - getSidebarPanelLinesExpanded, - type SidebarPanelState, -} from "../src/lib/tui-panel-state.js"; - -describe("getSidebarPanelLinesExpanded", () => { - it("returns linesExpanded when present and non-empty", () => { - const panel: SidebarPanelState = { - status: "ready", - lines: ["line1"], - linesExpanded: ["expanded1", "expanded2"], - }; - expect(getSidebarPanelLinesExpanded(panel)).toEqual(["expanded1", "expanded2"]); - }); - - it("falls back to getSidebarPanelLines when linesExpanded is empty", () => { - const panel: SidebarPanelState = { - status: "ready", - lines: ["line1"], - linesExpanded: [], - }; - expect(getSidebarPanelLinesExpanded(panel)).toEqual(["line1"]); - }); - - it("falls back to getSidebarPanelLines when linesExpanded is undefined", () => { - const panel: SidebarPanelState = { - status: "ready", - lines: ["line1"], - }; - expect(getSidebarPanelLinesExpanded(panel)).toEqual(["line1"]); - }); - - it("falls back to status placeholder when lines and linesExpanded are both empty", () => { - const panel: SidebarPanelState = { - status: "ready", - lines: [], - }; - expect(getSidebarPanelLinesExpanded(panel)).toEqual(["Unavailable"]); - }); -}); - -// --- Mocks (all hoisted) --- - -const { collectQuotaRenderDataMock, buildSidebarQuotaPanelLinesMock } = vi.hoisted(() => ({ - collectQuotaRenderDataMock: vi.fn(), - buildSidebarQuotaPanelLinesMock: vi.fn(), -})); - -vi.mock("../src/lib/quota-render-data.js", async () => { - const actual = await vi.importActual( - "../src/lib/quota-render-data.js", - ); - return { ...actual, collectQuotaRenderData: collectQuotaRenderDataMock }; -}); - -vi.mock("../src/lib/tui-sidebar-format.js", async () => { - const actual = await vi.importActual( - "../src/lib/tui-sidebar-format.js", - ); - return { ...actual, buildSidebarQuotaPanelLines: buildSidebarQuotaPanelLinesMock }; -}); - -import { loadTuiSessionQuotaSurfaces } from "../src/lib/tui-runtime.js"; - -async function loadSidebarPanel(params: Parameters[0]) { - return (await loadTuiSessionQuotaSurfaces(params)).sidebar; -} - -describe("tui-runtime linesExpanded", () => { - const originalEnv = { ...process.env }; - let tempDir: string; - let worktreeDir: string; - let xdgConfigHome: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-collapse-tui-")); - worktreeDir = join(tempDir, "worktree"); - xdgConfigHome = join(tempDir, "xdg-config"); - - mkdirSync(worktreeDir, { recursive: true }); - mkdirSync(join(xdgConfigHome, "opencode"), { recursive: true }); - - process.env.HOME = tempDir; - process.env.XDG_CONFIG_HOME = xdgConfigHome; - process.env.XDG_DATA_HOME = join(tempDir, "xdg-data"); - process.env.XDG_CACHE_HOME = join(tempDir, "xdg-cache"); - process.env.XDG_STATE_HOME = join(tempDir, "xdg-state"); - delete process.env.OPENCODE_CONFIG_DIR; - - collectQuotaRenderDataMock.mockReset(); - buildSidebarQuotaPanelLinesMock.mockReset(); - }); - - afterEach(() => { - process.env = originalEnv; - rmSync(tempDir, { recursive: true, force: true }); - }); - - it("uses single-window lines plus expanded detail when sidebar format is singleWindow", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "singleWindow", - }, - }, - }), - "utf8", - ); - - const data = { - entries: [{ name: "Copilot 5h", percentRemaining: 18 }], - errors: [], - sessionTokens: undefined, - }; - const allWindowsData = { - entries: [ - { name: "Copilot 5h", percentRemaining: 18 }, - { name: "Copilot Daily", percentRemaining: 42 }, - ], - errors: [], - sessionTokens: undefined, - }; - - collectQuotaRenderDataMock.mockResolvedValue({ - data, - allWindowsData, - active: [{ id: "copilot" }, { id: "openai" }], - }); - - buildSidebarQuotaPanelLinesMock - .mockReturnValueOnce(["Single-window sidebar"]) - .mockReturnValueOnce(["[Copilot]", "5h window 18%", "Daily window 42%"]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: worktreeDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "session-expanded", - }); - - expect(panel.status).toBe("ready"); - expect(panel.lines).toEqual(["Single-window sidebar"]); - expect(panel.linesExpanded).toEqual(["[Copilot]", "5h window 18%", "Daily window 42%"]); - expect(panel.providerCount).toBe(2); - - expect(collectQuotaRenderDataMock).toHaveBeenCalledWith( - expect.objectContaining({ includeAllWindowsData: true }), - ); - - expect(buildSidebarQuotaPanelLinesMock).toHaveBeenCalledTimes(2); - expect(buildSidebarQuotaPanelLinesMock).toHaveBeenNthCalledWith(1, { - data, - config: expect.objectContaining({ formatStyle: "singleWindow" }), - }); - expect(buildSidebarQuotaPanelLinesMock).toHaveBeenNthCalledWith(2, { - data: allWindowsData, - config: expect.objectContaining({ formatStyle: "allWindows" }), - }); - }); - - it("uses compact lines plus expanded detail when sidebar format is allWindows", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "allWindows", - }, - }, - }), - "utf8", - ); - - const data = { - entries: [ - { name: "Copilot 5h", percentRemaining: 5 }, - { name: "OpenAI 5h", percentRemaining: 81 }, - ], - errors: [], - sessionTokens: undefined, - }; - const singleWindowData = { - entries: [ - { name: "Copilot", percentRemaining: 5 }, - { name: "OpenAI", percentRemaining: 81 }, - ], - errors: [], - sessionTokens: undefined, - }; - const allWindowsData = data; - - collectQuotaRenderDataMock.mockResolvedValue({ - data, - singleWindowData, - allWindowsData, - active: [{ id: "copilot" }, { id: "openai" }], - }); - - buildSidebarQuotaPanelLinesMock.mockReturnValueOnce([ - "[Copilot]", - "Quota 95%", - "[OpenAI]", - "5h window 19%", - ]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: worktreeDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "session-all-windows-collapsible", - }); - - expect(panel.status).toBe("ready"); - expect(panel.lines).toEqual(["Copilot 5% | OpenAI 81%"]); - expect(panel.linesExpanded).toEqual(["[Copilot]", "Quota 95%", "[OpenAI]", "5h window 19%"]); - expect(panel.providerCount).toBe(2); - - expect(buildSidebarQuotaPanelLinesMock).toHaveBeenCalledTimes(1); - expect(buildSidebarQuotaPanelLinesMock).toHaveBeenNthCalledWith(1, { - data: allWindowsData, - config: expect.objectContaining({ formatStyle: "allWindows" }), - }); - }); - - it("omits linesExpanded when allWindowsData is null", async () => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderDataMock.mockResolvedValue({ data: null, allWindowsData: null, active: [] }); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: worktreeDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: "session-no-expand", - }); - - expect(panel.status).toBe("ready"); - expect(panel.lines).toEqual([]); - expect(panel.linesExpanded).toBeUndefined(); - }); - - const VISIBLE_OVERFLOW_ENTRIES = [ - { name: "Copilot", group: "Copilot", percentRemaining: 5 }, - { name: "OpenAI", group: "OpenAI ChatGPT Plus Plan", percentRemaining: 81 }, - ]; - - it.each([ - { - name: "empty", - extraActive: { id: "glm-coding-plan" }, - extraErrors: [] as Array<{ label: string; message: string }>, - }, - { - name: "error", - extraActive: { id: "glm-coding-plan" }, - extraErrors: [{ label: "GLM", message: "Unavailable" }], - }, - { - name: "no-entry", - extraActive: { id: "kimi-code" }, - extraErrors: [] as Array<{ label: string; message: string }>, - }, - ])("counts visible groups and ignores active-but-$name providers", async ({ - extraActive, - extraErrors, - }) => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "allWindows", - }, - }, - }), - "utf8", - ); - - collectQuotaRenderDataMock.mockResolvedValue({ - data: { entries: VISIBLE_OVERFLOW_ENTRIES, errors: extraErrors, sessionTokens: undefined }, - singleWindowData: { - entries: VISIBLE_OVERFLOW_ENTRIES, - errors: extraErrors, - sessionTokens: undefined, - }, - allWindowsData: { - entries: VISIBLE_OVERFLOW_ENTRIES, - errors: extraErrors, - sessionTokens: undefined, - }, - active: [{ id: "copilot" }, { id: "openai" }, extraActive], - }); - - buildSidebarQuotaPanelLinesMock.mockReturnValue([ - "[Copilot]", - "Quota 95%", - "[OpenAI ChatGPT Plus Plan]", - "5h window 19%", - ]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: worktreeDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: `session-visible-count-${extraActive.id}`, - }); - - expect(panel.providerCount).toBe(2); - expect(panel.lines).toEqual([ - extraErrors.length > 0 ? "Copilot 5% | +1 | +1 issue" : "Copilot 5% | +1", - ]); - expect(panel.linesExpanded).toEqual([ - "[Copilot]", - "Quota 95%", - "[OpenAI ChatGPT Plus Plan]", - "5h window 19%", - ]); - }); - - it.each([ - "remaining", - "used", - ] as const)("keeps collapsed overflow and expand/collapse count in %s mode", async (percentDisplayMode) => { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - formatStyle: "allWindows", - percentDisplayMode, - }, - }, - }), - "utf8", - ); - - collectQuotaRenderDataMock.mockResolvedValue({ - data: { entries: VISIBLE_OVERFLOW_ENTRIES, errors: [], sessionTokens: undefined }, - singleWindowData: { - entries: VISIBLE_OVERFLOW_ENTRIES, - errors: [], - sessionTokens: undefined, - }, - allWindowsData: { - entries: VISIBLE_OVERFLOW_ENTRIES, - errors: [], - sessionTokens: undefined, - }, - active: [{ id: "copilot" }, { id: "openai" }, { id: "glm-coding-plan" }], - }); - - buildSidebarQuotaPanelLinesMock.mockReturnValue([ - "[Copilot]", - "Quota 95%", - "[OpenAI ChatGPT Plus Plan]", - "5h window 19%", - ]); - - const panel = await loadSidebarPanel({ - api: { - state: { - provider: [], - path: { worktree: worktreeDir, directory: worktreeDir }, - session: { messages: () => [] }, - }, - client: {}, - } as any, - sessionID: `session-overflow-${percentDisplayMode}`, - }); - - expect(panel.providerCount).toBe(2); - expect(panel.lines).toEqual([ - percentDisplayMode === "used" ? "Copilot 95% | +1" : "Copilot 5% | +1", - ]); - expect(panel.lines?.[0]?.length).toBeLessThanOrEqual(36); - expect(panel.linesExpanded).toEqual([ - "[Copilot]", - "Quota 95%", - "[OpenAI ChatGPT Plus Plan]", - "5h window 19%", - ]); - }); -}); diff --git a/tests/tui-sidebar-content.test.ts b/tests/tui-sidebar-content.test.ts deleted file mode 100644 index 5b21fa45..00000000 --- a/tests/tui-sidebar-content.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - buildSidebarContentRows, - collectSidebarHeadingLines, - formatSidebarHeading, - formatSidebarHeadingSegments, - renderSidebarContentFrame, -} from "../src/lib/tui-sidebar-content.js"; - -const COLLAPSED_LINES = ["Copilot 5% | OpenAI 81% | GLM 12%"]; -const EXPANDED_LINES = [ - "[Copilot]", - "Quota 95%", - "[OpenAI]", - "5h window 19%", - "[GLM]", - "Weekly 12%", -]; - -function panelRows(collapsed: boolean) { - return buildSidebarContentRows({ - collapsed, - heading: "Quota", - hasDetailLines: true, - providerCount: 3, - lines: collapsed ? COLLAPSED_LINES : EXPANDED_LINES, - }); -} - -describe("sidebar content rows", () => { - it("keeps the collapsed provider suffix on one heading as a muted segment", () => { - const heading = { - collapsed: true, - heading: "Quota", - hasDetailLines: true, - providerCount: 3, - }; - expect(formatSidebarHeading(heading)).toBe("▶ Quota (3 providers)"); - expect(formatSidebarHeadingSegments(heading)).toEqual([ - { text: "▶ Quota", style: "title" }, - { text: " (3 providers)", style: "muted" }, - ]); - }); - - it("replaces the collapsed heading with one expanded heading and no leftover suffix", () => { - const collapsed = panelRows(true); - const expanded = panelRows(false); - const collapsedFrame = renderSidebarContentFrame(collapsed); - const expandedFrame = renderSidebarContentFrame(expanded); - - expect(collapsed.filter((row) => row.kind === "header")).toHaveLength(1); - expect(expanded.filter((row) => row.kind === "header")).toHaveLength(1); - expect(collectSidebarHeadingLines(collapsedFrame)).toEqual(["▶ Quota (3 providers)"]); - expect(collectSidebarHeadingLines(expandedFrame)).toEqual(["▼ Quota"]); - expect(expanded[0]?.segments).toEqual([{ text: "▼ Quota", style: "title" }]); - expect(expanded[0]?.text).toBe("▼ Quota"); - expect(expandedFrame).not.toContain("▶"); - expect(expandedFrame).not.toContain("providers"); - expect(collapsedFrame).not.toContain("▼"); - expect(collapsed[0]?.text.endsWith(" ")).toBe(false); - expect(expanded[0]?.text.endsWith(" ")).toBe(false); - }); - - it("drops expanded body rows when collapsing back to the summary heading", () => { - const collapsedFrame = renderSidebarContentFrame(panelRows(true)); - expect(collapsedFrame).not.toContain("[Copilot]"); - expect(collectSidebarHeadingLines(collapsedFrame)).toEqual(["▶ Quota (3 providers)"]); - }); - - it("keeps expand/collapse aligned when collapsed overflow is explicit", () => { - const heading = { - collapsed: true, - heading: "Quota", - hasDetailLines: true, - providerCount: 2, - }; - const collapsed = buildSidebarContentRows({ - ...heading, - lines: ["Copilot 5% | +1"], - }); - const expanded = buildSidebarContentRows({ - ...heading, - collapsed: false, - lines: ["[Copilot]", "Quota 95%", "[OpenAI ChatGPT Plus Plan]", "5h window 19%"], - }); - - expect(formatSidebarHeading(heading)).toBe("▶ Quota (2 providers)"); - expect(collectSidebarHeadingLines(renderSidebarContentFrame(collapsed))).toEqual([ - "▶ Quota (2 providers)", - ]); - expect(collectSidebarHeadingLines(renderSidebarContentFrame(expanded))).toEqual(["▼ Quota"]); - expect(renderSidebarContentFrame(collapsed)).toContain("Copilot 5% | +1"); - expect(renderSidebarContentFrame(collapsed)).not.toContain("81%"); - expect(renderSidebarContentFrame(expanded)).toContain("[OpenAI ChatGPT Plus Plan]"); - expect(renderSidebarContentFrame(expanded)).not.toContain("+1"); - }); -}); diff --git a/tests/tui-sidebar-expansion-ghost.test.ts b/tests/tui-sidebar-expansion-ghost.test.ts deleted file mode 100644 index 0a4c804c..00000000 --- a/tests/tui-sidebar-expansion-ghost.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - buildSidebarContentRows, - collectSidebarHeadingLines, - renderSidebarContentFrame, -} from "../src/lib/tui-sidebar-content.js"; - -const COLLAPSED_LINES = ["Copilot 5% | OpenAI 81% | GLM 12%"]; -const EXPANDED_LINES = [ - "[Copilot]", - "Quota 95%", - "[OpenAI]", - "5h window 19%", - "[GLM]", - "Weekly 12%", -]; - -function panelRows(collapsed: boolean) { - return buildSidebarContentRows({ - collapsed, - heading: "Quota", - hasDetailLines: true, - providerCount: 3, - lines: collapsed ? COLLAPSED_LINES : EXPANDED_LINES, - }); -} - -describe("sidebar expansion heading rows", () => { - it("keeps one header row and swaps title/suffix segments when expanding", () => { - const collapsed = panelRows(true); - expect(collapsed[0]).toMatchObject({ - kind: "header", - text: "▶ Quota (3 providers)", - segments: [ - { text: "▶ Quota", style: "title" }, - { text: " (3 providers)", style: "muted" }, - ], - }); - expect(collapsed.map((row) => row.text)).toEqual(["▶ Quota (3 providers)", COLLAPSED_LINES[0]]); - expect(collectSidebarHeadingLines(renderSidebarContentFrame(collapsed))).toEqual([ - "▶ Quota (3 providers)", - ]); - - const expanded = panelRows(false); - expect(expanded[0]).toMatchObject({ - kind: "header", - text: "▼ Quota", - segments: [{ text: "▼ Quota", style: "title" }], - }); - expect(expanded.map((row) => row.text)).toEqual(["▼ Quota", ...EXPANDED_LINES]); - expect(collectSidebarHeadingLines(renderSidebarContentFrame(expanded))).toEqual(["▼ Quota"]); - expect(expanded[0]?.segments.some((segment) => segment.style === "muted")).toBe(false); - expect(expanded[0]?.text).not.toContain("▶"); - expect(expanded[0]?.text).not.toContain("providers"); - - const collapsedAgain = panelRows(true); - expect(collapsedAgain.map((row) => row.text)).toEqual([ - "▶ Quota (3 providers)", - COLLAPSED_LINES[0], - ]); - expect(collapsedAgain.map((row) => row.text).join("\n")).not.toContain("▼"); - expect(collapsedAgain.map((row) => row.text).join("\n")).not.toContain("[Copilot]"); - expect(collectSidebarHeadingLines(renderSidebarContentFrame(collapsedAgain))).toEqual([ - "▶ Quota (3 providers)", - ]); - }); -}); diff --git a/tests/tui-smoke.test.ts b/tests/tui-smoke.test.ts deleted file mode 100644 index edc6868e..00000000 --- a/tests/tui-smoke.test.ts +++ /dev/null @@ -1,2150 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const { - buildQuotaDialogCommandOutput, - cleanupFns, - createTuiQuotaClient, - disposeQuotaTelemetryOwner, - getTuiRuntimeRootHints, - getTuiSessionModelMeta, - loadTuiHomeBottomStatus, - loadTuiSessionQuotaSurfaces, - normalizeTuiSessionID, - resolveTuiSurfaceRegistration, - writeTuiQuotaExportIfEnabled, -} = vi.hoisted(() => ({ - buildQuotaDialogCommandOutput: vi.fn(), - cleanupFns: [] as Array<() => void>, - createTuiQuotaClient: vi.fn(() => ({ config: {} })), - disposeQuotaTelemetryOwner: vi.fn(), - getTuiRuntimeRootHints: vi.fn(() => ({ - worktreeRoot: "/tmp/worktree", - activeDirectory: "/tmp/worktree", - fallbackDirectory: "/tmp/worktree", - })), - getTuiSessionModelMeta: vi.fn(), - loadTuiHomeBottomStatus: vi.fn(), - loadTuiSessionQuotaSurfaces: vi.fn(), - normalizeTuiSessionID: vi.fn((value: unknown) => - typeof value === "string" && value.trim() && !value.includes("{") ? value.trim() : undefined, - ), - resolveTuiSurfaceRegistration: vi.fn(), - writeTuiQuotaExportIfEnabled: vi.fn(), -})); - -vi.mock("../src/lib/tui-runtime.js", () => ({ - createTuiQuotaClient, - getTuiRuntimeRootHints, - getTuiSessionModelMeta, - loadTuiHomeBottomStatus, - loadTuiSessionQuotaSurfaces, - normalizeTuiSessionID, - resolveTuiSurfaceRegistration, - writeTuiQuotaExportIfEnabled, -})); - -vi.mock("../src/lib/quota-telemetry.js", () => ({ - disposeQuotaTelemetryOwner, -})); - -vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - buildQuotaDialogCommandOutput, - }; -}); - -const TUI_COMMAND_IDS = [ - "quota", - "quota_status", - "quota_announcements", - "pricing_refresh", - "tokens_today", - "tokens_daily", - "tokens_weekly", - "tokens_monthly", - "tokens_all", - "tokens_session", - "tokens_session_all", - "tokens_between", -] as const; - -const TUI_COMMAND_GROUPS = [ - ["quota", "quota_status"], - ["quota_announcements", "pricing_refresh"], - ["tokens_today", "tokens_daily"], - ["tokens_weekly", "tokens_monthly"], - ["tokens_all", "tokens_session"], - ["tokens_session_all", "tokens_between"], -] as const; - -vi.mock("solid-js", () => ({ - Show: (props: { when: unknown; children?: unknown; fallback?: unknown }) => { - if (!props.when) return props.fallback ?? null; - return typeof props.children === "function" - ? (props.children as (value: unknown) => unknown)(props.when) - : props.children; - }, - Index: (props: { - each: unknown[] | undefined | null | false; - children: (item: () => unknown, index: number) => unknown; - }) => { - if (!props.each) return null; - return props.each.map((item, index) => props.children(() => item, index)); - }, - createEffect: (fn: () => void) => fn(), - createSignal: (initial: T) => { - let value = initial; - return [ - () => value, - (next: T | ((previous: T) => T)) => { - value = typeof next === "function" ? (next as (previous: T) => T)(value) : next; - return value; - }, - ]; - }, - onCleanup: (fn: () => void) => { - cleanupFns.push(fn); - }, -})); - -vi.mock("@opentui/solid/jsx-runtime", () => ({ - Fragment: Symbol.for("Fragment"), - jsx: (type: unknown, props: Record) => - typeof type === "function" ? type(props) : { type, props }, - jsxs: (type: unknown, props: Record) => - typeof type === "function" ? type(props) : { type, props }, -})); - -function createElement( - type: unknown, - props: Record | null, - ...children: unknown[] -) { - const nextProps = { - ...(props ?? {}), - ...(children.length === 0 ? {} : { children: children.length === 1 ? children[0] : children }), - }; - return typeof type === "function" ? type(nextProps) : { type, props: nextProps }; -} - -function flattenSidebarText(node: unknown): string { - if (node == null || node === false) return ""; - if (typeof node === "string" || typeof node === "number") return String(node); - if (Array.isArray(node)) return node.map(flattenSidebarText).join(""); - if (typeof node === "object" && "props" in node) { - return flattenSidebarText((node as { props?: { children?: unknown } }).props?.children); - } - return ""; -} - -function sidebarTextNodes(node: any): any[] { - const children = Array.isArray(node?.props?.children) - ? node.props.children - : [node?.props?.children]; - return children.filter(Boolean); -} - -function readSidebarDisplayLines(node: any): string[] { - return sidebarTextNodes(node).map((child: any) => flattenSidebarText(child).trimEnd()); -} - -function readSidebarHeaderTree(node: any): { header: any; children: any[] } { - const header = sidebarTextNodes(node)[0]; - const children = Array.isArray(header?.props?.children) - ? header.props.children - : [header?.props?.children]; - return { header, children: children.filter(Boolean) }; -} - -function createApi() { - const keymapLayers: Array<{ commands: Array> }> = []; - const dialog = { - setSize: vi.fn(), - replace: vi.fn(), - clear: vi.fn(), - }; - const registered: Array<{ - order?: number; - slots: Record unknown>; - }> = []; - const unsubscribers: Array> = []; - const eventHandlers = new Map void>>(); - const kvStore = new Map(); - const api = { - route: { - current: { - name: "session", - params: { sessionID: "session-route" }, - }, - }, - state: { - provider: [], - path: { - worktree: "/tmp/worktree", - directory: "/tmp/worktree", - }, - session: { - messages: vi.fn(() => []), - }, - }, - theme: { - current: { - text: "text", - textMuted: "muted", - }, - }, - ui: { - Prompt: vi.fn((props: Record) => ({ type: "Prompt", props })), - DialogPrompt: vi.fn((props: Record) => ({ type: "DialogPrompt", props })), - dialog, - toast: vi.fn(), - }, - event: { - on: vi.fn((eventName: string, handler: (event: any) => void) => { - const handlers = eventHandlers.get(eventName) ?? []; - handlers.push(handler); - eventHandlers.set(eventName, handlers); - const unsubscribe = vi.fn(); - unsubscribers.push(unsubscribe); - return unsubscribe; - }), - }, - kv: { - get: vi.fn((key: string, fallback?: unknown) => - kvStore.has(key) ? kvStore.get(key) : fallback, - ), - set: vi.fn((key: string, value: unknown) => { - kvStore.set(key, value); - }), - }, - slots: { - register: vi.fn( - (plugin: { - order?: number; - slots: Record unknown>; - }) => { - registered.push(plugin); - return `slot-${registered.length}`; - }, - ), - }, - lifecycle: { - onDispose: vi.fn(), - }, - keymap: { - registerLayer: vi.fn((layer: { commands: Array> }) => { - keymapLayers.push(layer); - return vi.fn(); - }), - }, - client: { - app: { log: vi.fn().mockResolvedValue(undefined) }, - session: { - prompt: vi.fn(), - command: vi.fn(), - }, - }, - }; - - return { - api, - registered, - unsubscribers, - eventHandlers, - kvStore, - keymapLayers, - dialog, - }; -} - -const tuiPlugin = (await import("../src/tui.tsx")).default; - -async function loadTuiModule() { - return tuiPlugin; -} - -function deferred() { - let resolve!: (value: T) => void; - let reject!: (error: unknown) => void; - const promise = new Promise((resolvePromise, rejectPromise) => { - resolve = resolvePromise; - reject = rejectPromise; - }); - return { promise, resolve, reject }; -} - -async function flushPromises(): Promise { - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); -} - -async function startTui( - plugin: Awaited>, - api: ReturnType["api"], -): Promise { - await plugin.tui(api as any, undefined, {} as any); - await flushPromises(); -} - -describe("tui plugin smoke", () => { - beforeEach(() => { - vi.useFakeTimers(); - (globalThis as any).React = { createElement }; - cleanupFns.length = 0; - buildQuotaDialogCommandOutput.mockReset(); - buildQuotaDialogCommandOutput.mockResolvedValue({ - state: "output", - command: "quota", - title: "OpenCode Quota", - output: "Quota line 1\n\nQuota line 3", - dialogSize: "xlarge", - }); - createTuiQuotaClient.mockClear(); - disposeQuotaTelemetryOwner.mockClear(); - getTuiRuntimeRootHints.mockClear(); - getTuiSessionModelMeta.mockReset(); - getTuiSessionModelMeta.mockResolvedValue({ modelID: "gpt-5", providerID: "openai" }); - loadTuiHomeBottomStatus.mockReset(); - loadTuiHomeBottomStatus.mockResolvedValue({ - status: "ready", - compact: { status: "ready", text: "Home quota" }, - }); - loadTuiSessionQuotaSurfaces.mockReset(); - loadTuiSessionQuotaSurfaces.mockResolvedValue({ - sidebar: { status: "ready", lines: ["Sidebar quota"] }, - compact: { status: "ready", text: "Session quota" }, - }); - resolveTuiSurfaceRegistration.mockReset(); - writeTuiQuotaExportIfEnabled.mockReset(); - writeTuiQuotaExportIfEnabled.mockResolvedValue(undefined); - }); - - afterEach(() => { - for (const cleanup of cleanupFns.splice(0)) cleanup(); - vi.clearAllTimers(); - delete (globalThis as any).React; - vi.useRealTimers(); - }); - - it("registers stable neutral hosts before late surface resolution and activates once", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, registered } = createApi(); - const registration = deferred(); - resolveTuiSurfaceRegistration.mockReturnValueOnce(registration.promise); - - await plugin.tui(api as any, undefined, {} as any); - - expect(resolveTuiSurfaceRegistration).toHaveBeenCalledOnce(); - expect(keymapLayers).toHaveLength(1); - expect(registered.map((entry) => entry.order)).toEqual([150, 90]); - expect(Object.keys(registered[0]!.slots)).toEqual(["sidebar_content"]); - expect(Object.keys(registered[1]!.slots)).toEqual(["session_prompt", "home_bottom"]); - expect(registered[0]!.slots.sidebar_content({}, { session_id: "session-1" })).toBeNull(); - expect(registered[1]!.slots.session_prompt({}, { session_id: "session-1" })).toBeNull(); - expect(registered[1]!.slots.home_bottom({}, {})).toBeNull(); - expect(loadTuiSessionQuotaSurfaces).not.toHaveBeenCalled(); - expect(loadTuiHomeBottomStatus).not.toHaveBeenCalled(); - keymapLayers[0]!.commands[0]!.run?.(); - expect(buildQuotaDialogCommandOutput).not.toHaveBeenCalled(); - - registration.resolve({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: true, - }); - await flushPromises(); - - expect(api.keymap.registerLayer).toHaveBeenCalledOnce(); - expect(api.slots.register).toHaveBeenCalledTimes(2); - expect(buildQuotaDialogCommandOutput).not.toHaveBeenCalled(); - - keymapLayers[0]!.commands[0]!.run?.(); - await flushPromises(); - - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledOnce(); - expect(api.client.session.prompt).toHaveBeenCalledOnce(); - expect(registered[1]!.slots.session_prompt({}, { session_id: "session-1" })).not.toBeNull(); - expect(registered[1]!.slots.home_bottom({}, {})).not.toBeNull(); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledOnce(); - expect(loadTuiHomeBottomStatus).toHaveBeenCalledOnce(); - }); - - it("uses independent one-shot session and home registration tickets", async () => { - const plugin = await loadTuiModule(); - const { api, registered, eventHandlers } = createApi(); - const initialRuntimeSeed = { marker: "registration" }; - resolveTuiSurfaceRegistration.mockImplementationOnce( - ( - _api: unknown, - options?: { captureInitialRuntime?: (seed: typeof initialRuntimeSeed) => void }, - ) => { - options?.captureInitialRuntime?.(initialRuntimeSeed); - return Promise.resolve({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: true, - }); - }, - ); - - await startTui(plugin, api); - registered[0]!.slots.sidebar_content({}, { session_id: "session-1" }); - registered[1]!.slots.session_prompt({}, { session_id: "session-1" }); - registered[1]!.slots.home_bottom({}, {}); - await flushPromises(); - - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(1); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenNthCalledWith(1, { - api, - sessionID: "session-1", - initialRuntimeSeed, - }); - expect(loadTuiHomeBottomStatus).toHaveBeenNthCalledWith(1, { - api, - initialRuntimeSeed, - }); - - for (const handler of eventHandlers.get("message.updated") ?? []) { - handler({ properties: { info: { sessionID: "session-1" } } }); - } - await vi.advanceTimersByTimeAsync(150); - expect(loadTuiHomeBottomStatus).toHaveBeenNthCalledWith(2, { api }); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenNthCalledWith(2, { - api, - sessionID: "session-1", - }); - }); - - it("consumes a session ticket when the initial load starts and does not pass it to a successor", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - const initialRuntimeSeed = { marker: "registration" }; - loadTuiSessionQuotaSurfaces.mockRejectedValueOnce(new Error("initial unavailable")); - resolveTuiSurfaceRegistration.mockImplementationOnce( - ( - _api: unknown, - options?: { captureInitialRuntime?: (seed: typeof initialRuntimeSeed) => void }, - ) => { - options?.captureInitialRuntime?.(initialRuntimeSeed); - return Promise.resolve({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - }, - ); - - await startTui(plugin, api); - const sidebar = registered[0]!.slots.sidebar_content; - sidebar({}, { session_id: "session-1" }); - await flushPromises(); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenNthCalledWith(1, { - api, - sessionID: "session-1", - initialRuntimeSeed, - }); - - cleanupFns.pop()!(); - sidebar({}, { session_id: "session-1" }); - await flushPromises(); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenNthCalledWith(2, { - api, - sessionID: "session-1", - }); - }); - - it("does not queue repeated commands while surface registration is pending", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers } = createApi(); - const registration = deferred(); - resolveTuiSurfaceRegistration.mockReturnValueOnce(registration.promise); - - await plugin.tui(api as any, undefined, {} as any); - for (let index = 0; index < 25; index += 1) keymapLayers[0]!.commands[0]!.run?.(); - - registration.resolve({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - await flushPromises(); - - expect(buildQuotaDialogCommandOutput).not.toHaveBeenCalled(); - keymapLayers[0]!.commands[0]!.run?.(); - await flushPromises(); - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledOnce(); - }); - - it("does not react to a pending command when resolution is followed by disposal", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers } = createApi(); - const registration = deferred(); - resolveTuiSurfaceRegistration.mockReturnValueOnce(registration.promise); - - await plugin.tui(api as any, undefined, {} as any); - keymapLayers[0]!.commands[0]!.run?.(); - registration.resolve({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - const dispose = api.lifecycle.onDispose.mock.calls[0]?.[0]; - dispose?.(); - await flushPromises(); - - keymapLayers[0]!.commands[0]!.run?.(); - await flushPromises(); - expect(buildQuotaDialogCommandOutput).not.toHaveBeenCalled(); - }); - - it("activates the existing inline-command and sidebar fallback after late failure", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, registered } = createApi(); - const registration = deferred(); - resolveTuiSurfaceRegistration.mockReturnValueOnce(registration.promise); - - await plugin.tui(api as any, undefined, {} as any); - expect(keymapLayers).toHaveLength(1); - expect(registered).toHaveLength(2); - expect(registered[0]!.slots.sidebar_content({}, { session_id: "session-1" })).toBeNull(); - - registration.reject(new Error("config unavailable")); - await flushPromises(); - - expect(api.keymap.registerLayer).toHaveBeenCalledOnce(); - expect(api.slots.register).toHaveBeenCalledTimes(2); - registered[0]!.slots.sidebar_content({}, { session_id: "session-1" }); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledOnce(); - expect(registered[1]!.slots.session_prompt({}, { session_id: "session-1" })).toBeNull(); - expect(registered[1]!.slots.home_bottom({}, {})).toBeNull(); - }); - - it("consumes late registration errors without retrying fallback", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - api.keymap.registerLayer.mockImplementationOnce(() => { - throw new Error("registration unavailable"); - }); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await plugin.tui(api as any, undefined, {} as any); - await flushPromises(); - - expect(api.keymap.registerLayer).toHaveBeenCalledOnce(); - expect(registered).toEqual([]); - }); - - it.each([ - ["first", 1], - ["second", 2], - ] as const)("keeps the installed command layer active when the %s slot registration throws", async (_label, failedAttempt) => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, registered } = createApi(); - const registration = deferred(); - resolveTuiSurfaceRegistration.mockReturnValueOnce(registration.promise); - let attempts = 0; - api.slots.register.mockImplementation((entry: any) => { - attempts += 1; - if (attempts === failedAttempt) throw new Error("slot registration unavailable"); - registered.push(entry); - return `slot-${registered.length}`; - }); - - await plugin.tui(api as any, undefined, {} as any); - expect(api.keymap.registerLayer).toHaveBeenCalledOnce(); - expect(api.slots.register).toHaveBeenCalledTimes(failedAttempt); - expect(registered).toHaveLength(failedAttempt - 1); - - registration.resolve({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: true, - }); - await flushPromises(); - - keymapLayers[0]!.commands[0]!.run?.(); - await flushPromises(); - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledOnce(); - expect(api.client.session.prompt).toHaveBeenCalledOnce(); - }); - - it("keeps eager hosts neutral and pending commands inert after disposal", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, registered } = createApi(); - const registration = deferred(); - resolveTuiSurfaceRegistration.mockReturnValueOnce(registration.promise); - - await plugin.tui(api as any, undefined, {} as any); - expect(api.lifecycle.onDispose).toHaveBeenCalledTimes(2); - expect(keymapLayers).toHaveLength(1); - expect(registered).toHaveLength(2); - - keymapLayers[0]!.commands[0]!.run?.(); - const dispose = api.lifecycle.onDispose.mock.calls[0]?.[0]; - dispose?.(); - registration.reject(new Error("config unavailable")); - await flushPromises(); - - expect(api.keymap.registerLayer).toHaveBeenCalledOnce(); - expect(api.slots.register).toHaveBeenCalledTimes(2); - expect(registered[0]!.slots.sidebar_content({}, { session_id: "session-1" })).toBeNull(); - expect(registered[1]!.slots.session_prompt({}, { session_id: "session-1" })).toBeNull(); - expect(registered[1]!.slots.home_bottom({}, {})).toBeNull(); - expect(buildQuotaDialogCommandOutput).not.toHaveBeenCalled(); - expect(loadTuiSessionQuotaSurfaces).not.toHaveBeenCalled(); - expect(loadTuiHomeBottomStatus).not.toHaveBeenCalled(); - expect(createTuiQuotaClient).toHaveBeenCalledOnce(); - expect(disposeQuotaTelemetryOwner).toHaveBeenCalledOnce(); - }); - - it("registers every deterministic command through the palette keymap", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, dialog } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - - expect(api.keymap.registerLayer).toHaveBeenCalledOnce(); - expect(api.lifecycle.onDispose).toHaveBeenCalledTimes(2); - const telemetryCleanup = api.lifecycle.onDispose.mock.calls[0]?.[0]; - expect(telemetryCleanup).toBeTypeOf("function"); - telemetryCleanup?.(); - expect(createTuiQuotaClient).toHaveBeenCalledOnce(); - expect(disposeQuotaTelemetryOwner).toHaveBeenCalledWith( - createTuiQuotaClient.mock.results[0]?.value, - ); - expect(keymapLayers[0]?.commands.map((command) => command.slashName)).toEqual(TUI_COMMAND_IDS); - for (const slashName of TUI_COMMAND_IDS) { - expect( - keymapLayers[0]?.commands.filter((command) => command.slashName === slashName), - ).toHaveLength(1); - } - expect(dialog.replace).not.toHaveBeenCalled(); - }); - - describe.each(["inline", "dialog"] as const)("%s native command display", (commandDisplay) => { - it.each( - TUI_COMMAND_GROUPS, - )("routes /%s and /%s once without model execution", async (...commands) => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, dialog } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay, - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - for (const command of commands) { - vi.clearAllMocks(); - const output = `${command} output`; - buildQuotaDialogCommandOutput.mockResolvedValueOnce({ - state: "output", - command, - title: command, - output, - dialogSize: "xlarge", - }); - const registeredCommand = keymapLayers[0]!.commands.find( - (item) => item.slashName === command, - )!; - (registeredCommand.run as (input?: unknown) => void)({ arguments: "" }); - await Promise.resolve(); - await Promise.resolve(); - - expect(buildQuotaDialogCommandOutput, command).toHaveBeenCalledOnce(); - expect(buildQuotaDialogCommandOutput, command).toHaveBeenCalledWith( - expect.objectContaining({ - command, - client: { config: {} }, - sessionID: "session-route", - }), - ); - if (commandDisplay === "inline") { - expect(api.client.session.prompt, command).toHaveBeenCalledOnce(); - expect(api.client.session.prompt, command).toHaveBeenCalledWith({ - sessionID: "session-route", - noReply: true, - parts: [{ type: "text", text: output, ignored: true }], - }); - expect(dialog.replace, command).not.toHaveBeenCalled(); - } else { - expect(api.client.session.prompt, command).not.toHaveBeenCalled(); - expect(dialog.replace, command).toHaveBeenCalledTimes(2); - } - expect(api.client.session.command, command).not.toHaveBeenCalled(); - } - }); - }); - - it("selects Home dialog destination before executing an inline-configured command", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, dialog } = createApi(); - (api.route.current as any) = { name: "home", params: {} }; - let dialogCallsAtExecution = 0; - buildQuotaDialogCommandOutput.mockImplementationOnce(async () => { - dialogCallsAtExecution = dialog.replace.mock.calls.length; - return { - state: "output", - command: "quota", - title: "OpenCode Quota", - output: "Home quota output", - dialogSize: "xlarge", - }; - }); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - const quota = keymapLayers[0]!.commands.find((command) => command.slashName === "quota")!; - (quota.run as (input?: unknown) => void)({ arguments: "" }); - await Promise.resolve(); - await Promise.resolve(); - - expect(dialogCallsAtExecution).toBe(1); - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledOnce(); - expect(api.client.session.prompt).not.toHaveBeenCalled(); - expect(dialog.replace).toHaveBeenCalledTimes(2); - expect(api.client.session.command).not.toHaveBeenCalled(); - }); - - it.each([ - "inline", - "dialog", - ] as const)("keeps command no-op behavior in %s mode", async (commandDisplay) => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, dialog } = createApi(); - buildQuotaDialogCommandOutput.mockResolvedValueOnce({ - state: "noop", - command: "pricing_refresh", - reason: "disabled", - }); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay, - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - const refresh = keymapLayers[0]!.commands.find( - (command) => command.slashName === "pricing_refresh", - )!; - (refresh.run as (input?: unknown) => void)({ arguments: "" }); - await Promise.resolve(); - await Promise.resolve(); - - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledOnce(); - expect(api.client.session.prompt).not.toHaveBeenCalled(); - expect(api.client.session.command).not.toHaveBeenCalled(); - if (commandDisplay === "inline") { - expect(dialog.replace).not.toHaveBeenCalled(); - expect(dialog.clear).not.toHaveBeenCalled(); - } else { - expect(dialog.replace).toHaveBeenCalledOnce(); - expect(dialog.clear).toHaveBeenCalledOnce(); - } - }); - - it("shows the command error without falling back to quota output dialog when inline injection fails", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, dialog } = createApi(); - api.client.session.prompt.mockRejectedValueOnce(new Error("prompt unavailable")); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - const quota = keymapLayers[0]!.commands.find((command) => command.slashName === "quota")!; - (quota.run as (input?: unknown) => void)(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - - expect(api.client.session.prompt).toHaveBeenCalledOnce(); - expect(dialog.replace).toHaveBeenCalledOnce(); - const errorDialog = dialog.replace.mock.calls[0]![0]() as any; - expect(errorDialog.props.children).not.toContain("Quota line 1"); - expect(api.ui.toast).toHaveBeenCalledWith({ - variant: "error", - message: "OpenCode Quota command failed", - }); - expect(api.client.session.command).not.toHaveBeenCalled(); - }); - - it("collects arguments with DialogPrompt before running an argument-capable command", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, dialog } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - const status = keymapLayers[0]!.commands.find( - (command) => command.slashName === "quota_status", - )!; - (status.run as (input?: unknown) => void)(); - await Promise.resolve(); - await Promise.resolve(); - - expect(dialog.replace).not.toHaveBeenCalled(); - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledWith( - expect.objectContaining({ command: "quota_status", arguments: undefined }), - ); - expect(api.client.session.prompt).toHaveBeenCalledOnce(); - expect(api.client.session.prompt).toHaveBeenCalledWith({ - sessionID: "session-route", - noReply: true, - parts: [ - { - type: "text", - text: "Quota line 1\n\nQuota line 3", - ignored: true, - }, - ], - }); - expect(api.client.session.command).not.toHaveBeenCalled(); - - const announcements = keymapLayers[0]!.commands.find( - (command) => command.slashName === "quota_announcements", - )!; - (announcements.run as (input?: unknown) => void)(); - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledOnce(); - const announcementsPrompt = dialog.replace.mock.calls.at(-1)![0]() as any; - announcementsPrompt.props.onConfirm(" "); - await Promise.resolve(); - await Promise.resolve(); - expect(buildQuotaDialogCommandOutput).toHaveBeenLastCalledWith( - expect.objectContaining({ command: "quota_announcements" }), - ); - expect(api.client.session.prompt).toHaveBeenCalledTimes(2); - expect(api.client.session.command).not.toHaveBeenCalled(); - }); - - it("keeps argument input in a dialog and routes final output to configured Dialog mode", async () => { - const plugin = await loadTuiModule(); - const { api, keymapLayers, dialog } = createApi(); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "dialog", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - const between = keymapLayers[0]!.commands.find( - (command) => command.slashName === "tokens_between", - )!; - (between.run as (input?: unknown) => void)(); - expect(buildQuotaDialogCommandOutput).not.toHaveBeenCalled(); - - const prompt = dialog.replace.mock.calls[0]![0]() as any; - prompt.props.onConfirm("2026-01-01 2026-01-15"); - await Promise.resolve(); - await Promise.resolve(); - - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledOnce(); - expect(buildQuotaDialogCommandOutput).toHaveBeenCalledWith( - expect.objectContaining({ - command: "tokens_between", - arguments: "2026-01-01 2026-01-15", - }), - ); - expect(dialog.replace).toHaveBeenCalledTimes(3); - expect(api.client.session.prompt).not.toHaveBeenCalled(); - expect(api.client.session.command).not.toHaveBeenCalled(); - }); - - it("keeps stable hosts registered while activating sidebar and compact surfaces independently", async () => { - const plugin = await loadTuiModule(); - const sidebarOnly = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: false }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, sidebarOnly.api); - - expect(sidebarOnly.registered).toHaveLength(2); - expect(sidebarOnly.registered.map((entry) => entry.order)).toEqual([150, 90]); - sidebarOnly.registered[0].slots.sidebar_content({}, { session_id: "session-1" }); - await flushPromises(); - expect( - sidebarOnly.registered[0].slots.sidebar_content({}, { session_id: "session-1" }), - ).not.toBeNull(); - expect( - sidebarOnly.registered[1].slots.session_prompt({}, { session_id: "session-1" }), - ).toBeNull(); - expect(sidebarOnly.registered[1].slots.home_bottom({}, {})).toBeNull(); - - const compactOnly = createApi(); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: true }, - homeBottom: true, - }); - - await startTui(plugin, compactOnly.api); - - expect(compactOnly.registered).toHaveLength(2); - expect(compactOnly.registered.map((entry) => entry.order)).toEqual([150, 90]); - expect( - compactOnly.registered[0].slots.sidebar_content({}, { session_id: "session-1" }), - ).toBeNull(); - expect( - compactOnly.registered[1].slots.session_prompt({}, { session_id: "session-1" }), - ).not.toBeNull(); - expect(compactOnly.registered[1].slots.home_bottom({}, {})).not.toBeNull(); - - const enabled = createApi(); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: true }, - homeBottom: true, - }); - - await startTui(plugin, enabled.api); - - expect(enabled.registered).toHaveLength(2); - expect(enabled.registered[0].order).toBe(150); - expect(Object.keys(enabled.registered[0].slots)).toEqual(["sidebar_content"]); - expect(enabled.registered[1].order).toBe(90); - expect(Object.keys(enabled.registered[1].slots)).toEqual(["session_prompt", "home_bottom"]); - }); - - it("renders sidebar summary count from runtime state and persists detail toggles", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { - status: "ready", - lines: ["OpenCode Go 5h 98%"], - linesExpanded: [ - "[OpenCode Go]", - "5h window 98%", - "Weekly window 53%", - "Monthly window 33%", - ], - providerCount: 2, - }, - compact: { status: "ready", text: "Session quota" }, - }); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - - const sidebarRegistration = registered.find((registration) => registration.order === 150); - expect(sidebarRegistration).toBeDefined(); - - sidebarRegistration!.slots.sidebar_content({}, { session_id: "session-1" }); - await Promise.resolve(); - - const collapsed = sidebarRegistration!.slots.sidebar_content( - {}, - { session_id: "session-1" }, - ) as any; - const collapsedLines = readSidebarDisplayLines(collapsed); - expect(collapsedLines).toEqual(["▶ Quota (2 providers)", "OpenCode Go 5h 98%"]); - const collapsedHeader = readSidebarHeaderTree(collapsed); - expect(collapsedHeader.header.type).toBe("text"); - expect(collapsedHeader.header.props.width).toBe("100%"); - expect(collapsedHeader.header.props.wrapMode).toBe("none"); - expect(collapsedHeader.children).toEqual([ - { type: "b", props: { children: "▶ Quota" } }, - { type: "span", props: { fg: "muted", children: " (2 providers)" } }, - ]); - expect(sidebarTextNodes(collapsed).every((child: any) => child.type === "text")).toBe(true); - - collapsedHeader.header.props.onMouseDown(); - - expect(api.kv.set).toHaveBeenCalledWith("quota-sidebar-collapsed", false); - - const expanded = sidebarRegistration!.slots.sidebar_content( - {}, - { session_id: "session-1" }, - ) as any; - const expandedLines = readSidebarDisplayLines(expanded); - expect(expandedLines).toEqual([ - "▼ Quota", - "[OpenCode Go]", - "5h window 98%", - "Weekly window 53%", - "Monthly window 33%", - ]); - const expandedHeader = readSidebarHeaderTree(expanded); - expect(expandedHeader.header.type).toBe("text"); - expect(expandedHeader.header.props.width).toBe("100%"); - expect(expandedHeader.children).toEqual([{ type: "b", props: { children: "▼ Quota" } }]); - expect( - expandedLines.filter((line: string) => line.includes("Quota") || line.startsWith("▶")), - ).toEqual(["▼ Quota"]); - }); - - it("keeps sidebar collapse icons while naming the bare percent mode", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { - status: "ready", - lines: ["OpenCode Go 98%"], - linesExpanded: ["[OpenCode Go]", "Five-hour 98%"], - headerPercentMode: "used", - }, - compact: { status: "disabled" }, - }); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - const registration = registered.find((item) => item.order === 150)!; - registration.slots.sidebar_content({}, { session_id: "session-mode" }); - await Promise.resolve(); - - const collapsed = registration.slots.sidebar_content({}, { session_id: "session-mode" }) as any; - expect(readSidebarDisplayLines(collapsed)[0]).toBe("▶ Quota [Used]"); - - collapsed.props.children[0].props.onMouseDown(); - const expanded = registration.slots.sidebar_content({}, { session_id: "session-mode" }) as any; - expect(readSidebarDisplayLines(expanded)[0]).toBe("▼ Quota [Used]"); - }); - - it("keeps non-expandable empty sidebar panels visible while collapsed", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "ready", lines: [] }, - compact: { status: "ready", text: "Session quota" }, - }); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - - const sidebarRegistration = registered.find((registration) => registration.order === 150); - expect(sidebarRegistration).toBeDefined(); - - sidebarRegistration!.slots.sidebar_content({}, { session_id: "session-1" }); - await Promise.resolve(); - - const rendered = sidebarRegistration!.slots.sidebar_content( - {}, - { session_id: "session-1" }, - ) as any; - expect(readSidebarDisplayLines(rendered)).toEqual(["Quota", "Unavailable"]); - }); - - it("activates only the sidebar host when surface resolution fails", async () => { - const plugin = await loadTuiModule(); - const fallback = createApi(); - - resolveTuiSurfaceRegistration.mockRejectedValueOnce(new Error("config unavailable")); - - await startTui(plugin, fallback.api); - - expect(fallback.registered).toHaveLength(2); - expect(fallback.registered.map((entry) => entry.order)).toEqual([150, 90]); - fallback.registered[0].slots.sidebar_content({}, { session_id: "session-1" }); - await flushPromises(); - expect( - fallback.registered[0].slots.sidebar_content({}, { session_id: "session-1" }), - ).not.toBeNull(); - expect(fallback.registered[1].slots.session_prompt({}, { session_id: "session-1" })).toBeNull(); - expect(fallback.registered[1].slots.home_bottom({}, {})).toBeNull(); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledWith({ - api: fallback.api, - sessionID: "session-1", - }); - }); - - it("does not register right-side compact slots", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: true }, - homeBottom: true, - }); - - await startTui(plugin, api); - - const slotNames = registered.flatMap((registration) => Object.keys(registration.slots)); - expect(slotNames).toContain("session_prompt"); - expect(slotNames).toContain("home_bottom"); - expect(slotNames).not.toContain("session_prompt_right"); - expect(slotNames).not.toContain("home_prompt_right"); - }); - - it("preserves session refresh delays, event filtering, interval refresh, and mount recovery", async () => { - const plugin = await loadTuiModule(); - const { api, registered, eventHandlers } = createApi(); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - registered[0]!.slots.sidebar_content({}, { session_id: "session-1" }); - await flushPromises(); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(1); - - await vi.advanceTimersByTimeAsync(500); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(2); - await vi.advanceTimersByTimeAsync(1_000); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(3); - await vi.advanceTimersByTimeAsync(2_500); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(4); - - eventHandlers.get("session.updated")![0]!({ properties: { info: { id: "other" } } }); - await vi.advanceTimersByTimeAsync(600); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(4); - - eventHandlers.get("session.updated")![0]!({ properties: { info: { id: "session-1" } } }); - await vi.advanceTimersByTimeAsync(149); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(4); - await vi.advanceTimersByTimeAsync(1); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(5); - await vi.advanceTimersByTimeAsync(450); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(6); - - await vi.advanceTimersByTimeAsync(54_800); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(7); - }); - - it("coalesces in-flight session refreshes and accepts the active completion before its follow-up", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - const first = deferred<{ - sidebar: { status: "ready"; lines: string[] }; - compact: { status: "ready"; text: string }; - }>(); - const second = deferred<{ - sidebar: { status: "ready"; lines: string[] }; - compact: { status: "ready"; text: string }; - }>(); - loadTuiSessionQuotaSurfaces.mockReset(); - loadTuiSessionQuotaSurfaces - .mockReturnValueOnce(first.promise) - .mockReturnValueOnce(second.promise); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - const sidebar = registered[0]!.slots.sidebar_content; - sidebar({}, { session_id: "session-1" }); - await vi.advanceTimersByTimeAsync(4_000); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledOnce(); - - first.resolve({ - sidebar: { status: "ready", lines: ["initial"] }, - compact: { status: "ready", text: "initial" }, - }); - await flushPromises(); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledTimes(2); - let rendered = sidebar({}, { session_id: "session-1" }) as any; - expect(readSidebarDisplayLines(rendered)).toEqual(["Quota", "initial"]); - - second.resolve({ - sidebar: { status: "ready", lines: ["refreshed"] }, - compact: { status: "ready", text: "refreshed" }, - }); - await flushPromises(); - rendered = sidebar({}, { session_id: "session-1" }); - expect(readSidebarDisplayLines(rendered)).toEqual(["Quota", "refreshed"]); - }); - - it("keeps shared session resources alive until the final release and then disposes them", async () => { - const plugin = await loadTuiModule(); - const { api, registered, unsubscribers } = createApi(); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - await startTui(plugin, api); - const sidebar = registered[0]!.slots.sidebar_content; - sidebar({}, { session_id: "session-1" }); - sidebar({}, { session_id: "session-1" }); - await flushPromises(); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledOnce(); - - cleanupFns.shift()!(); - expect(unsubscribers.every((unsubscribe) => !unsubscribe.mock.calls.length)).toBe(true); - cleanupFns.shift()!(); - expect(unsubscribers).toHaveLength(4); - expect(unsubscribers.every((unsubscribe) => unsubscribe.mock.calls.length === 1)).toBe(true); - - await vi.advanceTimersByTimeAsync(60_000); - expect(loadTuiSessionQuotaSurfaces).toHaveBeenCalledOnce(); - }); - - it("keeps home free of mount recovery and exports only accepted refreshes", async () => { - const plugin = await loadTuiModule(); - const { api, registered, eventHandlers } = createApi(); - const first = deferred(); - const second = deferred(); - loadTuiHomeBottomStatus.mockReset(); - loadTuiHomeBottomStatus.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: true, - }); - - await startTui(plugin, api); - registered.find((registration) => registration.order === 90)!.slots.home_bottom({}, {}); - await vi.advanceTimersByTimeAsync(4_000); - expect(loadTuiHomeBottomStatus).toHaveBeenCalledOnce(); - - eventHandlers.get("message.updated")![0]!({ properties: {} }); - await vi.advanceTimersByTimeAsync(600); - expect(loadTuiHomeBottomStatus).toHaveBeenCalledOnce(); - - first.resolve({ - status: "ready", - compact: { status: "ready", text: "initial" }, - }); - await flushPromises(); - expect(loadTuiHomeBottomStatus).toHaveBeenCalledTimes(2); - expect(writeTuiQuotaExportIfEnabled).toHaveBeenCalledOnce(); - - second.resolve({ - status: "ready", - compact: { status: "ready", text: "refreshed" }, - }); - await flushPromises(); - expect(writeTuiQuotaExportIfEnabled).toHaveBeenCalledTimes(2); - }); - - it("ignores rejected and disposed home completions without exporting", async () => { - const plugin = await loadTuiModule(); - const rejected = createApi(); - loadTuiHomeBottomStatus.mockRejectedValueOnce(new Error("unavailable")); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: true }, - homeBottom: true, - }); - await startTui(plugin, rejected.api); - rejected.registered - .find((registration) => registration.order === 90)! - .slots.home_bottom({}, {}); - await flushPromises(); - expect(writeTuiQuotaExportIfEnabled).not.toHaveBeenCalled(); - - const disposed = createApi(); - const pending = deferred(); - loadTuiHomeBottomStatus.mockReturnValueOnce(pending.promise); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: true }, - homeBottom: true, - }); - await startTui(plugin, disposed.api); - disposed.registered - .find((registration) => registration.order === 90)! - .slots.home_bottom({}, {}); - cleanupFns.pop()!(); - pending.resolve({ status: "ready", compact: { status: "disabled" } }); - await flushPromises(); - expect(writeTuiQuotaExportIfEnabled).not.toHaveBeenCalled(); - }); - - it("renders home compact status centered with a blank line above it", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: true }, - homeBottom: true, - }); - - await startTui(plugin, api); - - const compactRegistration = registered.find((registration) => registration.order === 90); - expect(compactRegistration).toBeDefined(); - - const loading = compactRegistration!.slots.home_bottom({}, {}) as any; - expect(loading).toMatchObject({ - type: "box", - props: { - children: [ - { type: "text", props: { children: " " } }, - null, - { - type: "box", - props: { - children: { - type: "text", - props: { children: "Quota loading…" }, - }, - }, - }, - ], - }, - }); - - await Promise.resolve(); - - const rendered = compactRegistration!.slots.home_bottom({}, {}) as any; - expect(rendered).toMatchObject({ - type: "box", - props: { - gap: 0, - children: [ - { - type: "text", - props: { children: " " }, - }, - null, - { - type: "box", - props: { - flexDirection: "row", - justifyContent: "center", - children: { - type: "text", - props: { - fg: "muted", - wrapMode: "none", - children: "Home quota", - }, - }, - }, - }, - ], - }, - }); - }); - - it("keeps announcement-only home host empty until a delayed announcement populates it", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - let resolveBottom!: (value: { - status: "ready"; - announcementText: string; - compact: { status: "disabled" }; - }) => void; - loadTuiHomeBottomStatus.mockReturnValueOnce( - new Promise((resolve) => { - resolveBottom = resolve; - }), - ); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: true }, - homeBottom: true, - }); - - await startTui(plugin, api); - - const homeBottom = registered.find((registration) => registration.order === 90)!.slots - .home_bottom; - const empty = homeBottom({}, {}) as any; - expect(empty).toEqual({ - type: "box", - props: { gap: 0, children: [null, null, null] }, - }); - - resolveBottom({ - status: "ready", - announcementText: "Notice: Maintainer announcement available. Run /quota_announcements.", - compact: { status: "disabled" }, - }); - await Promise.resolve(); - - const populated = homeBottom({}, {}) as any; - expect(populated.type).toBe("box"); - expect(populated.props.children[0]).toMatchObject({ - type: "text", - props: { children: " " }, - }); - expect(populated.props.children[1]).toMatchObject({ - type: "box", - props: { - children: { - type: "text", - props: { - children: "Notice: Maintainer announcement available. Run /quota_announcements.", - }, - }, - }, - }); - expect(populated.props.children[2]).toBeNull(); - }); - - it("keeps export-only home host empty while still writing the export", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - loadTuiHomeBottomStatus.mockResolvedValueOnce({ - status: "disabled", - compact: { status: "disabled" }, - }); - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: true, - }); - - await startTui(plugin, api); - - const rendered = registered - .find((registration) => registration.order === 90)! - .slots.home_bottom({}, {}) as any; - expect(rendered).toEqual({ - type: "box", - props: { gap: 0, children: [null, null, null] }, - }); - await Promise.resolve(); - expect(writeTuiQuotaExportIfEnabled).toHaveBeenCalledOnce(); - expect(writeTuiQuotaExportIfEnabled).toHaveBeenCalledWith({ api }); - }); - - it("wraps api.ui.Prompt and forwards session prompt props and ref exactly", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - const onSubmit = vi.fn(); - const ref = vi.fn(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: true, - homeBottom: false, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - - announcements: { homeBottom: false }, - homeBottom: false, - }); - - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { - name: "Copilot 5h", - percentRemaining: 50, - resetTimeIso: "2099-01-01T00:00:00.000Z", - }, - percentDisplayMode: "remaining", - }, - }); - - await startTui(plugin, api); - - const compactRegistration = registered.find((registration) => registration.order === 90); - expect(compactRegistration).toBeDefined(); - - const promptProps = { - session_id: "session-1", - visible: false, - disabled: true, - on_submit: onSubmit, - ref, - }; - compactRegistration!.slots.session_prompt({}, promptProps); - await flushPromises(); - const rendered = compactRegistration!.slots.session_prompt({}, promptProps) as any; - - const hint = rendered.props.children[1]; - expect(hint.type).toBe("box"); - expect(hint.props).toMatchObject({ - flexDirection: "row", - justifyContent: "flex-end", - gap: 1, - }); - expect(hint.props.children[0].props.children).toBe("Copilot 5h"); - expect(hint.props).not.toHaveProperty("position"); - expect(hint.props).not.toHaveProperty("left"); - expect(hint.props).not.toHaveProperty("bottom"); - expect(hint.props.children[1].props.children).toHaveLength(12); - expect(hint.props.children[2].props.children).toContain(" | "); - expect(hint.props.children[2].props.children).not.toContain("·"); - - expect(api.ui.Prompt).toHaveBeenCalledTimes(2); - expect(api.ui.Prompt).toHaveBeenCalledWith({ - sessionID: "session-1", - visible: false, - disabled: true, - onSubmit, - ref, - }); - }); - - it("renders a structured prompt-bar value without percentage bar cells", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - announcements: { homeBottom: false }, - homeBottom: false, - }); - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { semanticSegment: "Cursor: Known API spend USD 12.50" }, - percentDisplayMode: "remaining", - }, - }); - - await startTui(plugin, api); - const registration = registered.find((item) => item.order === 90)!; - registration.slots.session_prompt({}, { session_id: "session-rich" }); - await flushPromises(); - const rendered = registration.slots.session_prompt({}, { session_id: "session-rich" }) as any; - const hint = rendered.props.children[1]; - - expect(hint.props.children[0].props.children).toBe("Cursor: Known API spend USD 12.50"); - expect(JSON.stringify(hint)).not.toMatch(/[█░▓▒]/u); - }); - - it("renders exact reset and runway text with the 12-cell prompt bar", async () => { - vi.setSystemTime(new Date("2026-01-15T10:00:00.000Z")); - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - announcements: { homeBottom: false }, - homeBottom: false, - }); - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { - name: "OpenAI Weekly", - percentRemaining: 50, - resetTimeIso: "2026-01-17T15:14:00.000Z", - runway: { - kind: "before_reset", - projectedAtIso: "2026-01-15T11:50:00.000Z", - }, - }, - percentDisplayMode: "remaining", - }, - }); - - await startTui(plugin, api); - const registration = registered.find((item) => item.order === 90)!; - registration.slots.session_prompt({}, { session_id: "session-reset" }); - await flushPromises(); - const rendered = registration.slots.session_prompt({}, { session_id: "session-reset" }) as any; - const hint = rendered.props.children[1]; - - expect(hint.props.children[0].props.children).toBe("OpenAI Weekly"); - expect(hint.props.children[1].props.children).toHaveLength(12); - expect(hint.props.children[2].props.children).toBe("50% | 2d5h14m | r/o ≈ 1h 50m"); - }); - - it("keeps the prompt percentage bare while spacing reset units", async () => { - vi.setSystemTime(new Date("2026-01-15T10:00:00.000Z")); - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - announcements: { homeBottom: false }, - homeBottom: false, - }); - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { - name: "OpenAI Weekly", - percentRemaining: 81, - resetTimeIso: "2026-01-17T15:14:00.000Z", - }, - percentDisplayMode: "used", - resetTimeSpaced: true, - }, - }); - - await startTui(plugin, api); - const registration = registered.find((item) => item.order === 90)!; - registration.slots.session_prompt({}, { session_id: "session-spaced-reset" }); - await flushPromises(); - const rendered = registration.slots.session_prompt( - {}, - { session_id: "session-spaced-reset" }, - ) as any; - const hint = rendered.props.children[1]; - - expect(hint.props.children[0].props.children).toBe("OpenAI Weekly"); - expect(hint.props.children[1].props.children).toBe(`██${"░".repeat(10)}`); - expect(hint.props.children[2].props.children).toBe("19% | 2d 5h 14m"); - }); - - it("renders duplicate account identity instead of a window-only label", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - announcements: { homeBottom: false }, - homeBottom: false, - }); - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { - name: "OpenAI 5h", - group: "OpenAI (work)", - label: "5h:", - percentRemaining: 0, - }, - percentDisplayMode: "used", - }, - }); - - await startTui(plugin, api); - const registration = registered.find((item) => item.order === 90)!; - registration.slots.session_prompt({}, { session_id: "session-duplicate" }); - await flushPromises(); - const rendered = registration.slots.session_prompt( - {}, - { session_id: "session-duplicate" }, - ) as any; - const hint = rendered.props.children[1]; - - expect(hint.props.children[0].props.children).toBe("OpenAI (work) 5h"); - expect(hint.props.children[0].props.children).not.toBe("5h"); - expect(hint.props.children[1].props.children).toHaveLength(12); - expect(hint.props.children[2].props.children).toBe("100%"); - }); - - it("clips a long prompt-bar label without growing the 12-cell bar", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - const longGroup = "Alibaba Personal Token Plan With An Extremely Long Account Title"; - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - announcements: { homeBottom: false }, - homeBottom: false, - }); - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { - name: `${longGroup} Weekly`, - group: longGroup, - label: "Weekly:", - percentRemaining: 42, - }, - percentDisplayMode: "remaining", - }, - }); - - await startTui(plugin, api); - const registration = registered.find((item) => item.order === 90)!; - registration.slots.session_prompt({}, { session_id: "session-narrow" }); - await flushPromises(); - const rendered = registration.slots.session_prompt({}, { session_id: "session-narrow" }) as any; - const hint = rendered.props.children[1]; - const label = hint.props.children[0].props.children as string; - - expect(label).toContain("…"); - expect(label.startsWith("Alibaba")).toBe(true); - expect(label).not.toBe("Weekly"); - expect(label.length).toBeLessThanOrEqual(50); - expect(hint.props.children[0].props.wrapMode).toBe("none"); - expect(hint.props.children[1].props.children).toHaveLength(12); - expect(hint.props.children[2].props.children).toBe("42%"); - }); - - it("renders Anthropic Fable instead of a generic weekly window", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - announcements: { homeBottom: false }, - homeBottom: false, - }); - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { - name: "Claude Fable Weekly", - group: "Claude", - label: "Fable:", - percentRemaining: 98, - }, - percentDisplayMode: "remaining", - }, - }); - - await startTui(plugin, api); - const registration = registered.find((item) => item.order === 90)!; - registration.slots.session_prompt({}, { session_id: "session-fable" }); - await flushPromises(); - const rendered = registration.slots.session_prompt({}, { session_id: "session-fable" }) as any; - const hint = rendered.props.children[1]; - - expect(hint.props.children[0].props.children).toBe("Claude Fable"); - expect(hint.props.children[0].props.children).not.toBe("Claude Weekly"); - expect(hint.props.children[1].props.children).toHaveLength(12); - expect(hint.props.children[2].props.children).toBe("98%"); - }); - - it("renders Cursor API instead of a group-only label", async () => { - const plugin = await loadTuiModule(); - const { api, registered } = createApi(); - - resolveTuiSurfaceRegistration.mockResolvedValueOnce({ - commandDisplay: "inline", - sidebar: { enabled: false }, - compact: { - enabled: false, - homeBottom: false, - sessionPrompt: false, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - promptBar: { enabled: true }, - announcements: { homeBottom: false }, - homeBottom: false, - }); - loadTuiSessionQuotaSurfaces.mockResolvedValueOnce({ - sidebar: { status: "disabled", lines: [] }, - compact: { status: "disabled" }, - promptBar: { - status: "ready", - entry: { - name: "Cursor API", - group: "Cursor", - percentRemaining: 25, - }, - percentDisplayMode: "remaining", - }, - }); - - await startTui(plugin, api); - const registration = registered.find((item) => item.order === 90)!; - registration.slots.session_prompt({}, { session_id: "session-cursor-api" }); - await flushPromises(); - const rendered = registration.slots.session_prompt( - {}, - { session_id: "session-cursor-api" }, - ) as any; - const hint = rendered.props.children[1]; - - expect(hint.props.children[0].props.children).toBe("Cursor API"); - expect(hint.props.children[0].props.children).toContain("API"); - expect(hint.props.children[1].props.children).toHaveLength(12); - expect(hint.props.children[2].props.children).toBe("25%"); - }); -}); diff --git a/tests/tui-startup-registration.test.ts b/tests/tui-startup-registration.test.ts deleted file mode 100644 index 7b17e4af..00000000 --- a/tests/tui-startup-registration.test.ts +++ /dev/null @@ -1,462 +0,0 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -const { createTuiQuotaClient, disposeQuotaTelemetryOwner, resolveTuiSurfaceRegistration } = - vi.hoisted(() => ({ - createTuiQuotaClient: vi.fn(() => ({ config: {} })), - disposeQuotaTelemetryOwner: vi.fn(), - resolveTuiSurfaceRegistration: vi.fn(), - })); - -vi.mock("../src/lib/tui-runtime.js", () => ({ - createTuiQuotaClient, - getTuiRuntimeRootHints: vi.fn(() => ({})), - getTuiSessionModelMeta: vi.fn(), - loadTuiHomeBottomStatus: vi.fn(), - loadTuiSessionQuotaSurfaces: vi.fn(), - normalizeTuiSessionID: vi.fn(), - resolveTuiSurfaceRegistration, - writeTuiQuotaExportIfEnabled: vi.fn(), -})); - -vi.mock("../src/lib/quota-telemetry.js", () => ({ - disposeQuotaTelemetryOwner, -})); - -vi.mock("solid-js", () => ({ - Show: vi.fn(), - Index: vi.fn(), - createEffect: vi.fn(), - createSignal: (initial: T) => { - let value = initial; - return [ - () => value, - (next: T | ((previous: T) => T)) => { - value = typeof next === "function" ? (next as (previous: T) => T)(value) : next; - return value; - }, - ]; - }, - onCleanup: vi.fn(), -})); - -vi.mock("@opentui/solid/jsx-runtime", () => ({ - Fragment: Symbol.for("Fragment"), - jsx: vi.fn(), - jsxs: vi.fn(), -})); - -const OBSERVATION_HORIZON_MS = 60_000; -const WARMUP_SAMPLES = 3; -const RECORDED_SAMPLES = 30; - -const FULL_REGISTRATION = { - commandDisplay: "inline", - sidebar: { enabled: true }, - compact: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - hasNativeProviderQuota: false, - suppressedByNativeProviderQuota: false, - }, - announcements: { homeBottom: false }, - homeBottom: true, -} as const; - -type StartupMetric = - | "T_return" - | "T_plan_resolved" - | "T_command" - | "T_sidebar" - | "T_session_prompt" - | "T_home_bottom" - | "T_registration_complete"; - -type RecordedEvent = Exclude; - -type StartupSample = { - events: Partial>; - duplicateRegistrations: string[]; - keymapAttempts: number; -}; - -type Scenario = { - name: string; - config: "resolve" | "reject" | "never"; - delayMs?: number; - disposeAtMs?: number; - registerLayerThrows?: boolean; - completionEvents: RecordedEvent[]; - reportedMetrics: StartupMetric[]; - expected: Partial>; -}; - -type Aggregate = { - min: number | null; - median: number | null; - p95: number | null; - max: number | null; - censored: number; -}; - -type ScenarioResult = { - scenario: Scenario; - samples: StartupSample[]; - aggregates: Map; -}; - -const fullCompletionEvents: RecordedEvent[] = [ - "T_command", - "T_sidebar", - "T_session_prompt", - "T_home_bottom", -]; -const fullReportedMetrics: StartupMetric[] = [ - "T_return", - "T_plan_resolved", - ...fullCompletionEvents, - "T_registration_complete", -]; - -const scenarios: Scenario[] = [ - { - name: "immediate", - config: "resolve", - delayMs: 0, - completionEvents: fullCompletionEvents, - reportedMetrics: fullReportedMetrics, - expected: Object.fromEntries(fullReportedMetrics.map((metric) => [metric, 0])), - }, - { - name: "delay-25", - config: "resolve", - delayMs: 25, - completionEvents: fullCompletionEvents, - reportedMetrics: fullReportedMetrics, - expected: { - T_return: 0, - T_plan_resolved: 25, - T_command: 0, - T_sidebar: 0, - T_session_prompt: 0, - T_home_bottom: 0, - T_registration_complete: 0, - }, - }, - { - name: "delay-2000", - config: "resolve", - delayMs: 2_000, - completionEvents: fullCompletionEvents, - reportedMetrics: fullReportedMetrics, - expected: { - T_return: 0, - T_plan_resolved: 2_000, - T_command: 0, - T_sidebar: 0, - T_session_prompt: 0, - T_home_bottom: 0, - T_registration_complete: 0, - }, - }, - { - name: "never", - config: "never", - completionEvents: fullCompletionEvents, - reportedMetrics: fullReportedMetrics, - expected: { - T_return: 0, - T_plan_resolved: null, - T_command: 0, - T_sidebar: 0, - T_session_prompt: 0, - T_home_bottom: 0, - T_registration_complete: 0, - }, - }, - { - name: "reject-2000", - config: "reject", - delayMs: 2_000, - completionEvents: fullCompletionEvents, - reportedMetrics: fullReportedMetrics, - expected: { - T_return: 0, - T_plan_resolved: 2_000, - T_command: 0, - T_sidebar: 0, - T_session_prompt: 0, - T_home_bottom: 0, - T_registration_complete: 0, - }, - }, - { - name: "dispose-before-resolution", - config: "resolve", - delayMs: 2_000, - disposeAtMs: 100, - completionEvents: fullCompletionEvents, - reportedMetrics: fullReportedMetrics, - expected: { - T_return: 0, - T_plan_resolved: 2_000, - T_command: 0, - T_sidebar: 0, - T_session_prompt: 0, - T_home_bottom: 0, - T_registration_complete: 0, - }, - }, - { - name: "register-layer-throws", - config: "resolve", - delayMs: 0, - registerLayerThrows: true, - completionEvents: fullCompletionEvents, - reportedMetrics: fullReportedMetrics, - expected: { - T_return: 0, - T_plan_resolved: 0, - T_command: null, - T_sidebar: null, - T_session_prompt: null, - T_home_bottom: null, - T_registration_complete: null, - }, - }, -]; - -let plugin: typeof import("../src/tui.tsx")["default"]; -const results: ScenarioResult[] = []; - -function flushPromises(): Promise { - return Promise.resolve() - .then(() => undefined) - .then(() => undefined) - .then(() => undefined); -} - -function metricValue( - sample: StartupSample, - scenario: Scenario, - metric: StartupMetric, -): number | null { - if (metric !== "T_registration_complete") return sample.events[metric] ?? null; - - const values = scenario.completionEvents.map((event) => sample.events[event]); - if (values.some((value) => value === undefined)) return null; - return Math.max(...(values as number[])); -} - -function aggregate(samples: StartupSample[], scenario: Scenario, metric: StartupMetric): Aggregate { - const values = samples - .map((sample) => metricValue(sample, scenario, metric)) - .filter((value): value is number => value !== null) - .sort((left, right) => left - right); - - if (values.length === 0) { - return { min: null, median: null, p95: null, max: null, censored: samples.length }; - } - - const valueAt = (index: number): number => { - const value = values[index]; - if (value === undefined) throw new Error(`Missing aggregate value at index ${index}`); - return value; - }; - const medianIndex = Math.floor(values.length / 2); - const median = - values.length % 2 === 0 - ? (valueAt(medianIndex - 1) + valueAt(medianIndex)) / 2 - : valueAt(medianIndex); - const p95Index = Math.ceil(values.length * 0.95) - 1; - - return { - min: valueAt(0), - median, - p95: valueAt(p95Index), - max: valueAt(values.length - 1), - censored: samples.length - values.length, - }; -} - -async function runSample(scenario: Scenario): Promise { - vi.clearAllTimers(); - vi.setSystemTime(0); - - const events: StartupSample["events"] = {}; - const duplicateRegistrations: string[] = []; - const lifecycleCallbacks: Array<() => void> = []; - let keymapAttempts = 0; - - const record = (event: RecordedEvent) => { - if (events[event] !== undefined) { - duplicateRegistrations.push(event); - return; - } - events[event] = Date.now(); - }; - - const configGet = vi.fn(() => { - if (scenario.config === "never") return new Promise(() => {}); - if (scenario.delayMs === 0 && scenario.config === "resolve") { - return Promise.resolve({ data: {} }); - } - - return new Promise<{ data: Record }>((resolve, reject) => { - setTimeout(() => { - if (scenario.config === "reject") { - reject(new Error("config unavailable")); - return; - } - resolve({ data: {} }); - }, scenario.delayMs ?? 0); - }); - }); - - const api = { - lifecycle: { - onDispose: vi.fn((callback: () => void) => { - lifecycleCallbacks.push(callback); - }), - }, - keymap: { - registerLayer: vi.fn(() => { - keymapAttempts += 1; - if (scenario.registerLayerThrows) throw new Error("registration unavailable"); - record("T_command"); - return vi.fn(); - }), - }, - slots: { - register: vi.fn((registration: { slots: Record }) => { - for (const slotName of Object.keys(registration.slots)) { - if (slotName === "sidebar_content") record("T_sidebar"); - if (slotName === "session_prompt") record("T_session_prompt"); - if (slotName === "home_bottom") record("T_home_bottom"); - } - return "test-slot"; - }), - }, - client: { - config: { get: configGet }, - }, - }; - - resolveTuiSurfaceRegistration.mockImplementationOnce(async (receivedApi: typeof api) => { - try { - await receivedApi.client.config.get(); - } catch (error) { - record("T_plan_resolved"); - throw error; - } - record("T_plan_resolved"); - return FULL_REGISTRATION; - }); - - const returned = plugin.tui(api as never, undefined, {} as never); - void returned.then(() => record("T_return")); - - if (scenario.disposeAtMs !== undefined) { - setTimeout(() => { - for (const callback of [...lifecycleCallbacks]) callback(); - }, scenario.disposeAtMs); - } - - await flushPromises(); - await vi.advanceTimersByTimeAsync(OBSERVATION_HORIZON_MS); - await flushPromises(); - - return { events, duplicateRegistrations, keymapAttempts }; -} - -function formatValue(value: number | null): string { - return value === null ? `>${OBSERVATION_HORIZON_MS}` : String(value); -} - -beforeAll(async () => { - vi.useFakeTimers(); - (globalThis as { React?: unknown }).React = { - createElement: (type: unknown, props: unknown) => ({ type, props }), - }; - plugin = (await import("../src/tui.tsx")).default; -}); - -beforeEach(() => { - vi.useFakeTimers(); -}); - -afterAll(() => { - if (process.env.OPENCODE_QUOTA_TUI_STARTUP_REPORT === "1") { - console.log(`\nTUI startup registration candidate (virtual ms)`); - console.log( - `Warmups: ${WARMUP_SAMPLES}; recorded: ${RECORDED_SAMPLES}; horizon: ${OBSERVATION_HORIZON_MS} ms`, - ); - console.log("| Scenario | Metric | Min | Median | p95 | Max | Censored |"); - console.log("|---|---|---:|---:|---:|---:|---:|"); - for (const result of results) { - for (const metric of result.scenario.reportedMetrics) { - const value = result.aggregates.get(metric); - if (!value) throw new Error(`Missing aggregate for ${result.scenario.name} ${metric}`); - console.log( - `| ${result.scenario.name} | ${metric} | ${formatValue(value.min)} | ${formatValue(value.median)} | ${formatValue(value.p95)} | ${formatValue(value.max)} | ${value.censored} |`, - ); - } - if (result.scenario.name === "dispose-before-resolution") { - console.log("| dispose-before-resolution | late registrations | 0 | 0 | 0 | 0 | 0 |"); - } - } - } - - vi.useRealTimers(); - delete (globalThis as { React?: unknown }).React; -}); - -describe("TUI startup registration timing", () => { - for (const scenario of scenarios) { - it(`${scenario.name}: records deterministic entry-to-registration milestones`, async () => { - for (let index = 0; index < WARMUP_SAMPLES; index += 1) { - await runSample(scenario); - } - - const samples: StartupSample[] = []; - for (let index = 0; index < RECORDED_SAMPLES; index += 1) { - samples.push(await runSample(scenario)); - } - - const aggregates = new Map(); - for (const metric of scenario.reportedMetrics) { - const value = aggregate(samples, scenario, metric); - aggregates.set(metric, value); - - const expected = scenario.expected[metric]; - if (expected === null) { - expect(value).toEqual({ - min: null, - median: null, - p95: null, - max: null, - censored: RECORDED_SAMPLES, - }); - } else { - expect(value).toEqual({ - min: expected, - median: expected, - p95: expected, - max: expected, - censored: 0, - }); - } - } - - expect(samples.every((sample) => sample.duplicateRegistrations.length === 0)).toBe(true); - if (scenario.name === "dispose-before-resolution") { - expect(samples.every((sample) => sample.keymapAttempts === 1)).toBe(true); - } - if (scenario.name === "register-layer-throws") { - expect(samples.every((sample) => sample.keymapAttempts === 1)).toBe(true); - } - - results.push({ scenario, samples, aggregates }); - }); - } -}); diff --git a/tests/tui-startup-runtime-resolution.test.ts b/tests/tui-startup-runtime-resolution.test.ts deleted file mode 100644 index f0071d68..00000000 --- a/tests/tui-startup-runtime-resolution.test.ts +++ /dev/null @@ -1,693 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { readFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { - StartupRuntimePhase, - StartupRuntimeResult, - StartupRuntimeSample, - StartupTraceEvent, -} from "./helpers/tui-startup-runtime-resolution.js"; - -const instrumentation = vi.hoisted(() => ({ - controlledDelayMs: 0, - currentTopPhase: undefined as string | undefined, - currentSessionPhase: undefined as string | undefined, - currentContextPhase: undefined as string | undefined, - trace: [] as Array<{ - sequence: number; - phase: string; - operation: "runtime-context" | "config-load"; - edge: "start" | "complete"; - atMs: number; - configGeneration?: number; - }>, - sessionLoaderInvocations: 0, - homeLoaderInvocations: 0, - sessionFirstStartMs: undefined as number | undefined, - homeFirstStartMs: undefined as number | undefined, - eventTriggerTimes: new Set(), - cleanupFns: [] as Array<() => void>, -})); - -function configGeneration(config: unknown): number | undefined { - const maxWidth = (config as { tuiCompactStatus?: { maxWidth?: unknown } })?.tuiCompactStatus - ?.maxWidth; - return typeof maxWidth === "number" ? maxWidth - 40 : undefined; -} - -function pushTrace( - phase: string, - operation: "runtime-context" | "config-load", - edge: "start" | "complete", - generation?: number, -): void { - instrumentation.trace.push({ - sequence: instrumentation.trace.length, - phase, - operation, - edge, - atMs: Date.now(), - ...(generation === undefined ? {} : { configGeneration: generation }), - }); -} - -vi.mock("../src/lib/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: async (...args: Parameters) => { - const phase = instrumentation.currentContextPhase ?? "refresh-home"; - pushTrace(phase, "config-load", "start"); - await new Promise((resolve) => { - setTimeout(resolve, instrumentation.controlledDelayMs); - }); - const config = await actual.loadConfig(...args); - pushTrace(phase, "config-load", "complete", configGeneration(config)); - return config; - }, - }; -}); - -vi.mock("../src/lib/quota-runtime-context.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveQuotaRuntimeContext: ( - params: Parameters[0], - ) => { - const phase = params.sessionID - ? (instrumentation.currentSessionPhase ?? "refresh-session") - : (instrumentation.currentTopPhase ?? "refresh-home"); - pushTrace(phase, "runtime-context", "start"); - const previous = instrumentation.currentContextPhase; - instrumentation.currentContextPhase = phase; - let result: ReturnType; - try { - result = actual.resolveQuotaRuntimeContext(params); - } finally { - instrumentation.currentContextPhase = previous; - } - return result.then((runtime) => { - pushTrace(phase, "runtime-context", "complete", configGeneration(runtime.config)); - return runtime; - }); - }, - }; -}); - -function classifySessionPhase(): StartupRuntimePhase { - const now = Date.now(); - if (instrumentation.sessionFirstStartMs === undefined) { - instrumentation.sessionFirstStartMs = now; - return "initial-session"; - } - if (instrumentation.eventTriggerTimes.has(now)) return "event-session"; - const elapsed = now - instrumentation.sessionFirstStartMs; - if (elapsed >= 60_000) return "interval-session"; - if (elapsed === 500 || elapsed === 1_500 || elapsed === 4_000) return "recovery-session"; - return "refresh-session"; -} - -function classifyHomePhase(): StartupRuntimePhase { - const now = Date.now(); - if (instrumentation.homeFirstStartMs === undefined) { - instrumentation.homeFirstStartMs = now; - return "initial-home"; - } - if (instrumentation.eventTriggerTimes.has(now)) return "event-home"; - if (now - instrumentation.homeFirstStartMs >= 60_000) return "interval-home"; - return "refresh-home"; -} - -vi.mock("../src/lib/tui-runtime.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveTuiSurfaceRegistration: ( - ...args: Parameters - ) => { - const previous = instrumentation.currentTopPhase; - instrumentation.currentTopPhase = "registration"; - try { - return actual.resolveTuiSurfaceRegistration(...args); - } finally { - instrumentation.currentTopPhase = previous; - } - }, - loadTuiSessionQuotaSurfaces: ( - ...args: Parameters - ) => { - const phase = classifySessionPhase(); - instrumentation.sessionLoaderInvocations += 1; - const previous = instrumentation.currentSessionPhase; - instrumentation.currentSessionPhase = phase; - try { - return actual.loadTuiSessionQuotaSurfaces(...args); - } finally { - instrumentation.currentSessionPhase = previous; - } - }, - loadTuiHomeBottomStatus: (...args: Parameters) => { - const phase = classifyHomePhase(); - instrumentation.homeLoaderInvocations += 1; - const previous = instrumentation.currentTopPhase; - instrumentation.currentTopPhase = phase; - try { - return actual.loadTuiHomeBottomStatus(...args); - } finally { - instrumentation.currentTopPhase = previous; - } - }, - writeTuiQuotaExportIfEnabled: ( - ...args: Parameters - ) => { - const previous = instrumentation.currentTopPhase; - instrumentation.currentTopPhase = "home-export"; - try { - return actual.writeTuiQuotaExportIfEnabled(...args); - } finally { - instrumentation.currentTopPhase = previous; - } - }, - }; -}); - -vi.mock("../src/lib/quota-render-data.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - collectQuotaRenderData: vi.fn(async () => ({ - active: [], - data: { entries: [], errors: [], sessionTokens: undefined }, - })), - }; -}); - -vi.mock("../src/lib/tui-compact-format.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - buildCompactQuotaStatusLine: vi.fn( - (params: { maxWidth?: number }) => `generation-${(params.maxWidth ?? 40) - 40}`, - ), - }; -}); - -vi.mock("../src/lib/tui-sidebar-format.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - buildSidebarQuotaPanelLines: vi.fn( - (params: { config: { tuiCompactStatus: { maxWidth: number } } }) => [ - `generation-${params.config.tuiCompactStatus.maxWidth - 40}`, - ], - ), - }; -}); - -vi.mock("solid-js", () => ({ - Show: (props: { when: unknown; children?: unknown; fallback?: unknown }) => { - if (!props.when) return props.fallback ?? null; - return typeof props.children === "function" - ? (props.children as (value: unknown) => unknown)(props.when) - : props.children; - }, - Index: (props: { - each: unknown[] | undefined | null | false; - children: (item: () => unknown, index: number) => unknown; - }) => { - if (!props.each) return null; - return props.each.map((item, index) => props.children(() => item, index)); - }, - createEffect: (fn: () => void) => fn(), - createSignal: (initial: T) => { - let value = initial; - return [ - () => value, - (next: T | ((previous: T) => T)) => { - value = typeof next === "function" ? (next as (previous: T) => T)(value) : next; - return value; - }, - ]; - }, - onCleanup: (fn: () => void) => { - instrumentation.cleanupFns.push(fn); - }, -})); - -vi.mock("@opentui/solid/jsx-runtime", () => ({ - Fragment: Symbol.for("Fragment"), - jsx: (type: unknown, props: Record) => - typeof type === "function" ? type(props) : { type, props }, - jsxs: (type: unknown, props: Record) => - typeof type === "function" ? type(props) : { type, props }, -})); - -import { - formatStartupRuntimeReport, - runStartupRuntimeSamples, - TUI_RUNTIME_RESOLUTION_HORIZON_MS, -} from "./helpers/tui-startup-runtime-resolution.js"; - -const originalCwd = process.cwd(); -const originalEnv = { ...process.env }; -const reportResults: StartupRuntimeResult[] = []; -const plugin = (await import("../src/tui.tsx")).default; - -function createElement( - type: unknown, - props: Record | null, - ...children: unknown[] -) { - const nextProps = { - ...(props ?? {}), - ...(children.length === 0 ? {} : { children: children.length === 1 ? children[0] : children }), - }; - return typeof type === "function" ? type(nextProps) : { type, props: nextProps }; -} - -function writeGenerationConfig(worktreeDir: string, generation: number): void { - writeFileSync( - join(worktreeDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - onlyCurrentModel: false, - export: { enabled: false }, - maintainerAnnouncements: { enabled: false, home: false }, - tuiSidebarPanel: { enabled: true }, - tuiPromptBar: { enabled: false }, - tuiCompactStatus: { - enabled: true, - homeBottom: true, - sessionPrompt: true, - suppressWhenNativeProviderQuota: false, - maxWidth: 40 + generation, - }, - }, - }, - }), - "utf8", - ); -} - -function createApi(worktreeDir: string) { - const registered: Array<{ - order?: number; - slots: Record) => unknown>; - }> = []; - const lifecycleCallbacks: Array<() => void> = []; - const eventHandlers = new Map void>>(); - const api = { - route: { current: { name: "session", params: { sessionID: "session-1" } } }, - state: { - provider: [], - path: { worktree: worktreeDir, directory: worktreeDir }, - session: { messages: () => [] }, - }, - theme: { current: { text: "text", textMuted: "muted" } }, - ui: { - Prompt: (props: Record) => ({ type: "Prompt", props }), - DialogPrompt: (props: Record) => ({ type: "DialogPrompt", props }), - dialog: { setSize: vi.fn(), replace: vi.fn(), clear: vi.fn() }, - toast: vi.fn(), - }, - kv: { get: vi.fn((_key: string, fallback: unknown) => fallback), set: vi.fn() }, - event: { - on: vi.fn((eventName: string, handler: (event: unknown) => void) => { - const handlers = eventHandlers.get(eventName) ?? []; - handlers.push(handler); - eventHandlers.set(eventName, handlers); - return vi.fn(); - }), - }, - slots: { - register: vi.fn( - (registration: { - order?: number; - slots: Record) => unknown>; - }) => { - registered.push(registration); - return `slot-${registered.length}`; - }, - ), - }, - lifecycle: { - onDispose: vi.fn((callback: () => void) => { - lifecycleCallbacks.push(callback); - }), - }, - keymap: { registerLayer: vi.fn(() => vi.fn()) }, - client: { - app: { log: vi.fn().mockResolvedValue(undefined) }, - config: { get: vi.fn().mockResolvedValue({ data: {} }) }, - session: { prompt: vi.fn(), command: vi.fn() }, - }, - }; - return { api, registered, lifecycleCallbacks, eventHandlers }; -} - -function containsText(value: unknown, expected: string, seen = new Set()): boolean { - if (typeof value === "string") return value.includes(expected); - if (!value || typeof value !== "object" || seen.has(value)) return false; - seen.add(value); - return Object.values(value as Record).some((child) => - Array.isArray(child) - ? child.some((item) => containsText(item, expected, seen)) - : containsText(child, expected, seen), - ); -} - -async function flushAsyncWork(configPath: string): Promise { - for (let index = 0; index < 4; index += 1) { - await readFile(configPath, "utf8"); - await Promise.resolve(); - await Promise.resolve(); - } -} - -function resetInstrumentation(controlledDelayMs: number): void { - instrumentation.controlledDelayMs = controlledDelayMs; - instrumentation.currentTopPhase = undefined; - instrumentation.currentSessionPhase = undefined; - instrumentation.currentContextPhase = undefined; - instrumentation.trace.length = 0; - instrumentation.sessionLoaderInvocations = 0; - instrumentation.homeLoaderInvocations = 0; - instrumentation.sessionFirstStartMs = undefined; - instrumentation.homeFirstStartMs = undefined; - instrumentation.eventTriggerTimes.clear(); - instrumentation.cleanupFns.length = 0; -} - -type StartedScenario = { - worktreeDir: string; - configPath: string; - api: ReturnType["api"]; - registered: ReturnType["registered"]; - lifecycleCallbacks: Array<() => void>; - eventHandlers: ReturnType["eventHandlers"]; - mountAtMs: number; - milestones: StartupRuntimeSample["milestones"]; - rendered: StartupRuntimeSample["rendered"]; - observeGeneration: (generation: number) => StartupRuntimeSample["rendered"]; - advanceTo: (targetMs: number) => Promise; - emit: (eventName: string, event: unknown) => void; - cleanup: () => void; -}; - -async function startScenario(controlledDelayMs: number): Promise { - vi.clearAllTimers(); - vi.setSystemTime(0); - resetInstrumentation(controlledDelayMs); - - const tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-tui-resolution-")); - const worktreeDir = join(tempDir, "worktree"); - const xdgConfigHome = join(tempDir, "xdg-config"); - mkdirSync(worktreeDir, { recursive: true }); - mkdirSync(join(xdgConfigHome, "opencode"), { recursive: true }); - process.env.HOME = tempDir; - process.env.XDG_CONFIG_HOME = xdgConfigHome; - process.env.XDG_DATA_HOME = join(tempDir, "xdg-data"); - process.env.XDG_CACHE_HOME = join(tempDir, "xdg-cache"); - process.env.XDG_STATE_HOME = join(tempDir, "xdg-state"); - delete process.env.OPENCODE_CONFIG_DIR; - writeGenerationConfig(worktreeDir, 1); - const configPath = join(worktreeDir, "opencode.json"); - const { api, registered, lifecycleCallbacks, eventHandlers } = createApi(worktreeDir); - - await plugin.tui(api as never, undefined, {} as never); - const sidebarRegistration = registered.find((entry) => entry.order === 150); - const compactRegistration = registered.find((entry) => entry.order === 90); - if (!sidebarRegistration || !compactRegistration) { - throw new Error("TUI startup harness did not receive the expected slot registrations"); - } - const sidebarSlot = () => sidebarRegistration.slots.sidebar_content; - const compactSlots = () => compactRegistration.slots; - - expect(sidebarSlot()({}, { session_id: "session-1" })).toBeNull(); - expect(compactSlots().session_prompt({}, { session_id: "session-1" })).toBeNull(); - expect(compactSlots().home_bottom({}, {})).toBeNull(); - - await vi.advanceTimersToNextTimerAsync(); - await flushAsyncWork(configPath); - const mountAtMs = Date.now(); - - sidebarSlot()({}, { session_id: "session-1" }); - compactSlots().session_prompt({}, { session_id: "session-1" }); - compactSlots().home_bottom({}, {}); - - const milestones: StartupRuntimeSample["milestones"] = { - sidebarMs: null, - compactMs: null, - homeMs: null, - firstContentMs: null, - allInitialContentMs: null, - }; - const rendered = { sidebar: false, compact: false, home: false }; - const observeGeneration = (generation: number) => { - const expected = `generation-${generation}`; - const observed = { - sidebar: containsText(sidebarSlot()({}, { session_id: "session-1" }), expected), - compact: containsText( - compactSlots().session_prompt({}, { session_id: "session-1" }), - expected, - ), - home: containsText(compactSlots().home_bottom({}, {}), expected), - }; - return observed; - }; - - while ( - milestones.allInitialContentMs === null && - Date.now() < TUI_RUNTIME_RESOLUTION_HORIZON_MS - ) { - await flushAsyncWork(configPath); - const observed = observeGeneration(1); - for (const key of ["sidebar", "compact", "home"] as const) { - if (!rendered[key] && observed[key]) { - rendered[key] = true; - milestones[`${key}Ms` as "sidebarMs" | "compactMs" | "homeMs"] = Date.now(); - } - } - const accepted = [milestones.sidebarMs, milestones.compactMs, milestones.homeMs]; - const recorded = accepted.filter((value): value is number => value !== null); - milestones.firstContentMs = recorded.length > 0 ? Math.min(...recorded) : null; - milestones.allInitialContentMs = recorded.length === 3 ? Math.max(...recorded) : null; - if (milestones.allInitialContentMs === null) { - await vi.advanceTimersToNextTimerAsync(); - } - } - - return { - worktreeDir, - configPath, - api, - registered, - lifecycleCallbacks, - eventHandlers, - mountAtMs, - milestones, - rendered, - observeGeneration, - advanceTo: async (targetMs: number) => { - if (targetMs < Date.now()) - throw new Error(`Cannot move virtual time backwards to ${targetMs}`); - await vi.advanceTimersByTimeAsync(targetMs - Date.now()); - await flushAsyncWork(configPath); - }, - emit: (eventName: string, event: unknown) => { - for (const handler of eventHandlers.get(eventName) ?? []) handler(event); - }, - cleanup: () => { - for (const cleanup of instrumentation.cleanupFns.splice(0).reverse()) cleanup(); - for (const callback of lifecycleCallbacks.splice(0).reverse()) callback(); - vi.clearAllTimers(); - process.chdir(originalCwd); - process.env = { ...originalEnv }; - rmSync(tempDir, { recursive: true, force: true }); - }, - }; -} - -async function runInitialSample( - controlledDelayMs: number, - _sampleIndex: number, -): Promise { - const scenario = await startScenario(controlledDelayMs); - try { - return { - controlledDelayMs, - milestones: { ...scenario.milestones }, - trace: instrumentation.trace.map((event) => ({ - ...event, - phase: event.phase as StartupRuntimePhase, - })) as StartupTraceEvent[], - sessionLoaderInvocations: instrumentation.sessionLoaderInvocations, - homeLoaderInvocations: instrumentation.homeLoaderInvocations, - rendered: { ...scenario.rendered }, - }; - } finally { - scenario.cleanup(); - } -} - -beforeAll(() => { - vi.useFakeTimers(); - (globalThis as { React?: unknown }).React = { createElement }; -}); - -beforeEach(() => { - vi.useFakeTimers(); -}); - -afterEach(() => { - for (const cleanup of instrumentation.cleanupFns.splice(0).reverse()) cleanup(); - vi.clearAllTimers(); -}); - -afterAll(() => { - if (process.env.OPENCODE_QUOTA_TUI_RUNTIME_RESOLUTION_REPORT === "1") { - console.log(`\n${formatStartupRuntimeReport(reportResults)}`); - } - process.chdir(originalCwd); - process.env = { ...originalEnv }; - vi.useRealTimers(); - delete (globalThis as { React?: unknown }).React; -}); - -describe("TUI startup runtime/config resolution", () => { - it("measures deterministic 25 ms and 100 ms startup samples through real TUI slots", async () => { - for (const controlledDelayMs of [25, 100]) { - const result = await runStartupRuntimeSamples(controlledDelayMs, runInitialSample); - reportResults.push(result); - - expect(result.distinctCountVectors).toHaveLength(1); - expect(result.milestones.firstContentMs).toMatchObject({ - median: controlledDelayMs, - p95: controlledDelayMs, - }); - expect(result.milestones.allInitialContentMs).toMatchObject({ - median: controlledDelayMs, - p95: controlledDelayMs, - }); - expect(result.countsAtMilestones.allInitialContentMs).toEqual( - Array.from({ length: 30 }, () => ({ - runtimeStarts: 3, - runtimeCompletions: 3, - configStarts: 1, - configCompletions: 1, - })), - ); - for (const metric of Object.values(result.milestones)) { - expect(metric.censored).toBe(0); - } - expect(result.samples.every((sample) => Object.values(sample.rendered).every(Boolean))).toBe( - true, - ); - expect( - result.samples.every((sample) => - ["registration", "initial-session", "initial-home", "home-export"].every((phase) => - sample.trace.some( - (event) => event.phase === phase && event.operation === "runtime-context", - ), - ), - ), - ).toBe(true); - } - }); - - it("shares the initial sidebar and compact load, then rereads config on mount recovery", async () => { - const scenario = await startScenario(25); - try { - expect(scenario.rendered).toEqual({ sidebar: true, compact: true, home: true }); - expect(instrumentation.sessionLoaderInvocations).toBe(1); - writeGenerationConfig(scenario.worktreeDir, 2); - - await scenario.advanceTo(scenario.mountAtMs + 500); - await scenario.advanceTo(scenario.mountAtMs + 525); - - expect(scenario.observeGeneration(2)).toMatchObject({ sidebar: true, compact: true }); - expect( - instrumentation.trace.some( - (event) => - event.phase === "recovery-session" && - event.operation === "config-load" && - event.edge === "complete" && - event.configGeneration === 2, - ), - ).toBe(true); - } finally { - scenario.cleanup(); - } - }); - - it("rereads config for the 150 ms and 600 ms event refreshes", async () => { - const scenario = await startScenario(25); - try { - writeGenerationConfig(scenario.worktreeDir, 2); - const eventAtMs = Date.now(); - instrumentation.eventTriggerTimes.add(eventAtMs + 150); - instrumentation.eventTriggerTimes.add(eventAtMs + 600); - scenario.emit("session.updated", { properties: { info: { id: "session-1" } } }); - - await scenario.advanceTo(eventAtMs + 150); - await scenario.advanceTo(eventAtMs + 175); - expect(scenario.observeGeneration(2)).toEqual({ sidebar: true, compact: true, home: true }); - - await scenario.advanceTo(scenario.mountAtMs + 500); - await scenario.advanceTo(scenario.mountAtMs + 525); - writeGenerationConfig(scenario.worktreeDir, 3); - await scenario.advanceTo(eventAtMs + 600); - await scenario.advanceTo(eventAtMs + 625); - expect(scenario.observeGeneration(3)).toEqual({ sidebar: true, compact: true, home: true }); - - for (const phase of ["event-session", "event-home"] as const) { - expect( - instrumentation.trace.some( - (event) => - event.phase === phase && - event.operation === "config-load" && - event.edge === "complete" && - event.configGeneration === 3, - ), - ).toBe(true); - } - } finally { - scenario.cleanup(); - } - }); - - it("rereads current config on the 60 second interval after recovery loads", async () => { - const scenario = await startScenario(25); - try { - writeGenerationConfig(scenario.worktreeDir, 2); - for (const recoveryDelay of [500, 1_500, 4_000]) { - await scenario.advanceTo(scenario.mountAtMs + recoveryDelay); - await scenario.advanceTo(scenario.mountAtMs + recoveryDelay + 25); - } - writeGenerationConfig(scenario.worktreeDir, 3); - - await scenario.advanceTo(scenario.mountAtMs + 60_000); - await scenario.advanceTo(scenario.mountAtMs + 60_025); - expect(scenario.observeGeneration(3)).toEqual({ sidebar: true, compact: true, home: true }); - - for (const phase of ["interval-session", "interval-home"] as const) { - expect( - instrumentation.trace.some( - (event) => - event.phase === phase && - event.operation === "config-load" && - event.edge === "complete" && - event.configGeneration === 3, - ), - ).toBe(true); - } - } finally { - scenario.cleanup(); - } - }); -}); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index a126dbdc..0be534a9 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -16,7 +16,6 @@ import { createConfigModuleMock, createPluginRuntimePathsMockModule, createPluginTestClient, - createPluginToolMockModule, createPricingModuleMock, createProvidersRegistryModuleMock, createSessionTokensModuleMock, @@ -72,7 +71,6 @@ const otel = vi.hoisted(() => { }; }); -vi.mock("@opencode-ai/plugin", () => createPluginToolMockModule()); vi.mock("@opentelemetry/api", () => ({ metrics: { getMeter: otel.getMeter }, })); @@ -111,6 +109,34 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT, { includeCandidates: true }), ); +const renderedSurfaces = vi.hoisted(() => ({ sidebar: [] as string[], compact: [] as string[] })); +vi.mock("../src/lib/tui-sidebar-format.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + buildSidebarQuotaPanelLines: ( + params: Parameters[0], + ) => { + const lines = actual.buildSidebarQuotaPanelLines(params); + renderedSurfaces.sidebar.push(lines.join("\n")); + return lines; + }, + }; +}); +vi.mock("../src/lib/tui-compact-format.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + buildCompactQuotaStatusLine: ( + params: Parameters[0], + ) => { + const line = actual.buildCompactQuotaStatusLine(params); + renderedSurfaces.compact.push(line); + return line; + }, + }; +}); + type RegisteredTool = { name: string; execute(input: object, context: { sessionID: string }): Promise<{ content: string }>; @@ -142,6 +168,11 @@ async function setupV2Surfaces(client: ReturnType, provider const alert = vi.fn(async (_input: { title: string; message: string }) => {}); const toast = vi.fn(); const slots: string[] = []; + const renderers = new Map unknown>(); + vi.stubGlobal("React", { + createElement: (type: unknown, props: Record | null) => + typeof type === "function" ? type(props ?? {}) : { type, props }, + }); const { default: tuiPlugin } = await import("../src/tui-v2.js"); const dispose = tuiPlugin.setup({ client, @@ -162,8 +193,9 @@ async function setupV2Surfaces(client: ReturnType, provider }, }, ui: { - slot: (claim: { append: string; render: () => unknown }) => { + slot: (claim: { append: string; render: (props?: { sessionID: string }) => unknown }) => { slots.push(claim.append); + renderers.set(claim.append, claim.render); if (claim.append === "app") claim.render(); return () => {}; }, @@ -175,12 +207,26 @@ async function setupV2Surfaces(client: ReturnType, provider const quota = commands.find((command) => command.slash.name === "quota"); expect(quota).toBeDefined(); expect(commands.some((command) => command.slash.name === "quota_status")).toBe(true); + const renderSurface = async ( + append: "sidebar.content" | "prompt.footer" | "home.footer.status", + output: string[], + props?: { sessionID: string }, + ): Promise => { + output.length = 0; + renderers.get(append)?.(props); + await vi.waitFor(() => expect(output).toHaveLength(1)); + return output[0]; + }; return { tool: tool!, alert, toast, events, quota: quota!, + renderSidebar: (sessionID: string) => + renderSurface("sidebar.content", renderedSurfaces.sidebar, { sessionID }), + renderSessionPrompt: () => renderSurface("prompt.footer", renderedSurfaces.compact), + renderHomeBottom: () => renderSurface("home.footer.status", renderedSurfaces.compact), dispose: dispose as () => void, }; } @@ -555,47 +601,11 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expectedConsumed.set(key, Math.max(expectedConsumed.get(key) ?? 0, consumed)); } - const tuiApi = { - state: { - provider: PHASE5_RUNTIME_PROVIDER_IDS.map((id) => ({ id })), - path: { worktree: process.cwd(), directory: process.cwd() }, - session: { messages: () => [] }, - }, - client, - } as never; - - const { loadTuiHomeBottomStatus, loadTuiSessionQuotaSurfaces, resolveTuiSurfaceRegistration } = - await import("../src/lib/tui-runtime.js"); - - const registration = await resolveTuiSurfaceRegistration(tuiApi); - expect(registration).toEqual( - expect.objectContaining({ - sidebar: { enabled: true }, - compact: expect.objectContaining({ - enabled: true, - homeBottom: true, - sessionPrompt: true, - suppressedByNativeProviderQuota: false, - }), - homeBottom: true, - }), - ); - - const allWindows = await loadTuiSessionQuotaSurfaces({ - api: tuiApi, - sessionID: "phase5-session", - }); - expect(allWindows.sidebar.status).toBe("ready"); - expect(allWindows.compact.status).toBe("ready"); - const allWindowsSidebar = [ - ...allWindows.sidebar.lines, - ...(allWindows.sidebar.linesExpanded ?? []), - ].join("\n"); + const allWindowsSidebar = await v2.renderSidebar("phase5-session"); assertFixtureContent(allWindowsSidebar); assertTreeSessionTokenTotals(allWindowsSidebar); expect(allWindowsSidebar).toContain("tree-model"); - const sessionPromptCompact = - allWindows.compact.status === "ready" ? allWindows.compact.text : ""; + const sessionPromptCompact = await v2.renderSessionPrompt(); expect(sessionPromptCompact).toContain("64%"); expect(sessionPromptCompact).toContain("$12.34"); expect(sessionPromptCompact).toContain("80%"); @@ -604,21 +614,13 @@ describe("v4 Phase 5 cross-surface release evidence", () => { assertTreeSessionTokenTotals(sessionPromptCompact); assertPhase5CanariesRedacted(sessionPromptCompact); - const homeBottom = await loadTuiHomeBottomStatus({ api: tuiApi }); - expect(homeBottom.status).toBe("ready"); - const homeCompact = homeBottom.compact.status === "ready" ? homeBottom.compact.text : ""; + const homeCompact = await v2.renderHomeBottom(); expect(homeCompact).toBe(sessionPromptCompact.replace(/ \| tok [^|]+(?= \|)/u, "")); expect(homeCompact).not.toContain("tok "); assertPhase5CanariesRedacted(homeCompact); currentConfig = configFor("singleWindow"); - const singleWindow = await loadTuiSessionQuotaSurfaces({ - api: tuiApi, - sessionID: "phase5-session", - }); - expect(singleWindow.sidebar.status).toBe("ready"); - expect(singleWindow.compact.status).toBe("ready"); - const singleWindowSidebar = singleWindow.sidebar.lines.join("\n"); + const singleWindowSidebar = await v2.renderSidebar("phase5-session"); expect(singleWindowSidebar).toContain("64%"); expect(singleWindowSidebar).toContain("80%"); expect(singleWindowSidebar).toContain("HTTP 503"); @@ -635,9 +637,10 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const allOutput = JSON.stringify({ serverOutput, toastOutput, - allWindows, - homeBottom, - singleWindow, + allWindowsSidebar, + sessionPromptCompact, + homeCompact, + singleWindowSidebar, }); assertPhase5CanariesRedacted(allOutput); for (const source of PHASE5_QUOTA_PROVIDERS) { @@ -782,25 +785,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(toastOutput).toContain("Remaining: -5 requests"); expect(toastOutput).toContain("Remaining: -20 requests"); - const tuiApi = { - state: { - provider: [{ id: "minimax-coding-plan" }], - path: { worktree: process.cwd(), directory: process.cwd() }, - session: { messages: () => [] }, - }, - client, - } as never; - const { loadTuiSessionQuotaSurfaces } = await import("../src/lib/tui-runtime.js"); - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: tuiApi, - sessionID: "minimax-session", - }); - - expect(surfaces.sidebar.status).toBe("ready"); - const sidebarOutput = [ - ...surfaces.sidebar.lines, - ...(surfaces.sidebar.linesExpanded ?? []), - ].join("\n"); + const sidebarOutput = await v2.renderSidebar("minimax-session"); expect(sidebarOutput).toContain("MiniMax Token Plan"); expect(sidebarOutput).toContain("5h"); expect(sidebarOutput).toContain("Weekly"); @@ -808,8 +793,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(sidebarOutput).toContain("Remaining: -5 requests"); expect(sidebarOutput).toContain("Remaining: -20 requests"); - expect(surfaces.compact.status).toBe("ready"); - const compactOutput = surfaces.compact.status === "ready" ? surfaces.compact.text : ""; + const compactOutput = await v2.renderSessionPrompt(); expect(compactOutput.match(/0%/gu)).toHaveLength(2); expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(1); @@ -863,30 +847,11 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(toastOutput).toContain("Fable"); expect(toastOutput).toContain("98%"); - const tuiApi = { - state: { - provider: [{ id: "anthropic" }], - path: { worktree: process.cwd(), directory: process.cwd() }, - session: { messages: () => [] }, - }, - client, - } as never; - const { loadTuiSessionQuotaSurfaces } = await import("../src/lib/tui-runtime.js"); - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: tuiApi, - sessionID: "anthropic-fable-session", - }); - - expect(surfaces.sidebar.status).toBe("ready"); - const sidebarOutput = [ - ...surfaces.sidebar.lines, - ...(surfaces.sidebar.linesExpanded ?? []), - ].join("\n"); + const sidebarOutput = await v2.renderSidebar("anthropic-fable-session"); expect(sidebarOutput).toContain("Fable"); expect(sidebarOutput).toContain("98%"); - expect(surfaces.compact.status).toBe("ready"); - const compactOutput = surfaces.compact.status === "ready" ? surfaces.compact.text : ""; + const compactOutput = await v2.renderSessionPrompt(); expect(compactOutput).toContain("Fable"); expect(compactOutput).toContain("98%"); @@ -942,25 +907,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(toastOutput).toContain("46%"); expect(toastOutput).not.toContain("video"); - const tuiApi = { - state: { - provider: [{ id: "minimax-china-coding-plan" }], - path: { worktree: process.cwd(), directory: process.cwd() }, - session: { messages: () => [] }, - }, - client, - } as never; - const { loadTuiSessionQuotaSurfaces } = await import("../src/lib/tui-runtime.js"); - const surfaces = await loadTuiSessionQuotaSurfaces({ - api: tuiApi, - sessionID: "minimax-china-session", - }); - - expect(surfaces.sidebar.status).toBe("ready"); - const sidebarOutput = [ - ...surfaces.sidebar.lines, - ...(surfaces.sidebar.linesExpanded ?? []), - ].join("\n"); + const sidebarOutput = await v2.renderSidebar("minimax-china-session"); expect(sidebarOutput).toContain("MiniMax Token Plan"); expect(sidebarOutput).toContain("(CN)"); expect(sidebarOutput).toContain("5h"); @@ -969,8 +916,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(sidebarOutput).toContain("46%"); expect(sidebarOutput).not.toContain("video"); - expect(surfaces.compact.status).toBe("ready"); - const compactOutput = surfaces.compact.status === "ready" ? surfaces.compact.text : ""; + const compactOutput = await v2.renderSessionPrompt(); expect(compactOutput).toContain("33%"); expect(compactOutput).toContain("46%"); expect(compactOutput).not.toContain("video"); diff --git a/tsconfig.json b/tsconfig.json index 816538ca..9ba8fb42 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,6 @@ "resolveJsonModule": true, "isolatedModules": true }, - "include": ["src/**/*", "src/types/**/*"], + "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } From e0c6c3aaa6f25e50b57815a709a78ff8e2d9527b Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:30:26 +0200 Subject: [PATCH 26/97] refactor(auth)!: read every OpenCode credential from the database Remove getAuthPaths/getAuthPath. Cursor detection now reads the cursor credential row from OpenCode's credential database instead of OpenCode's auth.json; Cursor's own auth files are still checked. OpenCode 2 imports auth.json once into the credential table, so the plugin never reads it. Tests drop the remaining auth.json diagnostics fixtures. --- src/lib/cursor-detection.ts | 45 +++++++---------- src/lib/opencode-auth.ts | 17 ------- tests/lib.cursor-detection.test.ts | 57 ++++++++++++++++------ tests/lib.provider-api-key-configs.test.ts | 2 +- tests/lib.quota-export.test.ts | 2 +- tests/lib.quota-status.test.ts | 10 ++-- 6 files changed, 67 insertions(+), 66 deletions(-) diff --git a/src/lib/cursor-detection.ts b/src/lib/cursor-detection.ts index 461dcb47..a483818e 100644 --- a/src/lib/cursor-detection.ts +++ b/src/lib/cursor-detection.ts @@ -4,10 +4,10 @@ import { homedir, platform } from "os"; import { join } from "path"; import { CURSOR_LEGACY_PROVIDER_ID } from "./cursor-pricing.js"; import { parseJsonOrJsonc } from "./jsonc.js"; -import { getAuthPaths } from "./opencode-auth.js"; +import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; import { getQuotaProviderRuntimeIds } from "./provider-metadata.js"; -import type { AuthData, CursorOAuthAuthData } from "./types.js"; +import type { CursorOAuthAuthData } from "./types.js"; export interface CursorAuthPresence { state: "missing" | "present" | "invalid"; @@ -86,37 +86,28 @@ function isValidCursorOAuthEntry(value: unknown): value is CursorOAuthAuthData { } export async function inspectCursorAuthPresence(): Promise { - const authCandidatePaths = getAuthPaths(); + const credentialDatabasePaths = getCredentialDatabasePaths(); const legacyCandidatePaths = getCursorAuthCandidatePaths(); - const candidatePaths = dedupe([...authCandidatePaths, ...legacyCandidatePaths]); + const candidatePaths = dedupe([...credentialDatabasePaths, ...legacyCandidatePaths]); const presentPaths = candidatePaths.filter((path) => existsSync(path)); + const credentialDatabasePath = credentialDatabasePaths.find((path) => existsSync(path)); let invalidPath: string | undefined; let invalidError: string | undefined; - for (const path of authCandidatePaths) { - if (!existsSync(path)) continue; - - try { - const raw = await readFile(path, "utf8"); - const parsed = JSON.parse(raw) as AuthData; - const cursorAuth = parsed?.cursor; - - if (!cursorAuth) continue; - if (isValidCursorOAuthEntry(cursorAuth)) { - return { - state: "present", - selectedPath: path, - presentPaths, - candidatePaths, - }; - } - - invalidPath ??= path; - invalidError ??= "Cursor auth entry in auth.json is missing a valid oauth token payload"; - } catch (error) { - invalidPath ??= path; - invalidError ??= error instanceof Error ? error.message : String(error); + const cursorAuth = (await readAuthFile())?.cursor; + if (cursorAuth) { + if (isValidCursorOAuthEntry(cursorAuth)) { + return { + state: "present", + selectedPath: credentialDatabasePath, + presentPaths, + candidatePaths, + }; } + + invalidPath = credentialDatabasePath; + invalidError = + "Cursor credential in the OpenCode database is missing a valid oauth token payload"; } for (const path of legacyCandidatePaths) { diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 6734c2a0..56f6b623 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -73,23 +73,6 @@ export function formatCredentialDisplayNames( let authCache: AuthCacheEntry | null = null; -/** - * Get candidate legacy auth.json paths for Cursor OAuth compatibility. - * Some OpenCode installations use Linux-style paths even on macOS, so we - * check multiple locations. - */ -export function getAuthPaths(): string[] { - // Generate legacy candidates from OpenCode runtime dir semantics (xdg-basedir) - // plus platform fallbacks for alternate installs. - const { dataDirs } = getOpencodeRuntimeDirCandidates(); - return dataDirs.map((d) => join(d, "auth.json")); -} - -/** Returns the primary legacy auth.json path used for Cursor OAuth compatibility. */ -export function getAuthPath(): string { - return join(getOpencodeRuntimeDirs().dataDir, "auth.json"); -} - /** * Get candidate OpenCode credential database paths in priority order. * diff --git a/tests/lib.cursor-detection.test.ts b/tests/lib.cursor-detection.test.ts index f4471596..28fbf188 100644 --- a/tests/lib.cursor-detection.test.ts +++ b/tests/lib.cursor-detection.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const { mockFiles, testPaths } = vi.hoisted(() => { +const { mockFiles, mockAuth, testPaths } = vi.hoisted(() => { const separator = process.platform === "win32" ? "\\" : "/"; const join = (...parts: string[]) => parts.join(separator); const root = join(process.cwd(), ".cursor-detection-test"); @@ -8,9 +8,10 @@ const { mockFiles, testPaths } = vi.hoisted(() => { const config = join(root, "config"); return { mockFiles: new Map(), + mockAuth: { value: null as Record | null }, testPaths: { home, - auth: join(root, "auth.json"), + credentialDatabase: join(root, "opencode.db"), cursorAuth: join(home, ".config", "cursor", "auth.json"), config, opencodeConfig: join(config, "opencode.json"), @@ -44,7 +45,8 @@ vi.mock("os", async () => { }); vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPaths: () => [testPaths.auth], + getCredentialDatabasePaths: () => [testPaths.credentialDatabase], + readAuthFile: vi.fn(async () => mockAuth.value), })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ @@ -59,31 +61,56 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ describe("cursor detection", () => { beforeEach(() => { mockFiles.clear(); + mockAuth.value = null; vi.resetModules(); delete process.env.CURSOR_ACP_HOME_DIR; }); - it("prefers Cursor OAuth auth in OpenCode auth.json", async () => { - mockFiles.set( - testPaths.auth, - JSON.stringify({ - cursor: { - type: "oauth", - refresh: "refresh-token", - }, - }), - ); + it("prefers the Cursor OAuth credential in the OpenCode database", async () => { + mockFiles.set(testPaths.credentialDatabase, ""); + mockAuth.value = { + cursor: { + type: "oauth", + refresh: "refresh-token", + }, + }; mockFiles.set(testPaths.cursorAuth, JSON.stringify({ accessToken: "legacy-token" })); const { inspectCursorAuthPresence } = await import("../src/lib/cursor-detection.js"); const result = await inspectCursorAuthPresence(); expect(result.state).toBe("present"); - expect(result.selectedPath).toBe(testPaths.auth); - expect(result.presentPaths).toContain(testPaths.auth); + expect(result.selectedPath).toBe(testPaths.credentialDatabase); + expect(result.presentPaths).toContain(testPaths.credentialDatabase); expect(result.presentPaths).toContain(testPaths.cursorAuth); }); + it("falls back to Cursor's own auth file when the database credential is invalid", async () => { + mockFiles.set(testPaths.credentialDatabase, ""); + mockAuth.value = { cursor: { type: "oauth" } }; + mockFiles.set(testPaths.cursorAuth, JSON.stringify({ accessToken: "legacy-token" })); + + const { inspectCursorAuthPresence } = await import("../src/lib/cursor-detection.js"); + const result = await inspectCursorAuthPresence(); + + expect(result.state).toBe("present"); + expect(result.selectedPath).toBe(testPaths.cursorAuth); + }); + + it("reports an invalid Cursor credential in the OpenCode database", async () => { + mockFiles.set(testPaths.credentialDatabase, ""); + mockAuth.value = { cursor: { type: "oauth" } }; + + const { inspectCursorAuthPresence } = await import("../src/lib/cursor-detection.js"); + const result = await inspectCursorAuthPresence(); + + expect(result.state).toBe("invalid"); + expect(result.selectedPath).toBe(testPaths.credentialDatabase); + expect(result.error).toBe( + "Cursor credential in the OpenCode database is missing a valid oauth token payload", + ); + }); + it("detects the canonical Cursor companion package and provider.cursor config", async () => { mockFiles.set( testPaths.opencodeConfig, diff --git a/tests/lib.provider-api-key-configs.test.ts b/tests/lib.provider-api-key-configs.test.ts index a7556eb1..a770a6cb 100644 --- a/tests/lib.provider-api-key-configs.test.ts +++ b/tests/lib.provider-api-key-configs.test.ts @@ -301,7 +301,7 @@ describe("simple provider API key configs", () => { }); }); - it("keeps Synthetic credentials limited to env, trusted config, and auth.json", async () => { + it("keeps Synthetic credentials limited to env, trusted config, and opencode.db", async () => { const provider = providers.find((candidate) => candidate.name === "Synthetic")!; const module = await provider.load(); diff --git a/tests/lib.quota-export.test.ts b/tests/lib.quota-export.test.ts index 0ed7d899..eda334b2 100644 --- a/tests/lib.quota-export.test.ts +++ b/tests/lib.quota-export.test.ts @@ -414,7 +414,7 @@ describe("buildQuotaExport", () => { { key: "api_key_configured", value: "true" }, { key: "api_key_source", value: "env" }, { key: "api_key_checked_paths", value: "env:OPENROUTER_API_KEY" }, - { key: "api_key_auth_paths", value: "/tmp/auth.json" }, + { key: "api_key_credential_database_paths", value: "/tmp/opencode.db" }, { key: "secret_canary", value: secretCanary }, ], }, diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index db9b750a..61479842 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -1015,7 +1015,7 @@ describe("buildQuotaStatusReport", () => { api_key_configured: "true", api_key_source: "env", api_key_checked_paths: "env:OPENROUTER_API_KEY", - api_key_auth_paths: "/tmp/auth.json", + api_key_credential_database_paths: "/tmp/opencode.db", }), ], }); @@ -1024,7 +1024,7 @@ describe("buildQuotaStatusReport", () => { expect(section).toContain("- api_key_configured: true"); expect(section).toContain("- api_key_source: env"); expect(section).toContain("- api_key_checked_paths: env:OPENROUTER_API_KEY"); - expect(section).toContain("- api_key_auth_paths: /tmp/auth.json"); + expect(section).toContain("- api_key_credential_database_paths: /tmp/opencode.db"); expect(openrouterMocks.resolveOpenRouterApiKey).not.toHaveBeenCalled(); expect(openrouterMocks.queryOpenRouterQuota).not.toHaveBeenCalled(); expect(openrouterMocks.hasOpenRouterApiKeyConfigured).not.toHaveBeenCalled(); @@ -1037,7 +1037,7 @@ describe("buildQuotaStatusReport", () => { "openrouter", { api_key_configured: "true", - api_key_source: "auth.json", + api_key_source: "opencode.db", }, { errors: [{ label: "OpenRouter", message: "HTTP 401" }], @@ -1048,7 +1048,7 @@ describe("buildQuotaStatusReport", () => { const section = getReportSection(report, "openrouter:"); expect(section).toContain("- api_key_configured: true"); - expect(section).toContain("- api_key_source: auth.json"); + expect(section).toContain("- api_key_source: opencode.db"); expect(section).toContain("- live_probe: error"); expect(section).toContain("- live_error_1: HTTP 401"); expect(openrouterMocks.resolveOpenRouterApiKey).not.toHaveBeenCalled(); @@ -1075,7 +1075,7 @@ describe("buildQuotaStatusReport", () => { api_key_configured: "true", api_key_source: "env", api_key_checked_paths: "env:OPENROUTER_API_KEY", - api_key_auth_paths: "/tmp/auth.json", + api_key_credential_database_paths: "/tmp/opencode.db", }, { errors: [{ label: "OpenRouter", message: "HTTP 401" }], From 96ee44a0729d3ddd52e5ed40fb4b13dfc589858f Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:33:19 +0200 Subject: [PATCH 27/97] build!: one SQLite opener, no better-sqlite3, Node 22.13+ Credential and session reads now share one read-only opener in src/lib/opencode-sqlite.ts: bun:sqlite inside OpenCode 2 (Bun) and the built-in node:sqlite for the Node CLI. The optional better-sqlite3 branch, its dependency, type stub, and pnpm build approval are removed. BREAKING CHANGE: engines.node is now >=22.13.0, the first Node 22 release where node:sqlite works without a flag. --- CONTRIBUTING.md | 4 +- package.json | 5 +- pnpm-lock.yaml | 285 +----------------------------- pnpm-workspace.yaml | 1 - scripts/smoke-packed-package.mjs | 2 +- src/better-sqlite3.d.ts | 15 -- src/lib/opencode-auth.ts | 45 +---- src/lib/opencode-sqlite.ts | 96 +++------- tests/lib.opencode-sqlite.test.ts | 31 +--- tests/package-manifest.test.ts | 16 +- tests/v4-migration-docs.test.ts | 2 +- 11 files changed, 46 insertions(+), 456 deletions(-) delete mode 100644 src/better-sqlite3.d.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21838c6a..f04750e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ Thanks for contributing. This repo has strict local-only behavior and regression ## Development Setup -- The published package runtime supports Node.js `>=22.0.0` (matches `package.json` engines). +- The published package runtime supports Node.js `>=22.13.0` (matches `package.json` engines). - Repository development uses pnpm v11, which requires Node.js `>=22` for the pnpm CLI. - Enable the pinned package manager and install dependencies with: @@ -59,7 +59,7 @@ PR and `main` pushes trigger `.github/workflows/ci.yml` (`CI` workflow): - Job: `pnpm-quality` on Node `24.x` - Steps: frozen install, `pnpm verify`, then one exact npm artifact pack and upload - Job: `runtime-smoke` on Node `22.x` and `24.x` -- Runtime smoke installs that exact packed artifact as a consumer and verifies the default/server imports, TUI export payload, CLI help, and `engines.node >=22.0.0` +- Runtime smoke installs that exact packed artifact as a consumer and verifies the default/server imports, TUI export payload, CLI help, and `engines.node >=22.13.0` Release workflow `.github/workflows/publish-npm.yml` first checks the release tag, SHA, and package version, then runs `pnpm verify` on Node 24. After that, it packs one exact artifact, smoke-tests that artifact on Node 22 and 24, verifies it again before provenance publishing, and backfills the release version. Run `pnpm run release:check` on Node 24 when the release environment is available; it adds the release-version assertion after the canonical gate. diff --git a/package.json b/package.json index b1a1355f..6938d647 100644 --- a/package.json +++ b/package.json @@ -100,13 +100,10 @@ "solid-js": "^1.9.12", "xdg-basedir": "^5.1.0" }, - "optionalDependencies": { - "better-sqlite3": "^12.10.0" - }, "peerDependencies": { "@opencode/plugin": "2.0.15" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.13.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e7a4c1c..fd6175c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,10 +66,6 @@ importers: yaml: specifier: ^2.8.3 version: 2.8.3 - optionalDependencies: - better-sqlite3: - specifier: ^12.10.0 - version: 12.10.0 packages: @@ -1399,10 +1395,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - better-sqlite3@12.10.0: - resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} - engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} - bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -1410,12 +1402,6 @@ packages: resolution: {integrity: sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w==} engines: {node: ^20.17.0 || >=22.9.0} - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -1434,9 +1420,6 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bun-ffi-structs@0.3.1: resolution: {integrity: sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ==} peerDependencies: @@ -1453,9 +1436,6 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -1519,14 +1499,6 @@ packages: supports-color: optional: true - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1556,9 +1528,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -1595,10 +1564,6 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -1635,9 +1600,6 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - find-babel-config@2.1.2: resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==} @@ -1653,9 +1615,6 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-minipass@3.0.3: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -1691,9 +1650,6 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -1753,19 +1709,10 @@ packages: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore-walk@8.0.0: resolution: {integrity: sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==} engines: {node: ^20.17.0 || >=22.9.0} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -1933,10 +1880,6 @@ packages: engines: {node: '>=16'} hasBin: true - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1949,9 +1892,6 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass-collect@2.0.1: resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1988,9 +1928,6 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2006,17 +1943,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - negotiator@1.1.0: resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} engines: {node: '>=18'} - node-abi@3.92.0: - resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} - engines: {node: '>=10'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2075,9 +2005,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -2150,12 +2077,6 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. - hasBin: true - proc-log@6.1.0: resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -2174,24 +2095,13 @@ packages: resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} engines: {node: '>=12.0.0'} - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - pure-rand@8.4.0: resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - read-cmd-shim@6.0.0: resolution: {integrity: sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==} engines: {node: ^20.17.0 || >=22.9.0} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - redis@6.2.1: resolution: {integrity: sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg==} engines: {node: '>= 20.0.0'} @@ -2260,12 +2170,6 @@ packages: resolution: {integrity: sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==} engines: {node: ^20.17.0 || >=22.9.0} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -2319,9 +2223,6 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2330,21 +2231,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tar-fs@2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - tar@7.5.22: resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} engines: {node: '>=18'} @@ -2375,9 +2265,6 @@ packages: resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==} engines: {node: ^20.17.0 || >=22.9.0} - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -2520,9 +2407,6 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@7.0.1: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3020,7 +2904,7 @@ snapshots: fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 - '@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.214.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112)': + '@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.214.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112)': dependencies: '@opentelemetry/semantic-conventions': 1.43.0 effect: 4.0.0-rc.112 @@ -3367,7 +3251,7 @@ snapshots: '@opencode/util@2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(redis@6.2.1(@opentelemetry/api@1.9.1))': dependencies: - '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.214.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.214.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) '@effect/platform-node': 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112) '@npmcli/arborist': 9.4.0 @@ -3465,7 +3349,7 @@ snapshots: '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 optional: true @@ -3971,12 +3855,6 @@ snapshots: baseline-browser-mapping@2.10.44: {} - better-sqlite3@12.10.0: - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - optional: true - bignumber.js@9.3.1: {} bin-links@6.0.2: @@ -3987,18 +3865,6 @@ snapshots: read-cmd-shim: 6.0.0 write-file-atomic: 7.0.1 - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - optional: true - - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - optional: true - bowser@2.14.1: {} brace-expansion@2.1.4: @@ -4019,12 +3885,6 @@ snapshots: buffer-equal-constant-time@1.0.1: {} - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - optional: true - bun-ffi-structs@0.3.1(typescript@7.0.2): dependencies: typescript: 7.0.2 @@ -4046,9 +3906,6 @@ snapshots: chai@6.2.2: {} - chownr@1.1.4: - optional: true - chownr@3.0.0: {} ci-info@4.4.0: {} @@ -4090,14 +3947,6 @@ snapshots: dependencies: ms: 2.1.3 - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - optional: true - - deep-extend@0.6.0: - optional: true - detect-libc@2.1.2: optional: true @@ -4122,11 +3971,6 @@ snapshots: emoji-regex@9.2.2: {} - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - optional: true - entities@6.0.1: {} entities@7.0.1: {} @@ -4174,9 +4018,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - expand-template@2.0.3: - optional: true - expect-type@1.3.0: {} exponential-backoff@3.1.3: {} @@ -4206,9 +4047,6 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - file-uri-to-path@1.0.0: - optional: true - find-babel-config@2.1.2: dependencies: json5: 2.2.3 @@ -4226,9 +4064,6 @@ snapshots: dependencies: fetch-blob: 3.2.0 - fs-constants@1.0.0: - optional: true - fs-minipass@3.0.3: dependencies: minipass: 7.1.3 @@ -4269,9 +4104,6 @@ snapshots: get-east-asian-width@1.4.0: {} - github-from-package@0.0.0: - optional: true - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -4350,19 +4182,10 @@ snapshots: safer-buffer: 2.1.2 optional: true - ieee754@1.2.1: - optional: true - ignore-walk@8.0.0: dependencies: minimatch: 10.2.5 - inherits@2.0.4: - optional: true - - ini@1.3.8: - optional: true - ini@6.0.0: {} ip-address@10.7.2: {} @@ -4507,9 +4330,6 @@ snapshots: mime@4.1.0: {} - mimic-response@3.1.0: - optional: true - minimatch@10.2.5: dependencies: brace-expansion: 5.0.12 @@ -4522,9 +4342,6 @@ snapshots: dependencies: brace-expansion: 2.1.4 - minimist@1.2.8: - optional: true - minipass-collect@2.0.1: dependencies: minipass: 7.1.3 @@ -4561,9 +4378,6 @@ snapshots: dependencies: minipass: 7.1.3 - mkdirp-classic@0.5.3: - optional: true - ms@2.1.3: {} msgpackr-extract@3.0.4: @@ -4584,18 +4398,10 @@ snapshots: nanoid@3.3.11: {} - napi-build-utils@2.0.0: - optional: true - negotiator@1.1.0: dependencies: content-type: 2.1.0 - node-abi@3.92.0: - dependencies: - semver: 7.8.4 - optional: true - node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -4672,11 +4478,6 @@ snapshots: obug@2.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - optional: true - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -4760,22 +4561,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.92.0 - pump: 3.0.4 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.4 - tunnel-agent: 0.6.0 - optional: true - proc-log@6.1.0: {} proggy@4.0.0: {} @@ -4798,31 +4583,10 @@ snapshots: '@types/node': 22.19.6 long: 5.3.2 - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - optional: true - pure-rand@8.4.0: {} - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - optional: true - read-cmd-shim@6.0.0: {} - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - optional: true - redis@6.2.1(@opentelemetry/api@1.9.1): dependencies: '@redis/bloom': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) @@ -4916,16 +4680,6 @@ snapshots: transitivePeerDependencies: - supports-color - simple-concat@1.0.1: - optional: true - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - optional: true - sisteransi@1.0.5: {} smart-buffer@4.2.0: {} @@ -4986,11 +4740,6 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - optional: true - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4999,28 +4748,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-json-comments@2.0.1: - optional: true - supports-preserve-symlinks-flag@1.0.0: {} - tar-fs@2.1.4: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.4 - tar-stream: 2.2.0 - optional: true - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - optional: true - tar@7.5.22: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -5052,11 +4781,6 @@ snapshots: transitivePeerDependencies: - supports-color - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - optional: true - typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -5178,9 +4902,6 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 - wrappy@1.0.2: - optional: true - write-file-atomic@7.0.1: dependencies: signal-exit: 4.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f6333eef..8c8e1f73 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,4 @@ allowBuilds: - better-sqlite3: true esbuild: true lefthook: true msgpackr-extract: true diff --git a/scripts/smoke-packed-package.mjs b/scripts/smoke-packed-package.mjs index ae2a913d..db81c92c 100644 --- a/scripts/smoke-packed-package.mjs +++ b/scripts/smoke-packed-package.mjs @@ -80,7 +80,7 @@ try { const pkg = JSON.parse( await readFile("node_modules/@slkiser/opencode-quota/package.json", "utf8"), ); - assert.equal(pkg.engines?.node, ">=22.0.0"); + assert.equal(pkg.engines?.node, ">=22.13.0"); assert.equal(pkg.dependencies?.["@opentelemetry/api"], "^1.9.1"); for (const dependencyType of ["devDependencies", "optionalDependencies", "peerDependencies"]) { assert.equal(pkg[dependencyType]?.["@opentelemetry/api"], undefined); diff --git a/src/better-sqlite3.d.ts b/src/better-sqlite3.d.ts deleted file mode 100644 index 10feb180..00000000 --- a/src/better-sqlite3.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare module "better-sqlite3" { - class Statement { - all(...params: unknown[]): unknown[]; - get(...params: unknown[]): unknown; - run(...params: unknown[]): unknown; - } - - class Database { - constructor(filename: string, options?: { readonly?: boolean }); - prepare(sql: string): Statement; - close(): void; - } - - export default Database; -} diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 56f6b623..c4692c20 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -6,28 +6,17 @@ */ import { existsSync } from "fs"; -import { createRequire } from "module"; import { isAbsolute, join, resolve } from "path"; import { getOpencodeRuntimeDirCandidates, getOpencodeRuntimeDirs, } from "./opencode-runtime-paths.js"; +import { openOpenCodeSqliteReadOnly, type SqliteConn } from "./opencode-sqlite.js"; import type { AuthData } from "./types.js"; const DEFAULT_AUTH_CACHE_MAX_AGE_MS = 5_000; -const runtimeRequire = createRequire(import.meta.url); - -type CredentialDatabase = { - close(): void; - prepare(sql: string): { all(...params: string[]): unknown[] }; -}; - -type CredentialDatabaseConstructor = new ( - path: string, - options?: Record, -) => CredentialDatabase; type AuthCacheEntry = { timestamp: number; @@ -147,9 +136,9 @@ export function selectConnectionCredentialRows( return selected; } -function readCredentialRowsFromDatabases(paths: string[]): CredentialRow[] { +async function readCredentialRowsFromDatabases(paths: string[]): Promise { for (const path of paths) { - const rows = readCredentialDatabase(path); + const rows = await readCredentialDatabase(path); if (rows.length > 0) return rows; } @@ -169,17 +158,15 @@ function getCredentialDatabasePathsForDataDirs(dataDirs: string[]): string[] { return dataDirs.map((dataDir) => join(dataDir, "opencode.db")); } -function readCredentialDatabase(path: string): CredentialRow[] { +async function readCredentialDatabase(path: string): Promise { if (!existsSync(path)) return []; - let database: CredentialDatabase | undefined; + let database: SqliteConn | undefined; try { - database = openCredentialDatabase(path); - const rows = database - .prepare( - "SELECT id, integration_id, label, active, value FROM credential WHERE integration_id IS NOT NULL ORDER BY active DESC, time_updated DESC, id DESC", - ) - .all() as Array>; + database = await openOpenCodeSqliteReadOnly(path); + const rows = database.all>( + "SELECT id, integration_id, label, active, value FROM credential WHERE integration_id IS NOT NULL ORDER BY active DESC, time_updated DESC, id DESC", + ); const credentials: CredentialRow[] = []; for (const row of rows) { @@ -210,20 +197,6 @@ function readCredentialDatabase(path: string): CredentialRow[] { } } -function openCredentialDatabase(path: string): CredentialDatabase { - if ("Bun" in globalThis) { - const { Database } = runtimeRequire("bun:sqlite") as { - Database: CredentialDatabaseConstructor; - }; - return new Database(path, { readonly: true }); - } - - const { DatabaseSync } = runtimeRequire("node:sqlite") as { - DatabaseSync: CredentialDatabaseConstructor; - }; - return new DatabaseSync(path, { readOnly: true, timeout: 5_000 }); -} - function parseCredentialValue(value: string): Record | null { try { const parsed = JSON.parse(value); diff --git a/src/lib/opencode-sqlite.ts b/src/lib/opencode-sqlite.ts index 02fdc7d7..2668816b 100644 --- a/src/lib/opencode-sqlite.ts +++ b/src/lib/opencode-sqlite.ts @@ -19,13 +19,10 @@ interface BunSqliteModule { Database: new (path: string, options: { readonly: boolean }) => BunSqliteDatabase; } -interface PreparedSqliteDatabase { +interface NodeSqliteDatabase { prepare(sql: string): SqliteStatement; - close(): void; -} - -interface NodeSqliteDatabase extends PreparedSqliteDatabase { exec(sql: string): unknown; + close(): void; } interface NodeSqliteModule { @@ -39,10 +36,6 @@ interface NodeSqliteModule { ) => NodeSqliteDatabase; } -interface BetterSqlite3Module { - default: new (path: string, options?: { readonly?: boolean }) => PreparedSqliteDatabase; -} - function toParams(params?: unknown[]): unknown[] { return Array.isArray(params) ? params : []; } @@ -55,14 +48,6 @@ function runBunPragma(db: BunSqliteDatabase, sql: string): void { } } -function runPreparedPragma(db: PreparedSqliteDatabase, sql: string): void { - try { - db.prepare(sql).run(); - } catch { - // ignore - } -} - function runNodePragma(db: NodeSqliteDatabase, sql: string): void { try { db.exec(sql); @@ -71,7 +56,20 @@ function runNodePragma(db: NodeSqliteDatabase, sql: string): void { } } -function createPreparedSqliteConn(db: PreparedSqliteDatabase): SqliteConn { +async function openWithNodeSqlite(dbPath: string): Promise { + const mod = (await import("node:sqlite")) as unknown as NodeSqliteModule; + const db = new mod.DatabaseSync(dbPath, { + readOnly: true, + enableForeignKeyConstraints: true, + open: true, + }); + + // Keep reads deterministic and avoid accidental writes. + runNodePragma(db, "PRAGMA query_only = ON;"); + + // Avoid transient SQLITE_BUSY errors (WAL). + runNodePragma(db, "PRAGMA busy_timeout = 5000;"); + return { all(sql: string, params?: unknown[]): T[] { const stmt = db.prepare(sql); @@ -126,64 +124,14 @@ async function openWithBunSqlite(dbPath: string): Promise { }; } -async function importNodeSqlite(): Promise { - try { - return (await import("node:sqlite")) as unknown as NodeSqliteModule; - } catch { - return null; - } -} - -async function openWithNodeSqlite(dbPath: string, mod: NodeSqliteModule): Promise { - const db = new mod.DatabaseSync(dbPath, { - readOnly: true, - enableForeignKeyConstraints: true, - open: true, - }); - - // Keep reads deterministic and avoid accidental writes. - runNodePragma(db, "PRAGMA query_only = ON;"); - - // Avoid transient SQLITE_BUSY errors (WAL). - runNodePragma(db, "PRAGMA busy_timeout = 5000;"); - - return createPreparedSqliteConn(db); -} - -async function openWithBetterSqlite3(dbPath: string): Promise { - const mod = (await import("better-sqlite3")) as unknown as BetterSqlite3Module; - const db = new mod.default(dbPath, { readonly: true }); - - // Keep reads deterministic and avoid accidental writes. - runPreparedPragma(db, "PRAGMA query_only = ON;"); - - // Avoid transient SQLITE_BUSY errors (WAL). - runPreparedPragma(db, "PRAGMA busy_timeout = 5000;"); - - return createPreparedSqliteConn(db); -} - -async function openWithNodeRuntimeSqlite(dbPath: string): Promise { - const nodeSqlite = await importNodeSqlite(); - - if (nodeSqlite) { - return openWithNodeSqlite(dbPath, nodeSqlite); - } - - try { - return await openWithBetterSqlite3(dbPath); - } catch (cause) { - throw new Error( - "OpenCode SQLite backend unavailable in this Node runtime; node:sqlite or optional better-sqlite3 is required for local history reads.", - { cause }, - ); - } -} - +/** + * Open OpenCode's SQLite database read-only. + * Inside OpenCode 2 (Bun) this uses bun:sqlite; the Node CLI uses node:sqlite. + */ export async function openOpenCodeSqliteReadOnly(dbPath: string): Promise { - if (typeof globalThis === "object" && "Bun" in globalThis) { + if ("Bun" in globalThis) { return openWithBunSqlite(dbPath); } - return openWithNodeRuntimeSqlite(dbPath); + return openWithNodeSqlite(dbPath); } diff --git a/tests/lib.opencode-sqlite.test.ts b/tests/lib.opencode-sqlite.test.ts index 9abc38f1..bb62bcba 100644 --- a/tests/lib.opencode-sqlite.test.ts +++ b/tests/lib.opencode-sqlite.test.ts @@ -1,6 +1,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; import { describe, expect, it, vi } from "vitest"; import { openOpenCodeSqliteReadOnly } from "../src/lib/opencode-sqlite.js"; @@ -11,30 +12,13 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ getOpencodeRuntimeDirCandidates: () => ({ dataDirs: runtimePaths.dataDirs }), })); -async function importNodeSqlite(): Promise { - try { - return await import("node:sqlite"); - } catch { - return null; - } -} - describe("opencode sqlite adapter", () => { it("reads an OpenCode SQLite database through node:sqlite on Node runtimes", async () => { - const sqlite = await importNodeSqlite(); - - if (!sqlite) { - console.warn( - "Skipping node:sqlite adapter coverage because this Node runtime does not provide node:sqlite.", - ); - return; - } - const dir = await mkdtemp(join(tmpdir(), "opencode-sqlite-")); const dbPath = join(dir, "opencode.db"); try { - const writer = new sqlite.DatabaseSync(dbPath); + const writer = new DatabaseSync(dbPath); writer.exec(` CREATE TABLE usage ( id INTEGER PRIMARY KEY, @@ -72,22 +56,13 @@ describe("opencode sqlite adapter", () => { }); it("reads authoritative completed assistant rows by completion time", async () => { - const sqlite = await importNodeSqlite(); - - if (!sqlite) { - console.warn( - "Skipping completed accounting integration coverage because this Node runtime does not provide node:sqlite.", - ); - return; - } - const dir = await mkdtemp(join(tmpdir(), "opencode-accounting-")); const dbPath = join(dir, "opencode.db"); const cutoff = Date.parse("2026-07-16T00:00:00.000Z"); try { runtimePaths.dataDirs = [dir]; - const writer = new sqlite.DatabaseSync(dbPath); + const writer = new DatabaseSync(dbPath); writer.exec(` CREATE TABLE "session_v2" ( id TEXT PRIMARY KEY diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index 5c50799e..06131012 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -147,7 +147,7 @@ describe("package manifest compatibility", () => { expect(packageManagerMatch).not.toBeNull(); expect(Number(packageManagerMatch?.[1])).toBeGreaterThanOrEqual(11); - expect(pkg.engines?.node).toBe(">=22.0.0"); + expect(pkg.engines?.node).toBe(">=22.13.0"); expect(pkg.devDependencies?.typescript).toBe("7.0.2"); expect(pkg.devDependencies?.yaml).toBe("^2.8.3"); }); @@ -239,22 +239,15 @@ describe("package manifest compatibility", () => { }); it("ships the OpenTelemetry API while keeping the metrics SDK host-owned", () => { + expect(pkg).not.toHaveProperty("optionalDependencies"); expect(pkg.dependencies?.["@opentelemetry/api"]).toBe("^1.9.1"); - for (const dependencyType of [ - pkg.devDependencies, - pkg.optionalDependencies, - pkg.peerDependencies, - ]) { + for (const dependencyType of [pkg.devDependencies, pkg.peerDependencies]) { expect(dependencyType).not.toHaveProperty("@opentelemetry/api"); } expect(pkg.peerDependenciesMeta?.["@opentelemetry/api"]).toBeUndefined(); expect(pkg.devDependencies?.["@opentelemetry/sdk-metrics"]).toBe("2.10.0"); - for (const dependencyType of [ - pkg.dependencies, - pkg.optionalDependencies, - pkg.peerDependencies, - ]) { + for (const dependencyType of [pkg.dependencies, pkg.peerDependencies]) { expect(dependencyType).not.toHaveProperty("@opentelemetry/sdk-metrics"); } }); @@ -265,7 +258,6 @@ describe("package manifest compatibility", () => { expect(pnpmWorkspace).toContain("minimumReleaseAgeIgnoreMissingTime: false"); expect(pnpmWorkspace).toContain("blockExoticSubdeps: true"); expect(pnpmWorkspaceConfig.allowBuilds).toEqual({ - "better-sqlite3": true, esbuild: true, lefthook: true, "msgpackr-extract": true, diff --git a/tests/v4-migration-docs.test.ts b/tests/v4-migration-docs.test.ts index 8e35347f..86c3bd8d 100644 --- a/tests/v4-migration-docs.test.ts +++ b/tests/v4-migration-docs.test.ts @@ -23,7 +23,7 @@ const configuration = await readFile( describe("v4 migration documentation contract", () => { it("keeps requirements and navigation aligned", () => { - expect(packageJson.engines?.node).toBe(">=22.0.0"); + expect(packageJson.engines?.node).toBe(">=22.13.0"); expect(packageJson.peerDependencies?.["@opencode/plugin"]).toBe("2.0.15"); expect(packageJson.engines).not.toHaveProperty("opencode"); From dd9e5dd0dee174f1977d5b4d1f7065e9ca5d0623 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:35:36 +0200 Subject: [PATCH 28/97] refactor!: remove modules left unused by the OpenCode 2 migration Delete cache, command-handled, command-surfaces, quota-reset-notifications, and quota-toast-runtime: an import scan from every package entrypoint (src/index.ts, src/plugin.ts, src/tui-v2.tsx, tui.ts, src/bin/*) reaches none of them. Remove their tests and the stale quota-reset-notifications mocks. Trim tui-panel-state to the PromptBarEntry type, the only export still used. --- CONTRIBUTING.md | 3 +- src/lib/cache.ts | 160 --- src/lib/command-handled.ts | 41 - src/lib/command-surfaces.ts | 11 - src/lib/quota-reset-notifications.ts | 412 ------ src/lib/quota-toast-runtime.ts | 1018 -------------- src/lib/tui-panel-state.ts | 79 -- tests/cache.test.ts | 99 -- tests/command-handled.test.ts | 35 - tests/helpers/plugin-test-harness.ts | 2 +- tests/lib.command-surfaces.test.ts | 29 - tests/lib.quota-reset-notifications.test.ts | 453 ------- tests/lib.quota-toast-runtime.test.ts | 1168 ----------------- tests/plugin.maintainer-announcements.test.ts | 11 - tests/plugin.quota-command.test.ts | 9 - tests/tui-panel-state.test.ts | 121 -- 16 files changed, 2 insertions(+), 3649 deletions(-) delete mode 100644 src/lib/cache.ts delete mode 100644 src/lib/command-handled.ts delete mode 100644 src/lib/command-surfaces.ts delete mode 100644 src/lib/quota-reset-notifications.ts delete mode 100644 src/lib/quota-toast-runtime.ts delete mode 100644 tests/cache.test.ts delete mode 100644 tests/command-handled.test.ts delete mode 100644 tests/lib.command-surfaces.test.ts delete mode 100644 tests/lib.quota-reset-notifications.test.ts delete mode 100644 tests/lib.quota-toast-runtime.test.ts delete mode 100644 tests/tui-panel-state.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f04750e4..cf286fe8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,9 +82,8 @@ Recommended settings for `main`: - The TUI plugin owns only Sidebar, Compact status, home-bottom, prompt-wrapper, refresh, and resource-lifecycle surfaces. It must not register keymap commands or render native slash-command dialogs. - Slash commands (`/quota`, `/quota_status`, `/quota_announcements`, `/pricing_refresh`, `/tokens_*`) must route through `buildQuotaDialogCommandOutput()`; do not duplicate command-output logic in `src/plugin.ts`. - The handled-sentinel path can surface popup/log noise until upstream adds a clean cancellation API; keep docs aligned with anomalyco/opencode#18554 and anomalyco/opencode#18559. -- Keep `handled()` / `isCommandHandledError(...)` tests aligned with the server/web/desktop handled-sentinel boundary. - `injectRawOutput()` is shared by inline slash commands and the server `tool.quota_status` compatibility path. -- Keep `tests/plugin.command-handled-boundary.test.ts`, `tests/tui-smoke.test.ts`, and `tests/command-handled.test.ts` aligned with these invariants. +- Keep `tests/plugin.command-handled-boundary.test.ts` aligned with these invariants. Additional boundary tests to keep healthy when touching plugin/provider logic: diff --git a/src/lib/cache.ts b/src/lib/cache.ts deleted file mode 100644 index 7d6fce9b..00000000 --- a/src/lib/cache.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Keyed cache and throttling system for rendered quota toasts. - * - * This is presentation-only throttling: - * - Each cache key stores the last rendered toast string for that surface/session context - * - Only fetch if minIntervalMs has passed since the last fetch for that key - * - Deduplicate concurrent fetches per key - */ - -import type { CachedToast } from "./types.js"; - -type CacheEntry = { - cachedToast: CachedToast | null; - inFlightPromise: Promise | null; - lastFetchTime: number; -}; - -const cacheEntries = new Map(); - -function getCacheEntry(cacheKey: string): CacheEntry { - const existing = cacheEntries.get(cacheKey); - if (existing) { - return existing; - } - - const created: CacheEntry = { - cachedToast: null, - inFlightPromise: null, - lastFetchTime: 0, - }; - cacheEntries.set(cacheKey, created); - return created; -} - -/** - * Get the cached toast message for a key if still valid. - */ -export function getCachedToast(cacheKey: string, minIntervalMs: number): string | null { - const cacheEntry = cacheEntries.get(cacheKey); - if (!cacheEntry?.cachedToast) { - return null; - } - - const now = Date.now(); - const age = now - cacheEntry.cachedToast.timestamp; - - if (age < minIntervalMs) { - return cacheEntry.cachedToast.message; - } - - return null; -} - -/** - * Check if a new fetch should be initiated for a key. - */ -export function shouldFetch(cacheKey: string, minIntervalMs: number): boolean { - const cacheEntry = getCacheEntry(cacheKey); - const now = Date.now(); - return now - cacheEntry.lastFetchTime >= minIntervalMs; -} - -/** - * Get or start a fetch operation with keyed deduplication. - */ -export async function getOrFetch( - cacheKey: string, - fetchFn: () => Promise, - minIntervalMs: number, -): Promise { - const wrapped = async () => { - const message = await fetchFn(); - return { message, cache: true }; - }; - return getOrFetchWithCacheControl(cacheKey, wrapped, minIntervalMs); -} - -/** - * Get or start a fetch operation with keyed deduplication and cache control. - * - * This is useful when some results should be displayed but not cached - * (e.g. transient "all providers failed" cases). - */ -export async function getOrFetchWithCacheControl( - cacheKey: string, - fetchFn: () => Promise<{ message: string | null; cache?: boolean }>, - minIntervalMs: number, -): Promise { - const cacheEntry = getCacheEntry(cacheKey); - - const cached = getCachedToast(cacheKey, minIntervalMs); - if (cached !== null) { - return cached; - } - - if (cacheEntry.inFlightPromise) { - return cacheEntry.inFlightPromise; - } - - if (!shouldFetch(cacheKey, minIntervalMs)) { - return cacheEntry.cachedToast?.message ?? null; - } - - cacheEntry.lastFetchTime = Date.now(); - cacheEntry.inFlightPromise = (async () => { - try { - const out = await fetchFn(); - const result = out.message; - const cache = out.cache ?? true; - - if (result === null) { - cacheEntry.lastFetchTime = 0; - return null; - } - - if (!cache) { - cacheEntry.lastFetchTime = 0; - return result; - } - - cacheEntry.cachedToast = { - message: result, - timestamp: Date.now(), - }; - - return result; - } finally { - cacheEntry.inFlightPromise = null; - if (!cacheEntry.cachedToast && cacheEntry.lastFetchTime === 0) { - cacheEntries.delete(cacheKey); - } - } - })(); - - return cacheEntry.inFlightPromise; -} - -/** - * Clear one keyed entry, or the whole keyed cache if no key is provided. - */ -export function clearCache(cacheKey?: string): void { - if (cacheKey) { - cacheEntries.delete(cacheKey); - return; - } - - cacheEntries.clear(); -} - -/** - * Force update a keyed cache entry with a new message. - */ -export function updateCache(cacheKey: string, message: string): void { - const cacheEntry = getCacheEntry(cacheKey); - cacheEntry.cachedToast = { - message, - timestamp: Date.now(), - }; - cacheEntry.lastFetchTime = Date.now(); -} diff --git a/src/lib/command-handled.ts b/src/lib/command-handled.ts deleted file mode 100644 index 458094b9..00000000 --- a/src/lib/command-handled.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Command-handled sentinel. - * - * Thrown by slash-command handlers to signal that the command output - * has already been injected and no further processing is needed. - */ - -export const COMMAND_HANDLED_SENTINEL = "__QUOTA_COMMAND_HANDLED__" as const; - -const COMMAND_HANDLED_ERROR_BRAND = Symbol.for("@slkiser/opencode-quota/command-handled"); - -function createCommandHandledError(): Error { - const err = Object.create(Error.prototype) as Error; - Object.defineProperties(err, { - [COMMAND_HANDLED_ERROR_BRAND]: { value: true }, - message: { value: "", configurable: true, writable: true }, - name: { value: "", configurable: true, writable: true }, - stack: { value: "", configurable: true, writable: true }, - }); - return err; -} - -/** - * Throw a quiet command-handled abort error. - * Use this instead of `throw new Error("__QUOTA_COMMAND_HANDLED__")`. - */ -export function handled(): never { - throw createCommandHandledError(); -} - -/** - * Returns true when an error is a command-handled abort. - */ -export function isCommandHandledError(err: unknown): boolean { - if (!(err instanceof Error)) { - return false; - } - - const marker = (err as unknown as Record)[COMMAND_HANDLED_ERROR_BRAND]; - return marker === true || err.message === COMMAND_HANDLED_SENTINEL; -} diff --git a/src/lib/command-surfaces.ts b/src/lib/command-surfaces.ts deleted file mode 100644 index 8aa06b15..00000000 --- a/src/lib/command-surfaces.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * The embedded TUI worker already exposes local dialog commands. - * Registering the server copies there creates duplicate slash rows. - * Main-thread hosts, including mini, rely on the server catalog. - */ -export function shouldRegisterServerSlashCommands(params: { - isMainThread: boolean; - argv: readonly string[]; -}): boolean { - return params.isMainThread; -} diff --git a/src/lib/quota-reset-notifications.ts b/src/lib/quota-reset-notifications.ts deleted file mode 100644 index c6eacf40..00000000 --- a/src/lib/quota-reset-notifications.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { createHash, randomUUID } from "node:crypto"; -import { writeFileSync } from "node:fs"; -import { mkdir, open, readFile, rm, rmdir, stat, unlink } from "node:fs/promises"; -import { dirname, join } from "node:path"; - -import { writeJsonAtomic } from "./atomic-json.js"; -import { isPercentEntry, type QuotaProviderResult, type QuotaToastEntry } from "./entries.js"; -import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; -import { getQuotaProviderDisplayLabel } from "./provider-metadata.js"; -import { classifyQuotaWindowText, type QuotaWindowKind } from "./quota-entry-display.js"; -import type { QuotaResetWindow } from "./types.js"; - -const STATE_VERSION = 2; -const MAX_TRANSITION_AGE_MS = 24 * 60 * 60 * 1000; -const MAX_OBSERVATION_AGE_MS = 30 * 24 * 60 * 60 * 1000; -const LOCK_RETRY_DELAY_MS = 20; -const LOCK_TIMEOUT_MS = 2_000; -const STALE_LOCK_AGE_MS = 30_000; -const LOCK_OWNER_FILE = "owner.json"; -const LOCK_RECLAIM_FILE = "reclaim"; - -const WINDOW_KINDS: Readonly> = { - fiveHour: "five_hour", - hourly: "hour", - daily: "day", - weekly: "week", - monthly: "month", - yearly: "year", -}; - -type ResetObservation = { - resetAtMs: number; - percentRemaining: number; - observedAtMs: number; - notifiedResetAtMs?: number; -}; - -type ResetNotificationState = { - version: typeof STATE_VERSION; - observations: Record; -}; - -type ResetCandidate = { - current: ResetObservation; - notice: QuotaResetNotice; -}; - -type StateLockOwner = { - token: string; - pid: number; - createdAtMs: number; -}; - -export type QuotaResetProviderResult = { - providerId: string; - result: QuotaProviderResult; -}; - -export type QuotaResetNotice = { - providerId: string; - label: string; - window: QuotaResetWindow; - percentRemaining: number; -}; - -function getStatePath(): string { - return join( - getOpencodeRuntimeDirs().stateDir, - "opencode-quota", - "quota-reset-notifications.json", - ); -} - -function getWindow(entry: QuotaToastEntry): QuotaWindowKind | null { - if (entry.semantic) { - return entry.semantic.metric.kind === "window" ? entry.semantic.metric.window : null; - } - return classifyQuotaWindowText(entry.label ?? "") ?? classifyQuotaWindowText(entry.name); -} - -function getConfiguredWindow( - kind: QuotaWindowKind | null, - configured: readonly QuotaResetWindow[], -): QuotaResetWindow | null { - if (!kind) return null; - return configured.find((window) => WINDOW_KINDS[window] === kind) ?? null; -} - -function getIdentity(providerId: string, entry: QuotaToastEntry, window: QuotaResetWindow): string { - const raw = [ - providerId, - entry.accounting.sourceId ?? "single-source", - window, - entry.accounting.resultType, - ].join("\u001f"); - return createHash("sha256").update(raw).digest("hex"); -} - -function isObservation(value: unknown): value is ResetObservation { - if (!value || typeof value !== "object") return false; - const item = value as Partial; - return ( - Number.isFinite(item.resetAtMs) && - Number.isFinite(item.percentRemaining) && - Number.isFinite(item.observedAtMs) - ); -} - -async function readState(path: string): Promise { - try { - const parsed = JSON.parse(await readFile(path, "utf8")) as Partial; - if (parsed.version !== STATE_VERSION || !parsed.observations) throw new Error("invalid state"); - return { - version: STATE_VERSION, - observations: Object.fromEntries( - Object.entries(parsed.observations).filter((entry) => isObservation(entry[1])), - ), - }; - } catch { - return { version: STATE_VERSION, observations: {} }; - } -} - -function isFileSystemError(error: unknown, code: string): boolean { - return (error as NodeJS.ErrnoException | undefined)?.code === code; -} - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function parseLockOwner(value: string): StateLockOwner | null { - try { - const owner = JSON.parse(value) as Partial; - if ( - typeof owner.token !== "string" || - !Number.isSafeInteger(owner.pid) || - (owner.pid ?? 0) <= 0 || - !Number.isFinite(owner.createdAtMs) - ) { - return null; - } - return owner as StateLockOwner; - } catch { - return null; - } -} - -async function readLockOwner(lockPath: string): Promise { - try { - return parseLockOwner(await readFile(join(lockPath, LOCK_OWNER_FILE), "utf8")); - } catch (error) { - if (isFileSystemError(error, "ENOENT")) return null; - throw error; - } -} - -function isProcessAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch (error) { - return !isFileSystemError(error, "ESRCH"); - } -} - -async function getReclaimStatus( - lockPath: string, -): Promise<{ reclaimable: boolean; allowMissingOwner: boolean }> { - const owner = await readLockOwner(lockPath); - if (owner) return { reclaimable: !isProcessAlive(owner.pid), allowMissingOwner: false }; - - try { - const lock = await stat(lockPath); - const stale = Date.now() - lock.mtimeMs > STALE_LOCK_AGE_MS; - return { reclaimable: stale, allowMissingOwner: stale }; - } catch (error) { - if (isFileSystemError(error, "ENOENT")) { - return { reclaimable: true, allowMissingOwner: true }; - } - throw error; - } -} - -async function tryReclaimStateLock(lockPath: string, allowMissingOwner: boolean): Promise { - const reclaimPath = join(lockPath, LOCK_RECLAIM_FILE); - try { - const handle = await open(reclaimPath, "wx", 0o600); - await handle.close(); - } catch (error) { - if (isFileSystemError(error, "ENOENT")) return true; - if (!isFileSystemError(error, "EEXIST")) throw error; - - try { - const reclaim = await stat(reclaimPath); - if (Date.now() - reclaim.mtimeMs <= STALE_LOCK_AGE_MS) return false; - } catch (statError) { - if (isFileSystemError(statError, "ENOENT")) return false; - throw statError; - } - - const owner = await readLockOwner(lockPath); - if (owner && isProcessAlive(owner.pid)) { - await unlink(reclaimPath).catch(() => undefined); - return false; - } - if (!owner && !allowMissingOwner) return false; - await rm(lockPath, { recursive: true, force: true }); - return true; - } - - const owner = await readLockOwner(lockPath); - const reclaimable = owner ? !isProcessAlive(owner.pid) : allowMissingOwner; - if (reclaimable) { - await rm(lockPath, { recursive: true, force: true }); - return true; - } - - await unlink(reclaimPath).catch((error: unknown) => { - if (!isFileSystemError(error, "ENOENT")) throw error; - }); - return false; -} - -async function acquireStateLock(statePath: string): Promise<() => Promise> { - await mkdir(dirname(statePath), { recursive: true, mode: 0o700 }); - const lockPath = `${statePath}.lock`; - const token = randomUUID(); - const deadline = Date.now() + LOCK_TIMEOUT_MS; - - while (true) { - try { - await mkdir(lockPath, { mode: 0o700 }); - const owner = { token, pid: process.pid, createdAtMs: Date.now() } satisfies StateLockOwner; - try { - writeFileSync(join(lockPath, LOCK_OWNER_FILE), JSON.stringify(owner), { - flag: "wx", - mode: 0o600, - }); - } catch (error) { - await rmdir(lockPath).catch(() => undefined); - throw error; - } - - return async () => { - const currentOwner = await readLockOwner(lockPath); - if (currentOwner?.token !== token) return; - await rm(lockPath, { recursive: true, force: true }); - }; - } catch (error) { - if (!isFileSystemError(error, "EEXIST")) throw error; - } - - const status = await getReclaimStatus(lockPath); - if (status.reclaimable && (await tryReclaimStateLock(lockPath, status.allowMissingOwner))) { - continue; - } - - if (Date.now() >= deadline) { - throw new Error(`Timed out acquiring quota reset notification state lock: ${lockPath}`); - } - await delay(LOCK_RETRY_DELAY_MS); - } -} - -function didReset(previous: ResetObservation, current: ResetObservation, nowMs: number): boolean { - const transitionAge = nowMs - previous.resetAtMs; - return ( - previous.observedAtMs <= previous.resetAtMs && - transitionAge >= 0 && - transitionAge <= MAX_TRANSITION_AGE_MS && - current.resetAtMs > previous.resetAtMs && - current.resetAtMs > nowMs && - current.percentRemaining > previous.percentRemaining && - previous.percentRemaining < 100 && - previous.notifiedResetAtMs !== previous.resetAtMs - ); -} - -function updateObservation( - previous: ResetObservation | undefined, - current: ResetObservation, - nowMs: number, -): { observation: ResetObservation; notify: boolean } { - if (!previous) return { observation: current, notify: false }; - if (current.resetAtMs < previous.resetAtMs || current.observedAtMs < previous.observedAtMs) { - return { observation: previous, notify: false }; - } - - if (current.resetAtMs === previous.resetAtMs) { - if (current.resetAtMs <= nowMs && previous.observedAtMs <= previous.resetAtMs) { - return { observation: previous, notify: false }; - } - return { - observation: { ...current, notifiedResetAtMs: previous.notifiedResetAtMs }, - notify: false, - }; - } - - if (nowMs < previous.resetAtMs) return { observation: previous, notify: false }; - - const notify = didReset(previous, current, nowMs); - return { - observation: notify ? { ...current, notifiedResetAtMs: previous.resetAtMs } : current, - notify, - }; -} - -function collectCandidates(params: { - providers: QuotaResetProviderResult[]; - windows: readonly QuotaResetWindow[]; - nowMs: number; -}): Map { - const candidates = new Map(); - - for (const provider of params.providers) { - for (const entry of provider.result.entries) { - if (!isPercentEntry(entry)) continue; - if (entry.accounting.resultType !== "quota" && entry.accounting.resultType !== "rate_limit") { - continue; - } - if (!entry.resetTimeIso) continue; - - const window = getConfiguredWindow(getWindow(entry), params.windows); - if (!window) continue; - const resetAtMs = Date.parse(entry.resetTimeIso); - if (!Number.isFinite(resetAtMs)) continue; - - const key = getIdentity(provider.providerId, entry, window); - const candidate: ResetCandidate = { - current: { - resetAtMs, - percentRemaining: entry.percentRemaining, - observedAtMs: params.nowMs, - }, - notice: { - providerId: provider.providerId, - label: entry.group?.trim() || getQuotaProviderDisplayLabel(provider.providerId), - window, - percentRemaining: entry.percentRemaining, - }, - }; - const matching = candidates.get(key); - if (matching) matching.push(candidate); - else candidates.set(key, [candidate]); - } - } - - return candidates; -} - -export async function observeQuotaResetNotifications(params: { - providers: QuotaResetProviderResult[]; - windows: readonly QuotaResetWindow[]; - nowMs?: number; - statePath?: string; -}): Promise { - const nowMs = params.nowMs ?? Date.now(); - const statePath = params.statePath ?? getStatePath(); - const candidates = collectCandidates({ - providers: params.providers, - windows: params.windows, - nowMs, - }); - const releaseLock = await acquireStateLock(statePath); - - try { - const state = await readState(statePath); - const observations = Object.fromEntries( - Object.entries(state.observations).filter( - ([, observation]) => nowMs - observation.observedAtMs <= MAX_OBSERVATION_AGE_MS, - ), - ); - const notices: QuotaResetNotice[] = []; - - for (const [key, matching] of candidates) { - if (matching.length !== 1) { - delete observations[key]; - continue; - } - const candidate = matching[0]; - if (!candidate) continue; - const update = updateObservation(observations[key], candidate.current, nowMs); - observations[key] = update.observation; - if (update.notify) notices.push(candidate.notice); - } - - await writeJsonAtomic( - statePath, - { version: STATE_VERSION, observations } satisfies ResetNotificationState, - { trailingNewline: true, directoryMode: 0o700, fileMode: 0o600 }, - ); - return notices; - } finally { - await releaseLock(); - } -} - -export function formatQuotaResetNotification(notices: readonly QuotaResetNotice[]): string | null { - if (notices.length === 0) return null; - const labels = [...new Set(notices.map((notice) => notice.label))]; - const shown = labels.slice(0, 3); - const overflow = labels.length - shown.length; - const providers = `${shown.join(", ")}${overflow > 0 ? ` and ${overflow} more` : ""}`; - - const [notice] = notices; - if (notice && notices.length === 1) { - const window = notice.window === "weekly" ? "Weekly" : "Quota"; - return `${window} quota reset: ${providers} is available again (${Math.round(notice.percentRemaining)}% remaining).`; - } - return `Quota reset: ${providers} are available again.`; -} diff --git a/src/lib/quota-toast-runtime.ts b/src/lib/quota-toast-runtime.ts deleted file mode 100644 index 27c4bafc..00000000 --- a/src/lib/quota-toast-runtime.ts +++ /dev/null @@ -1,1018 +0,0 @@ -import { getProviders } from "../providers/registry.js"; -import { - DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS, - isAlibabaModelId, - resolveAlibabaCodingPlanAuthCached, -} from "./alibaba-auth.js"; -import { getOrFetchWithCacheControl } from "./cache.js"; -import { createLoadConfigMeta, type LoadConfigMeta } from "./config.js"; -import type { RuntimeContextRootHints } from "./config-file-utils.js"; -import { isCursorModelId, isCursorProviderId } from "./cursor-pricing.js"; -import { sanitizeDisplayText } from "./display-sanitize.js"; -import { formatQuotaRows } from "./format.js"; -import { formatQuotaModeHeading } from "./format-utils.js"; -import { - BUNDLED_MAINTAINER_ANNOUNCEMENTS, - formatMaintainerAnnouncementHomeCountLine, - getMaintainerAnnouncementsSummary, -} from "./maintainer-announcements.js"; -import { - maybeRefreshPricingSnapshot, - setPricingSnapshotAutoRefresh, - setPricingSnapshotSelection, -} from "./modelsdev-pricing.js"; -import { resolveQuotaFormatStyle } from "./quota-format-style.js"; -import { customQuotaProviderDefinitions, QUOTA_PROVIDERS_AGGREGATE_ID } from "./quota-providers.js"; -import { - collectQuotaRenderData, - type QuotaStatusLiveProbe, - type SessionModelMeta, -} from "./quota-render-data.js"; -import { - formatQuotaResetNotification, - observeQuotaResetNotifications, -} from "./quota-reset-notifications.js"; -import { - createQuotaRuntimeRequestContext, - type QuotaRuntimeClient, - type QuotaRuntimeContext, - resolveQuotaRuntimeContext, -} from "./quota-runtime-context.js"; -import type { SessionTokenError } from "./quota-status.js"; -import { inspectTuiConfig } from "./tui-config-diagnostics.js"; -import { DEFAULT_CONFIG, type QuotaToastConfig } from "./types.js"; - -export type DeferredQuotaRefreshReason = - | "config_load_failed" - | "no_available_providers" - | "provider_fetch_failed" - | "no_reportable_data"; - -type DeferredQuotaRefreshState = { - sessionID: string; - attempts: number; - reason: DeferredQuotaRefreshReason; - queuedAtMs: number; - timer: ReturnType | null; - inFlight: boolean; -}; - -export type QuotaToastCollectionResult = { - message: string | null; - cacheRenderedMessage: boolean; - retryable: boolean; - retryReason?: DeferredQuotaRefreshReason; - hasQuotaRows: boolean; - detectedProviderIds: string[]; - freshProviderResults: QuotaStatusLiveProbe[]; - sessionTokenError?: SessionTokenError; - shouldReconcileDetectedProviders: boolean; -}; - -type QuotaToastEmissionPlan = { - config: QuotaToastConfig; - message: string | null; - detectedProviderIds: string[]; - freshProviderResults: QuotaStatusLiveProbe[]; - complete: () => void; - deferredRetryResult?: { - retryable: boolean; - retryReason?: DeferredQuotaRefreshReason; - }; -}; - -type QuotaToastBody = { - message: string; - variant: "info" | "success" | "warning" | "error"; - duration?: number; - title?: string; -}; - -export interface QuotaToastRuntime { - handleTrigger(params: { sessionID: string; trigger: string }): Promise; -} - -export interface QuotaToastRuntimeDependencies { - client: QuotaRuntimeClient; - roots: () => RuntimeContextRootHints; - resolveSessionMeta: (sessionID: string) => Promise; - isSubagentSession: (sessionID: string) => Promise; - reconcileDetectedProviders: (providerIds: readonly string[]) => Promise; - setSessionTokenError: (error: SessionTokenError | undefined) => void; - showToast: (body: QuotaToastBody) => Promise; - log: (message: string, extra?: Record) => Promise; - onInitialized: (extra: Record) => void; -} - -const DEFERRED_QUOTA_REFRESH_DELAYS_MS = [3_000, 15_000, 60_000, 300_000] as const; - -export function createQuotaToastRuntime( - dependencies: QuotaToastRuntimeDependencies, -): QuotaToastRuntime { - let config: QuotaToastConfig = DEFAULT_CONFIG; - let configLoaded = false; - let configInFlight: Promise | null = null; - let configMeta: LoadConfigMeta = createLoadConfigMeta(); - let runtimeProviders = getProviders(); - let initDone = false; - - const deferredQuotaRefreshes = new Map(); - const detectedProviderIdsByToastCacheKey = new Map(); - const maintainerAnnouncementToastFallback = { - pending: true, - inFlight: false, - }; - - function getDeferredQuotaRefreshDelayMs(attempts: number): number { - const index = Math.min(Math.max(0, attempts), DEFERRED_QUOTA_REFRESH_DELAYS_MS.length - 1); - return DEFERRED_QUOTA_REFRESH_DELAYS_MS[index]!; - } - - function clearDeferredQuotaRefresh(sessionID: string): void { - const state = deferredQuotaRefreshes.get(sessionID); - if (state?.timer) { - clearTimeout(state.timer); - } - deferredQuotaRefreshes.delete(sessionID); - } - - function clearDeferredQuotaRefreshTimer(state: DeferredQuotaRefreshState): void { - if (!state.timer) return; - clearTimeout(state.timer); - state.timer = null; - } - - function scheduleDeferredQuotaRefresh(params: { - sessionID: string; - reason: DeferredQuotaRefreshReason; - incrementAttempts: boolean; - }): void { - let state = deferredQuotaRefreshes.get(params.sessionID); - if (!state) { - state = { - sessionID: params.sessionID, - attempts: 0, - reason: params.reason, - queuedAtMs: Date.now(), - timer: null, - inFlight: false, - }; - deferredQuotaRefreshes.set(params.sessionID, state); - } else { - if (params.incrementAttempts) { - state.attempts += 1; - } - state.reason = params.reason; - clearDeferredQuotaRefreshTimer(state); - } - - const delayMs = getDeferredQuotaRefreshDelayMs(state.attempts); - state.timer = setTimeout(() => { - void runDeferredQuotaRefresh(params.sessionID); - }, delayMs); - state.timer.unref?.(); - - void dependencies.log("Deferred quota refresh scheduled", { - sessionID: params.sessionID, - reason: params.reason, - attempts: state.attempts, - delayMs, - }); - } - - async function runDeferredQuotaRefresh(sessionID: string): Promise { - const state = deferredQuotaRefreshes.get(sessionID); - if (!state || state.inFlight) return; - - await handleTriggerInternal({ - sessionID, - trigger: "deferred.retry", - deferredRetry: true, - }); - } - - function isProviderEnabled(providerId: string): boolean { - return config.enabledProviders === "auto" || config.enabledProviders.includes(providerId); - } - - async function shouldBypassToastCacheForLiveLocalUsage(params: { - sessionID: string; - sessionMeta?: SessionModelMeta; - }): Promise { - if ( - isProviderEnabled(QUOTA_PROVIDERS_AGGREGATE_ID) && - customQuotaProviderDefinitions(config.quotaProviders).some( - (definition) => definition.mode === "local-estimate", - ) - ) { - return true; - } - - const currentSession = - params.sessionMeta ?? (await dependencies.resolveSessionMeta(params.sessionID)); - const currentModel = currentSession.modelID; - if ( - currentSession.providerID === "alibaba-coding-plan" || - currentSession.providerID === "alibaba" || - isAlibabaModelId(currentModel) - ) { - const plan = await resolveAlibabaCodingPlanAuthCached({ - maxAgeMs: DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS, - fallbackTier: "lite", - }); - return plan.state === "configured" && isProviderEnabled("alibaba-coding-plan"); - } - - if (isCursorProviderId(currentSession.providerID) || isCursorModelId(currentModel)) { - return isProviderEnabled("cursor"); - } - - return false; - } - - function buildToastCacheKey(params: { - sessionID: string; - sessionMeta?: SessionModelMeta; - }): string { - const formatStyle = resolveQuotaFormatStyle(config.formatStyle); - const enabledProviders = - config.enabledProviders === "auto" ? "auto" : config.enabledProviders.join(","); - const currentModel = - config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.modelID ?? "") : ""; - const currentProviderID = - config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.providerID ?? "") : ""; - const renderIdentity = JSON.stringify({ - accountingDetail: config.accountingDetail, - quotaProjection: config.quotaProjection, - percentLabelStyle: config.percentLabelStyle, - resetTimeDecimals: config.resetTimeDecimals, - resetTimeSpaced: config.resetTimeSpaced, - sessionTokenScope: config.sessionTokenScope, - opencodeGoWindows: config.opencodeGoWindows, - opencodeMonthlyLimit: config.opencodeMonthlyLimit, - quotaProviders: config.quotaProviders, - }); - const rootIdentity = JSON.stringify(dependencies.roots()); - - return [ - `sessionID=${params.sessionID}`, - `enabledProviders=${enabledProviders}`, - `formatStyle=${formatStyle}`, - `percentDisplayMode=${config.percentDisplayMode}`, - `layout=${JSON.stringify(config.layout)}`, - `showSessionTokens=${config.showSessionTokens ? "yes" : "no"}`, - `onlyCurrentModel=${config.onlyCurrentModel ? "yes" : "no"}`, - `currentModel=${currentModel}`, - `currentProviderID=${currentProviderID}`, - `anthropicBinaryPath=${config.anthropicBinaryPath}`, - `cursorPlan=${config.cursorPlan}`, - `cursorIncludedApiUsd=${config.cursorIncludedApiUsd ?? ""}`, - `cursorBillingCycleStartDay=${config.cursorBillingCycleStartDay ?? ""}`, - `renderIdentity=${renderIdentity}`, - `rootIdentity=${rootIdentity}`, - ].join("|"); - } - - async function kickPricingRefresh(params: { - reason: "init" | "tokens" | "status"; - maxWaitMs?: number; - }): Promise { - try { - const refreshPromise = maybeRefreshPricingSnapshot({ - reason: params.reason, - snapshotSelection: config.pricingSnapshot.source, - }); - const guardedRefreshPromise = refreshPromise.catch(() => undefined); - if (!params.maxWaitMs || params.maxWaitMs <= 0) { - void guardedRefreshPromise; - return; - } - - await Promise.race([ - guardedRefreshPromise, - new Promise((resolve) => { - setTimeout(resolve, params.maxWaitMs); - }), - ]); - } catch (error) { - await dependencies.log("Pricing refresh failed", { - reason: params.reason, - error: error instanceof Error ? error.message : String(error), - }); - } - } - - function onFirstConfigLoaded(): void { - if (initDone) return; - initDone = true; - - if (config.enabled) { - void kickPricingRefresh({ reason: "init" }); - } - - dependencies.onInitialized({ - configLoaded, - configSource: configMeta.source, - configPaths: configMeta.paths, - enabledProviders: config.enabledProviders, - minIntervalMs: config.minIntervalMs, - cursorPlan: config.cursorPlan, - cursorIncludedApiUsd: config.cursorIncludedApiUsd, - cursorBillingCycleStartDay: config.cursorBillingCycleStartDay, - pricingSnapshotSource: config.pricingSnapshot.source, - pricingSnapshotAutoRefresh: config.pricingSnapshot.autoRefresh, - showOnIdle: config.showOnIdle, - showOnQuestion: config.showOnQuestion, - showOnCompact: config.showOnCompact, - showOnBothFail: config.showOnBothFail, - }); - } - - async function refreshConfig(): Promise { - if (configInFlight) return configInFlight; - - configInFlight = (async () => { - try { - const runtime = await resolveQuotaRuntimeContext({ - client: dependencies.client, - roots: dependencies.roots(), - }); - configMeta = runtime.configMeta; - config = runtime.config; - runtimeProviders = runtime.providers; - setPricingSnapshotAutoRefresh(config.pricingSnapshot.autoRefresh); - setPricingSnapshotSelection(config.pricingSnapshot.source); - configLoaded = true; - onFirstConfigLoaded(); - } catch { - config = DEFAULT_CONFIG; - configMeta = createLoadConfigMeta(); - runtimeProviders = getProviders(); - setPricingSnapshotAutoRefresh(DEFAULT_CONFIG.pricingSnapshot.autoRefresh); - setPricingSnapshotSelection(DEFAULT_CONFIG.pricingSnapshot.source); - } finally { - configInFlight = null; - } - })(); - - return configInFlight; - } - - async function resolvePluginRuntimeContext(params: { - sessionID?: string; - sessionMeta?: SessionModelMeta; - }): Promise { - return resolveQuotaRuntimeContext({ - client: dependencies.client, - roots: dependencies.roots(), - config, - configMeta, - providers: runtimeProviders, - sessionID: params.sessionID, - sessionMeta: params.sessionMeta, - resolveSessionMeta: dependencies.resolveSessionMeta, - includeSessionMeta: (runtimeConfig) => runtimeConfig.onlyCurrentModel, - }); - } - - function emptyCollectionResult(params: { - message: string | null; - retryable: boolean; - retryReason?: DeferredQuotaRefreshReason; - }): QuotaToastCollectionResult { - return { - message: params.message, - cacheRenderedMessage: false, - retryable: params.retryable, - retryReason: params.retryReason, - hasQuotaRows: false, - detectedProviderIds: [], - freshProviderResults: [], - shouldReconcileDetectedProviders: false, - }; - } - - async function fetchQuotaMessageResult(params: { - trigger: string; - sessionID: string; - sessionMeta?: SessionModelMeta; - bypassProviderCache?: boolean; - }): Promise { - if (!configLoaded) { - await refreshConfig(); - } - - if (!configLoaded) { - return emptyCollectionResult({ - message: config.debug - ? formatQuotaToastDebugInfo({ - trigger: params.trigger, - reason: "config load failed", - config, - configMeta, - }) - : null, - retryable: true, - retryReason: "config_load_failed", - }); - } - - if (!config.enabled) { - return emptyCollectionResult({ - message: config.debug - ? formatQuotaToastDebugInfo({ - trigger: params.trigger, - reason: "disabled", - config, - configMeta, - }) - : null, - retryable: false, - }); - } - - if (config.enabledProviders !== "auto" && config.enabledProviders.length === 0) { - return emptyCollectionResult({ - message: config.debug - ? formatQuotaToastDebugInfo({ - trigger: params.trigger, - reason: "enabledProviders empty", - config, - configMeta, - }) - : null, - retryable: false, - }); - } - - const runtime = await resolvePluginRuntimeContext({ - sessionID: params.sessionID, - sessionMeta: params.sessionMeta, - }); - const collection = await collectQuotaToastMessage({ - trigger: params.trigger, - runtime, - bypassProviderCache: params.bypassProviderCache, - }); - - if (collection.shouldReconcileDetectedProviders) { - await dependencies.reconcileDetectedProviders(collection.detectedProviderIds); - } - if (runtime.config.showSessionTokens) { - dependencies.setSessionTokenError(collection.sessionTokenError); - } - - return collection; - } - - async function reconcileDeferredQuotaRefresh(params: { - sessionID: string; - result: QuotaToastCollectionResult; - consumedDeferredRetry: boolean; - trigger: string; - }): Promise { - const existing = deferredQuotaRefreshes.get(params.sessionID); - - if (!params.result.retryable) { - if (existing) { - clearDeferredQuotaRefresh(params.sessionID); - await dependencies.log("Deferred quota refresh cleared", { - sessionID: params.sessionID, - trigger: params.trigger, - reason: params.result.hasQuotaRows ? "quota_rows_available" : "not_retryable", - }); - } - return; - } - - if (!params.result.retryReason) return; - - scheduleDeferredQuotaRefresh({ - sessionID: params.sessionID, - reason: params.result.retryReason, - incrementAttempts: params.consumedDeferredRetry, - }); - } - - function isTriggerEnabled(trigger: string): boolean { - if (trigger === "session.idle") return config.showOnIdle; - if (trigger === "session.compacted") return config.showOnCompact; - if (trigger === "question") return config.showOnQuestion; - return trigger === "deferred.retry"; - } - - function triggerMaintainerAnnouncementToastFallback( - trigger: string, - detectedProviderIds: string[], - runtimeConfig: QuotaToastConfig, - ): void { - if ( - !maintainerAnnouncementToastFallback.pending || - maintainerAnnouncementToastFallback.inFlight - ) { - return; - } - - if (!runtimeConfig.enabled || !runtimeConfig.enableToast) { - maintainerAnnouncementToastFallback.pending = false; - return; - } - - if ( - !runtimeConfig.maintainerAnnouncements.enabled || - !runtimeConfig.maintainerAnnouncements.home - ) { - maintainerAnnouncementToastFallback.pending = false; - return; - } - - maintainerAnnouncementToastFallback.inFlight = true; - void (async () => { - try { - const summary = getMaintainerAnnouncementsSummary({ - announcements: BUNDLED_MAINTAINER_ANNOUNCEMENTS, - enabledProviders: detectedProviderIds, - }); - - if (summary.activeCount <= 0) { - if (summary.futureCount <= 0) { - maintainerAnnouncementToastFallback.pending = false; - } - return; - } - - const tuiDiagnostics = await inspectTuiConfig({ roots: dependencies.roots() }); - if (tuiDiagnostics.quotaPluginConfigured) { - maintainerAnnouncementToastFallback.pending = false; - return; - } - - const message = formatMaintainerAnnouncementHomeCountLine(summary.activeCount); - if (!message) return; - - await dependencies.showToast({ - message: sanitizeDisplayText(message), - variant: "info", - duration: runtimeConfig.toastDurationMs, - }); - maintainerAnnouncementToastFallback.pending = false; - await dependencies.log("Displayed maintainer announcement fallback toast", { trigger }); - } catch (error) { - await dependencies.log("Failed to show maintainer announcement fallback toast", { - trigger, - error: error instanceof Error ? error.message : String(error), - }); - } finally { - maintainerAnnouncementToastFallback.inFlight = false; - } - })(); - } - - async function emitQuotaToastPlan(params: { - sessionID: string; - trigger: string; - plan: QuotaToastEmissionPlan; - }): Promise { - const { - config: runtimeConfig, - message, - detectedProviderIds, - freshProviderResults, - } = params.plan; - let resetNotification: string | undefined; - if ( - runtimeConfig.enableToast && - runtimeConfig.resetNotifications.enabled && - freshProviderResults.length > 0 - ) { - try { - const notices = await observeQuotaResetNotifications({ - providers: freshProviderResults, - windows: runtimeConfig.resetNotifications.windows, - }); - resetNotification = formatQuotaResetNotification(notices) ?? undefined; - } catch (error) { - await dependencies.log("Failed to observe quota reset transitions", { - error: error instanceof Error ? error.message : String(error), - }); - } - } - - if (params.plan.deferredRetryResult) { - await dependencies.log("Deferred quota refresh did not produce reportable data", { - sessionID: params.sessionID, - trigger: params.trigger, - retryable: params.plan.deferredRetryResult.retryable, - retryReason: params.plan.deferredRetryResult.retryReason, - }); - return; - } - - if (!message) { - await dependencies.log("No quota message to display", { trigger: params.trigger }); - return; - } - - if (!runtimeConfig.enableToast) { - await dependencies.log("Toast disabled (enableToast=false)", { trigger: params.trigger }); - return; - } - - try { - await dependencies.showToast({ - ...(runtimeConfig.percentLabelStyle === "bare" - ? { title: formatQuotaModeHeading(runtimeConfig.percentDisplayMode) } - : {}), - message: sanitizeDisplayText(message), - variant: "info", - duration: runtimeConfig.toastDurationMs, - }); - triggerMaintainerAnnouncementToastFallback( - params.trigger, - detectedProviderIds, - runtimeConfig, - ); - await dependencies.log("Displayed quota toast", { message, trigger: params.trigger }); - if (resetNotification) { - await dependencies.showToast({ - title: "Quota available", - message: sanitizeDisplayText(resetNotification), - variant: "success", - duration: runtimeConfig.toastDurationMs, - }); - await dependencies.log("Displayed quota reset notification", { - message: resetNotification, - trigger: params.trigger, - }); - } - } catch (error) { - await dependencies.log("Failed to show toast", { - error: error instanceof Error ? error.message : String(error), - }); - } - } - - async function prepareTrigger(params: { - sessionID: string; - trigger: string; - deferredRetry?: boolean; - }): Promise { - if (!configLoaded) { - await refreshConfig(); - } - - if (!config.enabled) { - clearDeferredQuotaRefresh(params.sessionID); - return null; - } - if (!isTriggerEnabled(params.trigger)) return null; - - const pendingDeferred = deferredQuotaRefreshes.get(params.sessionID); - const consumedDeferredRetry = params.deferredRetry === true || Boolean(pendingDeferred); - if (pendingDeferred) { - if (pendingDeferred.inFlight && !params.deferredRetry) { - await dependencies.log("Skipping duplicate deferred quota refresh", { - sessionID: params.sessionID, - trigger: params.trigger, - }); - return null; - } - pendingDeferred.inFlight = true; - clearDeferredQuotaRefreshTimer(pendingDeferred); - } - - let planPrepared = false; - let completed = false; - const complete = () => { - if (completed) return; - completed = true; - const state = deferredQuotaRefreshes.get(params.sessionID); - if (state && state === pendingDeferred) { - state.inFlight = false; - } - }; - - try { - if (await dependencies.isSubagentSession(params.sessionID)) { - if (consumedDeferredRetry) { - clearDeferredQuotaRefresh(params.sessionID); - } - await dependencies.log("Skipping toast for subagent session", { - sessionID: params.sessionID, - trigger: params.trigger, - }); - return null; - } - - const sessionMeta = await dependencies.resolveSessionMeta(params.sessionID); - const bypassForLiveLocalUsage = await shouldBypassToastCacheForLiveLocalUsage({ - sessionID: params.sessionID, - sessionMeta, - }); - const bypassMessageCache = config.debug || consumedDeferredRetry || bypassForLiveLocalUsage; - const bypassProviderCache = consumedDeferredRetry || bypassForLiveLocalUsage; - const toastCacheKey = buildToastCacheKey({ - sessionID: params.sessionID, - sessionMeta, - }); - - let fetchResult: QuotaToastCollectionResult | undefined; - const fetchForToast = () => - fetchQuotaMessageResult({ - trigger: params.trigger, - sessionID: params.sessionID, - sessionMeta, - bypassProviderCache, - }); - - const message = bypassMessageCache - ? await (async () => { - fetchResult = await fetchForToast(); - return fetchResult.message; - })() - : await (async () => { - const fetched: { result?: QuotaToastCollectionResult } = {}; - const cachedMessage = await getOrFetchWithCacheControl( - toastCacheKey, - async () => { - const result = await fetchForToast(); - fetched.result = result; - return { - message: result.message, - cache: Boolean( - result.message && result.cacheRenderedMessage && result.hasQuotaRows, - ), - }; - }, - config.minIntervalMs, - ); - fetchResult = fetched.result; - return cachedMessage; - })(); - - if (fetchResult) { - detectedProviderIdsByToastCacheKey.set(toastCacheKey, [...fetchResult.detectedProviderIds]); - await reconcileDeferredQuotaRefresh({ - sessionID: params.sessionID, - result: fetchResult, - consumedDeferredRetry, - trigger: params.trigger, - }); - } - - planPrepared = true; - return { - config, - message, - detectedProviderIds: - fetchResult?.detectedProviderIds ?? - detectedProviderIdsByToastCacheKey.get(toastCacheKey) ?? - [], - freshProviderResults: fetchResult?.freshProviderResults ?? [], - complete, - deferredRetryResult: - params.deferredRetry && fetchResult && !fetchResult.hasQuotaRows - ? { - retryable: fetchResult.retryable, - retryReason: fetchResult.retryReason, - } - : undefined, - }; - } finally { - if (!planPrepared) complete(); - } - } - - async function handleTriggerInternal(params: { - sessionID: string; - trigger: string; - deferredRetry?: boolean; - }): Promise { - const plan = await prepareTrigger(params); - if (!plan) return; - - try { - await emitQuotaToastPlan({ ...params, plan }); - } finally { - plan.complete(); - } - } - - async function handleTrigger(params: { sessionID: string; trigger: string }): Promise { - await handleTriggerInternal(params); - } - - return { handleTrigger }; -} - -export function formatQuotaToastDebugInfo(params: { - trigger: string; - reason: string; - config: QuotaToastConfig; - configMeta: Pick; - currentModel?: string; - availability?: Array<{ id: string; ok: boolean }>; -}): string { - const availability = params.availability - ? params.availability.map((item) => `${item.id}=${item.ok ? "ok" : "no"}`).join(" ") - : "unknown"; - - const providers = - params.config.enabledProviders === "auto" - ? "(auto)" - : params.config.enabledProviders.length > 0 - ? params.config.enabledProviders.join(",") - : "(none)"; - - const modelPart = params.currentModel ? ` model=${params.currentModel}` : ""; - const paths = params.configMeta.paths.length > 0 ? params.configMeta.paths.join(" | ") : "(none)"; - - return [ - "Quota Toast Debug (opencode-quota)", - `trigger=${params.trigger} reason=${params.reason}`, - `configSource=${params.configMeta.source} paths=${paths}`, - `enabled=${params.config.enabled} providers=${providers}${modelPart}`, - `available=${availability}`, - ].join("\n"); -} - -function hasRetryableProviderError(errors: Array<{ retryable?: boolean }>): boolean { - return errors.some((error) => error.retryable === true); -} - -export async function collectQuotaToastMessage(params: { - trigger: string; - runtime: QuotaRuntimeContext; - bypassProviderCache?: boolean; -}): Promise { - const runtimeConfig = params.runtime.config; - const quotaResult = await collectQuotaRenderData({ - client: params.runtime.client, - resolveRuntimeProviderIds: params.runtime.resolveRuntimeProviderIds, - config: runtimeConfig, - configMeta: params.runtime.configMeta, - request: createQuotaRuntimeRequestContext(params.runtime), - surfaceExplicitProviderIssues: true, - formatStyle: resolveQuotaFormatStyle(runtimeConfig.formatStyle), - bypassProviderCache: params.bypassProviderCache, - providers: params.runtime.providers, - }); - const { - selection, - availability, - active, - providerResults, - attemptedAny, - hasExplicitProviderIssues, - data, - } = quotaResult; - const detectedProviderIds = active.map((provider) => provider.id); - const collectionMetadata = { - detectedProviderIds, - freshProviderResults: providerResults, - sessionTokenError: quotaResult.sessionTokenError, - shouldReconcileDetectedProviders: selection?.isAutoMode === true, - }; - - const currentModel = selection?.currentModel; - const errors = data?.errors ?? []; - const hasProviderQuotaRows = Boolean(data?.entries.length); - const hasQuotaRows = Boolean(hasProviderQuotaRows || data?.sessionTokens); - const hasRetryableFetchFailure = attemptedAny && hasRetryableProviderError(errors); - const retryableAvailabilityFailure = - active.length === 0 && availability.some((item) => !item.ok && item.error === true); - - if (active.length === 0 && !(hasExplicitProviderIssues && errors.length > 0)) { - const message = runtimeConfig.debug - ? formatQuotaToastDebugInfo({ - trigger: params.trigger, - reason: "no enabled providers available", - config: runtimeConfig, - configMeta: params.runtime.configMeta, - currentModel, - availability: availability.map((item) => ({ - id: item.provider.id, - ok: item.ok, - })), - }) - : null; - const retryableNoProviders = selection?.isAutoMode === true || retryableAvailabilityFailure; - return { - message, - cacheRenderedMessage: false, - retryable: retryableNoProviders, - retryReason: retryableNoProviders ? "no_available_providers" : undefined, - hasQuotaRows: false, - ...collectionMetadata, - }; - } - - if (hasQuotaRows) { - const formatted = formatQuotaRows({ - version: "1.0.0", - layout: runtimeConfig.layout, - entries: data?.entries ?? [], - errors: data?.errors ?? [], - style: resolveQuotaFormatStyle(runtimeConfig.formatStyle), - percentDisplayMode: runtimeConfig.percentDisplayMode, - percentLabelStyle: runtimeConfig.percentLabelStyle, - accountingDetail: runtimeConfig.accountingDetail, - resetTimeDecimals: runtimeConfig.resetTimeDecimals, - resetTimeSpaced: runtimeConfig.resetTimeSpaced, - sessionTokens: data?.sessionTokens, - }); - - const retryableMaskedProviderFailure = !hasProviderQuotaRows && hasRetryableFetchFailure; - - if (!runtimeConfig.debug) { - return { - message: formatted, - cacheRenderedMessage: true, - retryable: retryableMaskedProviderFailure, - retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined, - hasQuotaRows: true, - ...collectionMetadata, - }; - } - - const debugFooter = `\n\n[debug] src=${params.runtime.configMeta.source} providers=${runtimeConfig.enabledProviders === "auto" ? "(auto)" : runtimeConfig.enabledProviders.join(",") || "(none)"} avail=${availability - .map((item) => `${item.provider.id}:${item.ok ? "ok" : "no"}`) - .join(" ")}`; - - return { - message: formatted + debugFooter, - cacheRenderedMessage: false, - retryable: retryableMaskedProviderFailure, - retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined, - hasQuotaRows: true, - ...collectionMetadata, - }; - } - - if ( - (runtimeConfig.showOnBothFail && attemptedAny && errors.length > 0) || - hasExplicitProviderIssues - ) { - const errorLines = errors.map((error) => `${error.label}: ${error.message}`).join("\n"); - const retryableFetchFailure = hasRetryableFetchFailure; - const retryableFailure = retryableFetchFailure || retryableAvailabilityFailure; - const retryReason: DeferredQuotaRefreshReason | undefined = retryableFetchFailure - ? "provider_fetch_failed" - : retryableAvailabilityFailure - ? "no_available_providers" - : undefined; - const message = !runtimeConfig.debug - ? errorLines || "Quota unavailable" - : (errorLines || "Quota unavailable") + - "\n\n" + - formatQuotaToastDebugInfo({ - trigger: params.trigger, - reason: hasExplicitProviderIssues - ? "providers missing/unavailable" - : "all providers failed", - config: runtimeConfig, - configMeta: params.runtime.configMeta, - currentModel, - availability: availability.map((item) => ({ - id: item.provider.id, - ok: item.ok, - })), - }); - return { - message, - cacheRenderedMessage: false, - retryable: retryableFailure, - retryReason, - hasQuotaRows: false, - ...collectionMetadata, - }; - } - - const retryableNoData = - hasRetryableFetchFailure || - (selection?.isAutoMode === true && active.length > 0 && errors.length === 0); - return { - message: runtimeConfig.debug - ? formatQuotaToastDebugInfo({ - trigger: params.trigger, - reason: "no entries", - config: runtimeConfig, - configMeta: params.runtime.configMeta, - currentModel, - availability: availability.map((item) => ({ - id: item.provider.id, - ok: item.ok, - })), - }) - : null, - cacheRenderedMessage: false, - retryable: retryableNoData, - retryReason: hasRetryableFetchFailure - ? "provider_fetch_failed" - : retryableNoData - ? "no_reportable_data" - : undefined, - hasQuotaRows: false, - ...collectionMetadata, - }; -} diff --git a/src/lib/tui-panel-state.ts b/src/lib/tui-panel-state.ts index 97f00f69..f7748296 100644 --- a/src/lib/tui-panel-state.ts +++ b/src/lib/tui-panel-state.ts @@ -1,30 +1,4 @@ -import { sanitizeSingleLineDisplayText } from "./display-sanitize.js"; import type { AccountingMetadata, QuotaRunwayProjection } from "./entries.js"; -import type { PercentDisplayMode } from "./types.js"; - -const SIDEBAR_LOADING_LINE = "Loading…"; -const SIDEBAR_UNAVAILABLE_LINE = "Unavailable"; -const COMPACT_LOADING_TEXT = "Quota loading…"; - -type PanelStatus = "loading" | "disabled" | "ready"; - -export type SidebarPanelState = { - status: PanelStatus; - lines: string[]; - linesExpanded?: string[]; - providerCount?: number; - headerPercentMode?: PercentDisplayMode; -}; - -export type CompactStatusState = - | { status: "loading"; text?: string } - | { status: "disabled"; text?: string } - | { status: "ready"; text: string }; - -export type HomeBottomState = - | { status: "loading"; announcementText?: string; compact: CompactStatusState } - | { status: "disabled"; announcementText?: string; compact: CompactStatusState } - | { status: "ready"; announcementText?: string; compact: CompactStatusState }; export type PromptBarEntry = { /** Preformatted primary semantic label/value segment for rich accounting rows. */ @@ -39,56 +13,3 @@ export type PromptBarEntry = { runway?: QuotaRunwayProjection; accounting?: AccountingMetadata; }; - -export type PromptBarState = - | { status: "loading" | "disabled" } - | { - status: "ready"; - entry?: PromptBarEntry; - percentDisplayMode?: PercentDisplayMode; - resetTimeDecimals?: number; - resetTimeSpaced?: boolean; - }; - -export function shouldRenderSidebarPanel(panel: SidebarPanelState): boolean { - return panel.status !== "disabled"; -} - -export function getSidebarPanelLines(panel: SidebarPanelState): string[] { - if (panel.lines.length > 0) return panel.lines; - - switch (panel.status) { - case "ready": - return [SIDEBAR_UNAVAILABLE_LINE]; - case "loading": - return [SIDEBAR_LOADING_LINE]; - default: - return []; - } -} - -export function getSidebarPanelLinesExpanded(panel: SidebarPanelState): string[] { - if (panel.linesExpanded && panel.linesExpanded.length > 0) return panel.linesExpanded; - return getSidebarPanelLines(panel); -} - -export function shouldRenderCompactStatus(panel: CompactStatusState): boolean { - return panel.status !== "disabled"; -} - -export function getCompactStatusText(panel: CompactStatusState): string { - if (panel.status === "disabled") return ""; - - const text = sanitizeSingleLineDisplayText(panel.text ?? ""); - if (text) return text; - - return panel.status === "loading" ? COMPACT_LOADING_TEXT : ""; -} - -export function shouldRenderHomeBottom(panel: HomeBottomState): boolean { - return Boolean(getHomeBottomAnnouncementText(panel) || shouldRenderCompactStatus(panel.compact)); -} - -export function getHomeBottomAnnouncementText(panel: HomeBottomState): string { - return sanitizeSingleLineDisplayText(panel.announcementText ?? ""); -} diff --git a/tests/cache.test.ts b/tests/cache.test.ts deleted file mode 100644 index 869074eb..00000000 --- a/tests/cache.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import { - clearCache, - getCachedToast, - getOrFetch, - getOrFetchWithCacheControl, - shouldFetch, - updateCache, -} from "../src/lib/cache.js"; - -describe("cache", () => { - const primaryKey = "toast:primary"; - const secondaryKey = "toast:secondary"; - - beforeEach(() => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z")); - clearCache(); - }); - - afterEach(() => { - clearCache(); - }); - - it("returns cached toast within minIntervalMs", () => { - updateCache(primaryKey, "hello"); - expect(getCachedToast(primaryKey, 60_000)).toBe("hello"); - - vi.advanceTimersByTime(59_999); - expect(getCachedToast(primaryKey, 60_000)).toBe("hello"); - }); - - it("treats cached toast as stale after minIntervalMs", () => { - updateCache(primaryKey, "hello"); - vi.advanceTimersByTime(60_000); - expect(getCachedToast(primaryKey, 60_000)).toBeNull(); - }); - - it("deduplicates concurrent fetches per key", async () => { - const fetchFn = vi.fn(async () => { - await vi.advanceTimersByTimeAsync(10); - return "result"; - }); - - const p1 = getOrFetch(primaryKey, fetchFn, 0); - const p2 = getOrFetch(primaryKey, fetchFn, 0); - - await vi.runAllTimersAsync(); - - await expect(p1).resolves.toBe("result"); - await expect(p2).resolves.toBe("result"); - expect(fetchFn).toHaveBeenCalledTimes(1); - }); - - it("shouldFetch honors minIntervalMs per key", () => { - // shouldFetch is based on internal lastFetchTime; this asserts its basic behavior - // when the cache layer calls into it. - expect(shouldFetch(primaryKey, 60_000)).toBe(true); - }); - - it("does not cache when cache=false", async () => { - const fetchFn = vi.fn(async () => ({ message: "err", cache: false })); - - const out1 = await getOrFetchWithCacheControl(primaryKey, fetchFn, 60_000); - expect(out1).toBe("err"); - expect(fetchFn).toHaveBeenCalledTimes(1); - - // Second call should fetch again (not cached / not throttled) - const out2 = await getOrFetchWithCacheControl(primaryKey, fetchFn, 60_000); - expect(out2).toBe("err"); - expect(fetchFn).toHaveBeenCalledTimes(2); - }); - - it("keeps keyed caches isolated", async () => { - const fetchFn = vi.fn(); - fetchFn.mockResolvedValueOnce({ message: "primary", cache: true }); - fetchFn.mockResolvedValueOnce({ message: "secondary", cache: true }); - - await expect(getOrFetchWithCacheControl(primaryKey, fetchFn, 60_000)).resolves.toBe("primary"); - await expect(getOrFetchWithCacheControl(secondaryKey, fetchFn, 60_000)).resolves.toBe( - "secondary", - ); - - expect(getCachedToast(primaryKey, 60_000)).toBe("primary"); - expect(getCachedToast(secondaryKey, 60_000)).toBe("secondary"); - expect(fetchFn).toHaveBeenCalledTimes(2); - }); - - it("clears only the requested key when given one", () => { - updateCache(primaryKey, "primary"); - updateCache(secondaryKey, "secondary"); - - clearCache(primaryKey); - - expect(getCachedToast(primaryKey, 60_000)).toBeNull(); - expect(getCachedToast(secondaryKey, 60_000)).toBe("secondary"); - }); -}); diff --git a/tests/command-handled.test.ts b/tests/command-handled.test.ts deleted file mode 100644 index 224f1dbd..00000000 --- a/tests/command-handled.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - COMMAND_HANDLED_SENTINEL, - handled, - isCommandHandledError, -} from "../src/lib/command-handled.js"; - -function captureHandledAbort(): unknown { - try { - handled(); - } catch (err) { - return err; - } - throw new Error("handled() returned normally"); -} - -describe("command handled abort", () => { - it("throws a quiet branded handled abort", () => { - const err = captureHandledAbort(); - - expect(err).toBeInstanceOf(Error); - expect(isCommandHandledError(err)).toBe(true); - expect((err as Error).message).toBe(""); - expect((err as Error).name).toBe(""); - expect(String(err)).toBe(""); - expect((err as Error).stack).toBe(""); - expect(String(err)).not.toContain(COMMAND_HANDLED_SENTINEL); - }); - - it("recognizes legacy sentinel-message errors", () => { - expect(isCommandHandledError(new Error(COMMAND_HANDLED_SENTINEL))).toBe(true); - expect(isCommandHandledError(new Error())).toBe(false); - }); -}); diff --git a/tests/helpers/plugin-test-harness.ts b/tests/helpers/plugin-test-harness.ts index c94f691a..b46f029f 100644 --- a/tests/helpers/plugin-test-harness.ts +++ b/tests/helpers/plugin-test-harness.ts @@ -261,7 +261,7 @@ export function seedDefaultPluginBootstrapMocks( vi.clearAllMocks(); if (options.resetModules || options.resetPluginState) { - // Fresh module instances clear singleton state such as src/lib/cache.ts. + // Fresh module instances clear module-level singleton state. vi.resetModules(); } diff --git a/tests/lib.command-surfaces.test.ts b/tests/lib.command-surfaces.test.ts deleted file mode 100644 index 60e6828b..00000000 --- a/tests/lib.command-surfaces.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { shouldRegisterServerSlashCommands } from "../src/lib/command-surfaces.js"; - -describe("deterministic command surfaces", () => { - it("keeps server slash commands out of the embedded TUI worker", () => { - expect(shouldRegisterServerSlashCommands({ isMainThread: false, argv: ["opencode"] })).toBe( - false, - ); - }); - - it("registers server slash commands for mini mode", () => { - expect( - shouldRegisterServerSlashCommands({ isMainThread: true, argv: ["opencode", "--mini"] }), - ).toBe(true); - expect( - shouldRegisterServerSlashCommands({ isMainThread: true, argv: ["opencode", "--mini=true"] }), - ).toBe(true); - }); - - it("registers slash commands on web and server hosts", () => { - expect( - shouldRegisterServerSlashCommands({ isMainThread: true, argv: ["opencode", "web"] }), - ).toBe(true); - expect( - shouldRegisterServerSlashCommands({ isMainThread: true, argv: ["opencode", "serve"] }), - ).toBe(true); - }); -}); diff --git a/tests/lib.quota-reset-notifications.test.ts b/tests/lib.quota-reset-notifications.test.ts deleted file mode 100644 index 7421f13a..00000000 --- a/tests/lib.quota-reset-notifications.test.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { spawn } from "node:child_process"; -import { once } from "node:events"; -import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; - -import type { QuotaResetProviderResult } from "../src/lib/quota-reset-notifications.js"; -import { - formatQuotaResetNotification, - observeQuotaResetNotifications, -} from "../src/lib/quota-reset-notifications.js"; - -const created: string[] = []; - -async function statePath(): Promise { - const root = await mkdtemp(join(tmpdir(), "opencode-quota-reset-")); - created.push(root); - return join(root, "state.json"); -} - -function provider(params: { - percentRemaining: number; - resetAtMs: number; - providerId?: string; - sourceId?: string; - name?: string; - group?: string; - label?: string; - resultType?: "quota" | "usage"; -}): QuotaResetProviderResult { - const providerId = params.providerId ?? "openai"; - const name = params.name ?? (providerId === "openai" ? "OpenAI" : "Anthropic"); - return { - providerId, - result: { - attempted: true, - errors: [], - entries: [ - { - name, - group: params.group ?? (params.sourceId ? `${name} (${params.sourceId})` : name), - label: params.label ?? "7d", - percentRemaining: params.percentRemaining, - resetTimeIso: new Date(params.resetAtMs).toISOString(), - accounting: { - resultType: params.resultType ?? "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", - sourceId: params.sourceId, - }, - }, - ], - }, - }; -} - -afterEach(async () => { - await Promise.all(created.splice(0).map((path) => rm(path, { recursive: true, force: true }))); -}); - -describe("quota reset notifications", () => { - it("uses the first observation as a baseline and persists one acknowledgement", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const firstReset = start + 60 * 60 * 1000; - const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; - - expect( - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 12, resetAtMs: firstReset })], - windows: ["weekly"], - nowMs: start, - statePath: path, - }), - ).toEqual([]); - - const notices = await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 100, resetAtMs: nextReset })], - windows: ["weekly"], - nowMs: firstReset + 60_000, - statePath: path, - }); - expect(notices).toEqual([ - { - providerId: "openai", - label: "OpenAI", - window: "weekly", - percentRemaining: 100, - }, - ]); - expect(formatQuotaResetNotification(notices)).toBe( - "Weekly quota reset: OpenAI is available again (100% remaining).", - ); - - expect( - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 100, resetAtMs: nextReset })], - windows: ["weekly"], - nowMs: firstReset + 120_000, - statePath: path, - }), - ).toEqual([]); - }); - - it("reclaims a lock left by an exited process", async () => { - const path = await statePath(); - const lockPath = `${path}.lock`; - await mkdir(lockPath, { mode: 0o700 }); - const exitedOwner = spawn(process.execPath, ["-e", ""]); - const exitedPid = exitedOwner.pid; - if (!exitedPid) throw new Error("Failed to start lock-owner test process"); - await once(exitedOwner, "exit"); - await writeFile( - join(lockPath, "owner.json"), - JSON.stringify({ token: "exited-owner", pid: exitedPid, createdAtMs: Date.now() }), - ); - - const start = Date.UTC(2026, 0, 1, 12); - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 10, resetAtMs: start + 60 * 60 * 1000 })], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - expect(Object.keys(JSON.parse(await readFile(path, "utf8")).observations)).toHaveLength(1); - await expect(stat(lockPath)).rejects.toMatchObject({ code: "ENOENT" }); - }); - - it("serializes concurrent observers so the same reset is notified once", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const firstReset = start + 60 * 60 * 1000; - const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 10, resetAtMs: firstReset })], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - const results = await Promise.all( - [1, 2].map(() => - observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 100, resetAtMs: nextReset })], - windows: ["weekly"], - nowMs: firstReset + 60_000, - statePath: path, - }), - ), - ); - - expect(results.flat()).toHaveLength(1); - }); - - it("preserves disjoint observations written concurrently", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const reset = start + 60 * 60 * 1000; - - await Promise.all( - ["work@example.com", "personal@example.com"].map((sourceId) => - observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 5, resetAtMs: reset, sourceId })], - windows: ["weekly"], - nowMs: start, - statePath: path, - }), - ), - ); - - const state = JSON.parse(await readFile(path, "utf8")); - expect(Object.keys(state.observations)).toHaveLength(2); - }); - - it("preserves a pre-boundary baseline while a provider still reports the expired reset", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const firstReset = start + 60 * 60 * 1000; - const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 10, resetAtMs: firstReset })], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 10, resetAtMs: firstReset })], - windows: ["weekly"], - nowMs: firstReset + 60_000, - statePath: path, - }); - - expect( - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 100, resetAtMs: nextReset })], - windows: ["weekly"], - nowMs: firstReset + 120_000, - statePath: path, - }), - ).toHaveLength(1); - }); - - it("keeps identity stable when presentation labels change", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const firstReset = start + 60 * 60 * 1000; - const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; - await observeQuotaResetNotifications({ - providers: [ - provider({ - percentRemaining: 10, - resetAtMs: firstReset, - sourceId: "account-1", - name: "Old provider name", - group: "Old account label", - label: "7d", - }), - ], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - const notices = await observeQuotaResetNotifications({ - providers: [ - provider({ - percentRemaining: 100, - resetAtMs: nextReset, - sourceId: "account-1", - name: "New provider name", - group: "New account label", - label: "weekly", - }), - ], - windows: ["weekly"], - nowMs: firstReset + 60_000, - statePath: path, - }); - - expect(notices).toEqual([ - expect.objectContaining({ providerId: "openai", label: "New account label" }), - ]); - }); - - it("skips ambiguous rows instead of attributing one series to another", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const reset = start + 60 * 60 * 1000; - - await observeQuotaResetNotifications({ - providers: [ - provider({ percentRemaining: 5, resetAtMs: reset, name: "First" }), - provider({ percentRemaining: 15, resetAtMs: reset, name: "Second" }), - ], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - expect(JSON.parse(await readFile(path, "utf8")).observations).toEqual({}); - }); - - it("aggregates providers that reset together", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const firstReset = start + 60 * 60 * 1000; - const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; - const initial = [ - provider({ percentRemaining: 5, resetAtMs: firstReset, providerId: "openai" }), - provider({ percentRemaining: 15, resetAtMs: firstReset, providerId: "anthropic" }), - ]; - await observeQuotaResetNotifications({ - providers: initial, - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - const notices = await observeQuotaResetNotifications({ - providers: [ - provider({ percentRemaining: 100, resetAtMs: nextReset, providerId: "openai" }), - provider({ percentRemaining: 100, resetAtMs: nextReset, providerId: "anthropic" }), - ], - windows: ["weekly"], - nowMs: firstReset + 60_000, - statePath: path, - }); - - expect(notices).toHaveLength(2); - expect(formatQuotaResetNotification(notices)).toBe( - "Quota reset: OpenAI, Anthropic are available again.", - ); - }); - - it("does not notify before the previous reset is crossed or when quota did not improve", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const firstReset = start + 60 * 60 * 1000; - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 25, resetAtMs: firstReset })], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - expect( - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 100, resetAtMs: firstReset + 604_800_000 })], - windows: ["weekly"], - nowMs: start + 30 * 60 * 1000, - statePath: path, - }), - ).toEqual([]); - }); - - it("limits structured reset observations to typed-window quota and rate-limit percentages", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const reset = start + 60 * 60 * 1000; - const accounting = { - resultType: "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", - } as const; - - await observeQuotaResetNotifications({ - providers: [ - { - providerId: "openai", - result: { - attempted: true, - errors: [], - entries: [ - { - kind: "quantity", - accounting: { ...accounting, resultType: "balance" }, - semantic: { - metric: { kind: "component", component: "current_balance" }, - prominence: "primary", - }, - name: "Balance", - quantity: { decimal: "10", unit: { kind: "currency", code: "USD" } }, - resetTimeIso: new Date(reset).toISOString(), - }, - { - kind: "boolean", - accounting: { ...accounting, resultType: "status" }, - semantic: { - metric: { kind: "component", component: "auto_reload" }, - prominence: "supplementary", - }, - name: "Auto-reload", - value: true, - resetTimeIso: new Date(reset).toISOString(), - }, - { - accounting: { ...accounting, resultType: "budget" }, - semantic: { metric: { kind: "window", window: "week" }, prominence: "primary" }, - name: "Budget", - percentRemaining: 10, - resetTimeIso: new Date(reset).toISOString(), - }, - { - accounting, - semantic: { metric: { kind: "aggregate" }, prominence: "primary" }, - name: "Aggregate quota", - label: "Weekly:", - percentRemaining: 10, - resetTimeIso: new Date(reset).toISOString(), - }, - { - accounting: { ...accounting, sourceId: "quota" }, - semantic: { metric: { kind: "window", window: "week" }, prominence: "primary" }, - name: "Typed quota", - label: "Monthly:", - percentRemaining: 10, - resetTimeIso: new Date(reset).toISOString(), - }, - { - accounting: { ...accounting, resultType: "rate_limit", sourceId: "rate" }, - semantic: { metric: { kind: "window", window: "week" }, prominence: "primary" }, - name: "Typed rate limit", - label: "Monthly:", - percentRemaining: 20, - resetTimeIso: new Date(reset).toISOString(), - }, - { - accounting: { ...accounting, sourceId: "monthly" }, - semantic: { metric: { kind: "window", window: "month" }, prominence: "primary" }, - name: "Typed monthly quota", - label: "Weekly:", - percentRemaining: 30, - resetTimeIso: new Date(reset).toISOString(), - }, - ], - }, - }, - ], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - expect(Object.keys(JSON.parse(await readFile(path, "utf8")).observations)).toHaveLength(2); - }); - - it("filters window and accounting types", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const reset = start + 60 * 60 * 1000; - - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 10, resetAtMs: reset, label: "Monthly" })], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - await observeQuotaResetNotifications({ - providers: [provider({ percentRemaining: 10, resetAtMs: reset, resultType: "usage" })], - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - expect(JSON.parse(await readFile(path, "utf8")).observations).toEqual({}); - }); - - it("keeps accounts independent without persisting source identifiers", async () => { - const path = await statePath(); - const start = Date.UTC(2026, 0, 1, 12); - const reset = start + 60 * 60 * 1000; - const sources = ["work@example.com", "personal@example.com"]; - - await observeQuotaResetNotifications({ - providers: sources.map((sourceId) => - provider({ percentRemaining: 5, resetAtMs: reset, sourceId }), - ), - windows: ["weekly"], - nowMs: start, - statePath: path, - }); - - const state = await readFile(path, "utf8"); - expect(Object.keys(JSON.parse(state).observations)).toHaveLength(2); - expect(state).not.toContain("work@example.com"); - expect(state).not.toContain("personal@example.com"); - }); -}); diff --git a/tests/lib.quota-toast-runtime.test.ts b/tests/lib.quota-toast-runtime.test.ts deleted file mode 100644 index 06c1f1bb..00000000 --- a/tests/lib.quota-toast-runtime.test.ts +++ /dev/null @@ -1,1168 +0,0 @@ -import { rm } from "fs/promises"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import type { DEFAULT_CONFIG } from "../src/lib/types.js"; -import { - createAlibabaAuthModuleMock, - createPluginTestClient as createClient, - createConfigModuleMock, - createPluginRuntimePathsMockModule, - createPluginTuiConfigInspection, - createPricingModuleMock, - createProvidersRegistryModuleMock, - createSessionTokensModuleMock, - getToastMessage, - makeQuotaToastTestConfig, - seedDefaultPluginBootstrapMocks, -} from "./helpers/plugin-test-harness.js"; - -const TEST_RUNTIME_ROOT = "/tmp/opencode-quota-toast-runtime-tests"; -const TEST_ACCOUNTING = { - resultType: "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", -} as const; -const ANNOUNCEMENT_TOAST_MESSAGE = - "Notice: Maintainer announcement available. Run /quota_announcements."; - -const TEST_ANNOUNCEMENT = vi.hoisted(() => ({ - id: "copilot-credits", - message: "Copilot billing is changing.", - url: "https://github.blog/example", - providerIds: ["copilot"], -})); - -const mocks = vi.hoisted(() => ({ - loadConfig: vi.fn(), - getProviders: vi.fn(), - maybeRefreshPricingSnapshot: vi.fn(), - getPricingSnapshotMeta: vi.fn(), - getPricingSnapshotSource: vi.fn(), - getRuntimePricingRefreshStatePath: vi.fn(), - getRuntimePricingSnapshotPath: vi.fn(), - setPricingSnapshotAutoRefresh: vi.fn(), - setPricingSnapshotSelection: vi.fn(), - resolveAlibabaCodingPlanAuthCached: vi.fn(), - fetchSessionTokensForDisplay: vi.fn(), - observeQuotaResetNotifications: vi.fn(), - formatQuotaResetNotification: vi.fn(), - getMaintainerAnnouncementsSummary: vi.fn(), - inspectTuiConfig: vi.fn(), -})); - -vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); -vi.mock("../src/providers/registry.js", () => - createProvidersRegistryModuleMock(mocks.getProviders), -); -vi.mock("../src/lib/modelsdev-pricing.js", () => createPricingModuleMock(mocks)); -vi.mock("../src/lib/session-tokens.js", () => - createSessionTokensModuleMock(mocks.fetchSessionTokensForDisplay), -); -vi.mock("../src/lib/alibaba-auth.js", () => - createAlibabaAuthModuleMock(mocks.resolveAlibabaCodingPlanAuthCached), -); -vi.mock("../src/lib/opencode-runtime-paths.js", () => - createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT, { includeCandidates: true }), -); -vi.mock("../src/lib/quota-reset-notifications.js", () => ({ - observeQuotaResetNotifications: mocks.observeQuotaResetNotifications, - formatQuotaResetNotification: mocks.formatQuotaResetNotification, -})); -vi.mock("../src/lib/tui-config-diagnostics.js", () => ({ - inspectTuiConfig: mocks.inspectTuiConfig, -})); -vi.mock("../src/lib/maintainer-announcements.js", () => ({ - BUNDLED_MAINTAINER_ANNOUNCEMENTS: [TEST_ANNOUNCEMENT], - formatMaintainerAnnouncementHomeCountLine: (activeCount: number) => - activeCount > 0 ? ANNOUNCEMENT_TOAST_MESSAGE : "", - getMaintainerAnnouncementsSummary: mocks.getMaintainerAnnouncementsSummary, -})); - -function makeAnnouncementSummary(overrides: Record = {}) { - return { - source: "bundled_only", - network: false, - bundledCount: 1, - activeCount: 1, - futureCount: 0, - expiredCount: 0, - activeAnnouncements: [ - { - announcement: TEST_ANNOUNCEMENT, - active: true, - reasons: [], - }, - ], - evaluations: [], - ...overrides, - }; -} - -function makeToastConfig(overrides: Partial = {}) { - return makeQuotaToastTestConfig({ - enabled: true, - enableToast: true, - enabledProviders: ["openai"], - showOnIdle: true, - showOnCompact: true, - showOnQuestion: true, - showSessionTokens: false, - minIntervalMs: 60_000, - maintainerAnnouncements: { enabled: false, home: false }, - ...overrides, - }); -} - -function makeProviderResult(name: string, percentRemaining: number) { - return { - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name, percentRemaining }], - errors: [], - }; -} - -async function createRuntime( - client: ReturnType, - overrides: { - isSubagentSession?: (sessionID: string) => Promise; - reconcileDetectedProviders?: (providerIds: readonly string[]) => Promise; - setSessionTokenError?: (error: unknown) => void; - showToast?: (body: Record) => Promise; - log?: (message: string, extra?: Record) => Promise; - roots?: () => { - workspaceRoot: string; - configRoot: string; - fallbackDirectory: string; - }; - } = {}, -) { - const { createQuotaToastRuntime } = await import("../src/lib/quota-toast-runtime.js"); - const reconcileDetectedProviders = - overrides.reconcileDetectedProviders ?? vi.fn().mockResolvedValue(undefined); - const setSessionTokenError = overrides.setSessionTokenError ?? vi.fn(); - const log = - overrides.log ?? - (async (message: string, extra?: Record) => { - await client.app.log({ body: { message, extra } }); - }); - const showToast = - overrides.showToast ?? - (async (body: Record) => client.tui.showToast({ body })); - - const runtime = createQuotaToastRuntime({ - client: client as never, - roots: - overrides.roots ?? - (() => ({ - workspaceRoot: process.cwd(), - configRoot: process.cwd(), - fallbackDirectory: process.cwd(), - })), - resolveSessionMeta: async (sessionID) => { - const response = await client.session.get({ path: { id: sessionID } }); - return { - modelID: response.data?.model?.id, - providerID: response.data?.model?.providerID, - }; - }, - isSubagentSession: - overrides.isSubagentSession ?? - (async (sessionID) => { - const response = await client.session.get({ path: { id: sessionID } }); - return Boolean(response.data?.parentID); - }), - reconcileDetectedProviders, - setSessionTokenError: setSessionTokenError as never, - showToast: showToast as never, - log, - onInitialized: vi.fn(), - }); - - return { runtime, reconcileDetectedProviders, setSessionTokenError }; -} - -async function flushFireAndForgetWork(): Promise { - for (let index = 0; index < 6; index += 1) await Promise.resolve(); -} - -describe("quota toast runtime state machine", () => { - beforeEach(async () => { - seedDefaultPluginBootstrapMocks(mocks, { - configOverrides: makeToastConfig(), - resetPluginState: true, - }); - mocks.observeQuotaResetNotifications.mockResolvedValue([]); - mocks.formatQuotaResetNotification.mockReturnValue(null); - mocks.getMaintainerAnnouncementsSummary.mockReturnValue(makeAnnouncementSummary()); - mocks.inspectTuiConfig.mockResolvedValue(createPluginTuiConfigInspection(TEST_RUNTIME_ROOT)); - await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); - const { __resetQuotaStateForTests } = await import("../src/lib/quota-state.js"); - __resetQuotaStateForTests(); - }); - - afterEach(async () => { - vi.useRealTimers(); - const { __resetQuotaStateForTests } = await import("../src/lib/quota-state.js"); - __resetQuotaStateForTests(); - await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); - }); - - it("loads configuration before applying per-trigger gates", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ showOnIdle: false, showOnCompact: true, showOnQuestion: false }), - ); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("OpenAI", 72)), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-gates", trigger: "session.idle" }); - expect(mocks.loadConfig).toHaveBeenCalledOnce(); - expect(provider.fetch).not.toHaveBeenCalled(); - expect(mocks.maybeRefreshPricingSnapshot).toHaveBeenCalledWith( - expect.objectContaining({ reason: "init" }), - ); - - await runtime.handleTrigger({ sessionID: "session-gates", trigger: "session.compacted" }); - expect(provider.fetch).toHaveBeenCalledOnce(); - expect(client.tui.showToast).toHaveBeenCalledOnce(); - }); - - it("owns current-model diagnostics and toast percentage formatting", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ enabledProviders: ["openai"], onlyCurrentModel: true }), - ); - const skippedProvider = { - id: "openai", - matchesCurrentModel: vi.fn().mockReturnValue(false), - isAvailable: vi.fn(), - fetch: vi.fn(), - }; - mocks.getProviders.mockReturnValue([skippedProvider]); - const skippedClient = createClient({ - modelID: "claude-3.7-sonnet", - providerID: "anthropic", - }); - const { runtime: skippedRuntime } = await createRuntime(skippedClient); - await skippedRuntime.handleTrigger({ - sessionID: "session-model-filter", - trigger: "session.idle", - }); - expect(skippedProvider.fetch).not.toHaveBeenCalled(); - expect(getToastMessage(skippedClient)).toContain( - "OpenAI: Skipped (current model: claude-3.7-sonnet)", - ); - - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ enabledProviders: ["copilot"], percentDisplayMode: "used" }), - ); - mocks.getProviders.mockReturnValue([ - { - id: "copilot", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Copilot", 81)), - }, - ]); - const formattedClient = createClient(); - const { runtime: formattedRuntime } = await createRuntime(formattedClient); - await formattedRuntime.handleTrigger({ - sessionID: "session-percent-display", - trigger: "session.idle", - }); - expect(getToastMessage(formattedClient)).toContain("19% used"); - expect(getToastMessage(formattedClient)).not.toContain("81% left"); - }); - - it("retries provider failures with provider-cache bypass and the complete saturated backoff", async () => { - vi.useFakeTimers(); - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig()); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockRejectedValue(new Error("warming up")), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-backoff", trigger: "session.idle" }); - expect(provider.fetch).toHaveBeenCalledTimes(1); - expect(getToastMessage(client)).toContain("OpenAI: Failed to read quota data"); - - for (const delay of [3_000, 15_000, 60_000, 300_000]) { - await vi.advanceTimersByTimeAsync(delay - 1); - const callsBeforeBoundary = provider.fetch.mock.calls.length; - await vi.advanceTimersByTimeAsync(1); - expect(provider.fetch).toHaveBeenCalledTimes(callsBeforeBoundary + 1); - } - - const scheduledDelays = client.app.log.mock.calls - .filter((call) => call[0]?.body?.message === "Deferred quota refresh scheduled") - .map((call) => call[0]?.body?.extra?.delayMs); - expect(scheduledDelays).toEqual([3_000, 15_000, 60_000, 300_000, 300_000]); - expect(client.tui.showToast).toHaveBeenCalledTimes(1); - }); - - it("retries provider-returned transient errors", async () => { - vi.useFakeTimers(); - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig()); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi - .fn() - .mockResolvedValueOnce({ - attempted: true, - entries: [], - errors: [ - { label: "OpenAI", message: "API error 503", retryable: true }, - { label: "Other", message: "API error 403" }, - ], - }) - .mockResolvedValueOnce(makeProviderResult("Recovered", 68)), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-transient-result", trigger: "session.idle" }); - expect(provider.fetch).toHaveBeenCalledOnce(); - - await vi.advanceTimersByTimeAsync(3_000); - expect(provider.fetch).toHaveBeenCalledTimes(2); - expect(getToastMessage(client, 1)).toContain("Recovered"); - }); - - it("retries suppressed provider failures and availability failures", async () => { - vi.useFakeTimers(); - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig({ showOnBothFail: false })); - const provider = { - id: "openai", - isAvailable: vi - .fn() - .mockRejectedValueOnce(new Error("auth unavailable")) - .mockResolvedValue(true), - fetch: vi - .fn() - .mockRejectedValueOnce(new Error("network unavailable")) - .mockResolvedValueOnce(makeProviderResult("Recovered", 63)), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-availability", trigger: "session.idle" }); - expect(provider.fetch).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(3_000); - expect(provider.fetch).toHaveBeenCalledOnce(); - expect(client.tui.showToast).toHaveBeenCalledOnce(); - expect(getToastMessage(client)).toContain("Unavailable (not detected)"); - - await vi.advanceTimersByTimeAsync(15_000); - expect(provider.fetch).toHaveBeenCalledTimes(2); - expect(client.tui.showToast).toHaveBeenCalledTimes(2); - expect(getToastMessage(client, 1)).toContain("63% left"); - }); - - it("preserves config_load_failed and no_reportable_data retry reasons", async () => { - vi.useFakeTimers(); - mocks.loadConfig.mockRejectedValue(new Error("config unavailable")); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ attempted: false, entries: [], errors: [] }), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-reasons", trigger: "session.idle" }); - expect(provider.fetch).not.toHaveBeenCalled(); - expect(client.app.log).toHaveBeenCalledWith({ - body: expect.objectContaining({ - message: "Deferred quota refresh scheduled", - extra: expect.objectContaining({ reason: "config_load_failed", delayMs: 3_000 }), - }), - }); - - mocks.loadConfig.mockReset(); - mocks.loadConfig.mockResolvedValue(makeToastConfig({ enabledProviders: "auto" })); - await vi.advanceTimersByTimeAsync(3_000); - expect(provider.fetch).toHaveBeenCalledOnce(); - expect(client.app.log).toHaveBeenCalledWith({ - body: expect.objectContaining({ - message: "Deferred quota refresh scheduled", - extra: expect.objectContaining({ reason: "no_reportable_data", delayMs: 15_000 }), - }), - }); - }); - - it("consumes a pending retry immediately and clears it when configuration becomes disabled", async () => { - vi.useFakeTimers(); - const loadedConfig = makeToastConfig(); - mocks.loadConfig.mockResolvedValueOnce(loadedConfig); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi - .fn() - .mockRejectedValueOnce(new Error("temporary failure")) - .mockResolvedValueOnce(makeProviderResult("Immediate", 66)) - .mockRejectedValue(new Error("replacement failure")), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-immediate", trigger: "session.idle" }); - await runtime.handleTrigger({ sessionID: "session-immediate", trigger: "session.compacted" }); - expect(provider.fetch).toHaveBeenCalledTimes(2); - expect(getToastMessage(client, 1)).toContain("Immediate"); - - await runtime.handleTrigger({ sessionID: "session-disabled", trigger: "session.idle" }); - expect(provider.fetch).toHaveBeenCalledTimes(3); - loadedConfig.enabled = false; - await runtime.handleTrigger({ sessionID: "session-disabled", trigger: "session.compacted" }); - await vi.advanceTimersByTimeAsync(300_000); - expect(provider.fetch).toHaveBeenCalledTimes(3); - }); - - it("suppresses duplicate lifecycle work while a deferred retry is in flight", async () => { - vi.useFakeTimers(); - let resolveRetry: ((value: unknown) => void) | undefined; - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig()); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi - .fn() - .mockRejectedValueOnce(new Error("warming up")) - .mockImplementationOnce( - () => - new Promise((resolve) => { - resolveRetry = resolve; - }), - ), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-in-flight", trigger: "session.idle" }); - await vi.advanceTimersByTimeAsync(3_000); - await vi.waitFor(() => expect(provider.fetch).toHaveBeenCalledTimes(2)); - await runtime.handleTrigger({ sessionID: "session-in-flight", trigger: "session.compacted" }); - expect(provider.fetch).toHaveBeenCalledTimes(2); - - resolveRetry?.(makeProviderResult("Recovered", 67)); - await vi.waitFor(() => expect(client.tui.showToast).toHaveBeenCalledTimes(2)); - }); - - it("does not let an older completion clear a replacement retry state's in-flight guard", async () => { - vi.useFakeTimers(); - let resolveOldToast: (() => void) | undefined; - let resolveNewRetry: ((value: unknown) => void) | undefined; - const loadedConfig = makeToastConfig(); - mocks.loadConfig.mockResolvedValueOnce(loadedConfig); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi - .fn() - .mockRejectedValueOnce(new Error("initial failure")) - .mockResolvedValueOnce(makeProviderResult("Old retry", 70)) - .mockRejectedValueOnce(new Error("replacement failure")) - .mockImplementationOnce( - () => - new Promise((resolve) => { - resolveNewRetry = resolve; - }), - ), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - client.tui.showToast.mockImplementation(({ body }: any) => { - if (body.message.includes("Old retry")) { - return new Promise((resolve) => { - resolveOldToast = () => resolve({}); - }); - } - return Promise.resolve({}); - }); - const { runtime } = await createRuntime(client); - const sessionID = "session-race-guard"; - - await runtime.handleTrigger({ sessionID, trigger: "session.idle" }); - await vi.advanceTimersByTimeAsync(3_000); - await vi.waitFor(() => expect(resolveOldToast).toBeTypeOf("function")); - - loadedConfig.enabled = false; - await runtime.handleTrigger({ sessionID, trigger: "session.compacted" }); - loadedConfig.enabled = true; - await runtime.handleTrigger({ sessionID, trigger: "session.idle" }); - expect(provider.fetch).toHaveBeenCalledTimes(3); - - await vi.advanceTimersByTimeAsync(3_000); - await vi.waitFor(() => expect(provider.fetch).toHaveBeenCalledTimes(4)); - resolveOldToast?.(); - await Promise.resolve(); - - await runtime.handleTrigger({ sessionID, trigger: "session.compacted" }); - expect(provider.fetch).toHaveBeenCalledTimes(4); - - resolveNewRetry?.(makeProviderResult("New retry", 75)); - await vi.waitFor(() => expect(getToastMessage(client, 3)).toContain("New retry")); - }); - - it("clears a consumed pending retry when the session becomes a subagent", async () => { - vi.useFakeTimers(); - let subagent = false; - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig()); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockRejectedValue(new Error("temporary failure")), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client, { - isSubagentSession: async () => subagent, - }); - - await runtime.handleTrigger({ sessionID: "session-subagent", trigger: "session.idle" }); - subagent = true; - await runtime.handleTrigger({ sessionID: "session-subagent", trigger: "session.compacted" }); - await vi.advanceTimersByTimeAsync(300_000); - expect(provider.fetch).toHaveBeenCalledOnce(); - }); - - it("keeps retry state local to each runtime instance", async () => { - vi.useFakeTimers(); - const configA = makeToastConfig(); - mocks.loadConfig.mockResolvedValueOnce(configA); - const providerA = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi - .fn() - .mockRejectedValueOnce(new Error("instance A warming up")) - .mockResolvedValueOnce(makeProviderResult("Instance A", 64)), - }; - mocks.getProviders.mockReturnValue([providerA]); - const clientA = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime: runtimeA } = await createRuntime(clientA); - await runtimeA.handleTrigger({ sessionID: "session-shared", trigger: "session.idle" }); - - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig()); - const providerB = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Instance B", 82)), - }; - mocks.getProviders.mockReturnValue([providerB]); - const clientB = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime: runtimeB } = await createRuntime(clientB); - await runtimeB.handleTrigger({ sessionID: "session-other", trigger: "session.idle" }); - - await vi.advanceTimersByTimeAsync(3_000); - expect(providerA.fetch).toHaveBeenCalledTimes(2); - expect(providerB.fetch).toHaveBeenCalledOnce(); - expect(getToastMessage(clientA, 1)).toContain("Instance A"); - expect(getToastMessage(clientB)).toContain("Instance B"); - }); - - it("bypasses rendered and provider caches for configured live-local providers", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ - enabledProviders: ["quota-providers"], - quotaProviders: [ - { - id: "local-project", - providerId: "local-project", - label: "Local project", - mode: "local-estimate", - windows: [{ id: "day", label: "Day", type: "utc-day", requestLimit: 10 }], - }, - ], - }), - ); - const provider = { - id: "quota-providers", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi - .fn() - .mockResolvedValueOnce(makeProviderResult("Local project", 90)) - .mockResolvedValueOnce(makeProviderResult("Local project", 80)), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "local-model", providerID: "local-project" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-local", trigger: "session.idle" }); - await runtime.handleTrigger({ sessionID: "session-local", trigger: "session.idle" }); - - expect(provider.fetch).toHaveBeenCalledTimes(2); - expect(getToastMessage(client, 0)).toContain("90% left"); - expect(getToastMessage(client, 1)).toContain("80% left"); - }); - - it("caches rendered quota rows but not error-only results", async () => { - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig({ enabledProviders: ["deepseek"] })); - const valueProvider = { - id: "deepseek", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - kind: "value", - accounting: TEST_ACCOUNTING, - name: "DeepSeek Balance", - value: "$12.34", - }, - ], - errors: [], - }), - }; - mocks.getProviders.mockReturnValue([valueProvider]); - const valueClient = createClient({ modelID: "deepseek-chat", providerID: "deepseek" }); - const { runtime: valueRuntime } = await createRuntime(valueClient); - await valueRuntime.handleTrigger({ sessionID: "session-value", trigger: "session.idle" }); - await valueRuntime.handleTrigger({ sessionID: "session-value", trigger: "session.idle" }); - expect(valueProvider.fetch).toHaveBeenCalledOnce(); - expect(getToastMessage(valueClient, 1)).toContain("$12.34"); - - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ enabledProviders: ["openrouter"], showOnBothFail: true }), - ); - const errorProvider = { - id: "openrouter", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [], - errors: [{ label: "DeepSeek", message: "Billing unavailable" }], - }), - }; - mocks.getProviders.mockReturnValue([errorProvider]); - const errorClient = createClient({ modelID: "openrouter/auto", providerID: "openrouter" }); - const { runtime: errorRuntime } = await createRuntime(errorClient); - await errorRuntime.handleTrigger({ sessionID: "session-errors", trigger: "session.idle" }); - await errorRuntime.handleTrigger({ sessionID: "session-errors", trigger: "session.idle" }); - expect(errorProvider.fetch).toHaveBeenCalledTimes(2); - expect(getToastMessage(errorClient, 1)).toContain("Billing unavailable"); - }); - - it("keys rendered throttling by session render context", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ enabledProviders: ["openai"], onlyCurrentModel: true }), - ); - const provider = { - id: "openai", - matchesCurrentModel: vi.fn().mockReturnValue(true), - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi - .fn() - .mockImplementation(async (context: any) => - makeProviderResult(context.config.currentModel ?? "unknown", 70), - ), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient(); - let currentModel = "openai/gpt-4"; - client.session.get.mockImplementation(async () => ({ - data: { model: { id: currentModel, providerID: "openai" } }, - })); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-model-key", trigger: "session.idle" }); - currentModel = "openai/gpt-5"; - await runtime.handleTrigger({ sessionID: "session-model-key", trigger: "session.idle" }); - - expect(provider.fetch).toHaveBeenCalledTimes(2); - expect(getToastMessage(client, 0)).toContain("openai/gpt-4"); - expect(getToastMessage(client, 1)).toContain("openai/gpt-5"); - expect(getToastMessage(client, 1)).not.toContain("openai/gpt-4"); - }); - - it("keeps collection and reconciliation ahead of the enableToast false late gate", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ - enableToast: false, - enabledProviders: "auto", - resetNotifications: { enabled: true, windows: ["weekly"] }, - }), - ); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("OpenAI", 71)), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const reconcileDetectedProviders = vi.fn().mockResolvedValue(undefined); - const { runtime } = await createRuntime(client, { reconcileDetectedProviders }); - - await runtime.handleTrigger({ sessionID: "session-late-gate", trigger: "session.idle" }); - - expect(mocks.loadConfig).toHaveBeenCalledOnce(); - expect(provider.isAvailable).toHaveBeenCalledOnce(); - expect(provider.fetch).toHaveBeenCalledOnce(); - expect(reconcileDetectedProviders).toHaveBeenCalledWith(["openai"]); - expect(mocks.observeQuotaResetNotifications).not.toHaveBeenCalled(); - expect(client.tui.showToast).not.toHaveBeenCalled(); - }); - - it("shares only rendered messages across runtimes without transferring metadata or reset text", async () => { - const sharedConfig = makeToastConfig({ - enabledProviders: "auto", - showSessionTokens: true, - minIntervalMs: 60_000, - resetNotifications: { enabled: true, windows: ["weekly"] }, - maintainerAnnouncements: { enabled: true, home: true }, - }); - mocks.loadConfig.mockResolvedValueOnce(sharedConfig); - const tokenErrorA = { sessionID: "session-shared-cache", error: "instance A token error" }; - mocks.fetchSessionTokensForDisplay.mockResolvedValueOnce({ - sessionTokens: undefined, - error: tokenErrorA, - }); - mocks.observeQuotaResetNotifications.mockResolvedValueOnce([ - { providerId: "copilot", entryName: "Copilot", window: "weekly" }, - ]); - mocks.formatQuotaResetNotification.mockReturnValueOnce("Copilot weekly quota reset"); - mocks.getMaintainerAnnouncementsSummary.mockImplementation((params: any) => - params.enabledProviders.includes("copilot") - ? makeAnnouncementSummary() - : makeAnnouncementSummary({ activeCount: 0, futureCount: 1, activeAnnouncements: [] }), - ); - const providerA = { - id: "copilot", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Copilot A", 81)), - }; - mocks.getProviders.mockReturnValue([providerA]); - const clientA = createClient(); - const reconcileA = vi.fn().mockResolvedValue(undefined); - const setTokenErrorA = vi.fn(); - const { runtime: runtimeA } = await createRuntime(clientA, { - reconcileDetectedProviders: reconcileA, - setSessionTokenError: setTokenErrorA, - }); - await runtimeA.handleTrigger({ sessionID: "session-shared-cache", trigger: "session.idle" }); - await flushFireAndForgetWork(); - - expect(providerA.fetch).toHaveBeenCalledOnce(); - expect(reconcileA).toHaveBeenCalledWith(["copilot"]); - expect(setTokenErrorA).toHaveBeenCalledWith(tokenErrorA); - expect(clientA.tui.showToast).toHaveBeenCalledTimes(3); - expect(getToastMessage(clientA, 0)).toContain("Copilot A"); - expect(getToastMessage(clientA, 1)).toBe(ANNOUNCEMENT_TOAST_MESSAGE); - expect(clientA.tui.showToast.mock.calls[2]?.[0]?.body?.variant).toBe("success"); - - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig({ ...sharedConfig })); - const providerB = { - id: "copilot", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Copilot B", 74)), - }; - mocks.getProviders.mockReturnValue([providerB]); - const clientB = createClient(); - const reconcileB = vi.fn().mockResolvedValue(undefined); - const setTokenErrorB = vi.fn(); - const { runtime: runtimeB } = await createRuntime(clientB, { - reconcileDetectedProviders: reconcileB, - setSessionTokenError: setTokenErrorB, - }); - await runtimeB.handleTrigger({ sessionID: "session-shared-cache", trigger: "session.idle" }); - await flushFireAndForgetWork(); - - expect(providerB.fetch).not.toHaveBeenCalled(); - expect(reconcileB).not.toHaveBeenCalled(); - expect(setTokenErrorB).not.toHaveBeenCalled(); - expect(getToastMessage(clientB)).toContain("Copilot A"); - expect(clientB.tui.showToast).toHaveBeenCalledOnce(); - expect(mocks.getMaintainerAnnouncementsSummary).toHaveBeenLastCalledWith( - expect.objectContaining({ enabledProviders: [] }), - ); - expect(mocks.observeQuotaResetNotifications).toHaveBeenCalledOnce(); - }); - - it("does not reuse a rendered message across different provider configuration", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ - enabledProviders: ["quota-providers"], - quotaProviders: [ - { id: "first", mode: "remote-api", url: "https://first.example", format: "quota-v1" }, - ], - }), - ); - const providerA = { - id: "quota-providers", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("First config", 81)), - }; - mocks.getProviders.mockReturnValue([providerA]); - const clientA = createClient(); - const { runtime: runtimeA } = await createRuntime(clientA); - await runtimeA.handleTrigger({ sessionID: "session-config-cache", trigger: "session.idle" }); - - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ - enabledProviders: ["quota-providers"], - quotaProviders: [ - { id: "second", mode: "remote-api", url: "https://second.example", format: "quota-v1" }, - ], - }), - ); - const providerB = { - id: "quota-providers", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Second config", 74)), - }; - mocks.getProviders.mockReturnValue([providerB]); - const clientB = createClient(); - const { runtime: runtimeB } = await createRuntime(clientB); - await runtimeB.handleTrigger({ sessionID: "session-config-cache", trigger: "session.idle" }); - - expect(providerA.fetch).toHaveBeenCalledOnce(); - expect(providerB.fetch).toHaveBeenCalledOnce(); - expect(getToastMessage(clientB)).toContain("Second config"); - expect(getToastMessage(clientB)).not.toContain("First config"); - }); - - it("separates rendered-message cache entries for each display option", async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-01-15T10:00:00.000Z")); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: { - ...TEST_ACCOUNTING, - observedAtIso: "2026-01-15T10:00:00.000Z", - }, - name: "OpenAI Weekly", - percentRemaining: 81, - resetTimeIso: "2026-01-17T15:14:00.000Z", - fixedWindow: { - kind: "fixed_window", - startedAtIso: "2026-01-15T09:00:00.000Z", - observedAtIso: "2026-01-15T10:00:00.000Z", - endsAtIso: "2026-01-17T15:14:00.000Z", - fullReset: true, - }, - }, - ], - errors: [], - }), - }; - mocks.getProviders.mockReturnValue([provider]); - - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig()); - const defaultClient = createClient(); - const { runtime: defaultRuntime } = await createRuntime(defaultClient); - await defaultRuntime.handleTrigger({ - sessionID: "session-display-cache", - trigger: "session.idle", - }); - expect(getToastMessage(defaultClient)).toContain("81% left"); - expect(getToastMessage(defaultClient)).toContain("2d 5h 14m"); - - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig({ resetTimeSpaced: false })); - const denseClient = createClient(); - const { runtime: denseRuntime } = await createRuntime(denseClient); - await denseRuntime.handleTrigger({ - sessionID: "session-display-cache", - trigger: "session.idle", - }); - expect(getToastMessage(denseClient)).toContain("81% left"); - expect(getToastMessage(denseClient)).toContain("2d5h14m"); - - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ percentDisplayMode: "used", percentLabelStyle: "bare" }), - ); - const bareClient = createClient(); - const { runtime: bareRuntime } = await createRuntime(bareClient); - await bareRuntime.handleTrigger({ - sessionID: "session-display-cache", - trigger: "session.idle", - }); - expect(getToastMessage(bareClient)).toContain("19%"); - expect(getToastMessage(bareClient)).not.toContain("19% used"); - expect(bareClient.tui.showToast).toHaveBeenCalledWith({ - body: expect.objectContaining({ title: "Quota [Used]" }), - }); - - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig({ quotaProjection: "runway" })); - const runwayClient = createClient(); - const { runtime: runwayRuntime } = await createRuntime(runwayClient); - await runwayRuntime.handleTrigger({ - sessionID: "session-display-cache", - trigger: "session.idle", - }); - expect(getToastMessage(runwayClient)).toContain("Runs out"); - expect(getToastMessage(defaultClient)).not.toContain("Runs out"); - - expect(provider.fetch).toHaveBeenCalledTimes(4); - }); - - it("emits reset text only from a fresh collection", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ resetNotifications: { enabled: true, windows: ["weekly"] } }), - ); - mocks.observeQuotaResetNotifications.mockResolvedValue([ - { providerId: "openai", entryName: "OpenAI", window: "weekly" }, - ]); - mocks.formatQuotaResetNotification.mockReturnValue("OpenAI weekly quota reset"); - const provider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("OpenAI", 77)), - }; - mocks.getProviders.mockReturnValue([provider]); - const client = createClient({ modelID: "openai/gpt-5", providerID: "openai" }); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-reset-cache", trigger: "session.idle" }); - await runtime.handleTrigger({ sessionID: "session-reset-cache", trigger: "session.idle" }); - - expect(provider.fetch).toHaveBeenCalledOnce(); - expect(mocks.observeQuotaResetNotifications).toHaveBeenCalledOnce(); - const successToasts = client.tui.showToast.mock.calls.filter( - (call) => call[0]?.body?.variant === "success", - ); - expect(successToasts).toHaveLength(1); - }); - - it("keeps announcement state runtime-local and one-shot", async () => { - const config = makeToastConfig({ - enabledProviders: ["copilot"], - maintainerAnnouncements: { enabled: true, home: true }, - minIntervalMs: 0, - }); - const provider = { - id: "copilot", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Copilot", 81)), - }; - - mocks.loadConfig.mockResolvedValueOnce(config); - mocks.getProviders.mockReturnValue([provider]); - const clientA = createClient(); - const { runtime: runtimeA } = await createRuntime(clientA); - await runtimeA.handleTrigger({ sessionID: "session-a-1", trigger: "question" }); - await runtimeA.handleTrigger({ sessionID: "session-a-2", trigger: "question" }); - await flushFireAndForgetWork(); - expect( - clientA.tui.showToast.mock.calls.filter( - (call) => call[0]?.body?.message === ANNOUNCEMENT_TOAST_MESSAGE, - ), - ).toHaveLength(1); - - mocks.loadConfig.mockResolvedValueOnce(makeToastConfig({ ...config })); - mocks.getProviders.mockReturnValue([provider]); - const clientB = createClient(); - const { runtime: runtimeB } = await createRuntime(clientB); - await runtimeB.handleTrigger({ sessionID: "session-b", trigger: "question" }); - await flushFireAndForgetWork(); - expect( - clientB.tui.showToast.mock.calls.filter( - (call) => call[0]?.body?.message === ANNOUNCEMENT_TOAST_MESSAGE, - ), - ).toHaveLength(1); - }); - - it("suppresses announcement fallback when TUI owns the surface", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ - enabledProviders: ["copilot"], - maintainerAnnouncements: { enabled: true, home: true }, - minIntervalMs: 0, - }), - ); - mocks.getProviders.mockReturnValue([ - { - id: "copilot", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Copilot", 81)), - }, - ]); - mocks.inspectTuiConfig.mockResolvedValue( - createPluginTuiConfigInspection(TEST_RUNTIME_ROOT, { - configured: true, - quotaPluginConfigured: true, - }), - ); - const client = createClient(); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-tui-1", trigger: "question" }); - await runtime.handleTrigger({ sessionID: "session-tui-2", trigger: "question" }); - await flushFireAndForgetWork(); - - expect(client.tui.showToast).toHaveBeenCalledTimes(2); - expect(mocks.inspectTuiConfig).toHaveBeenCalledOnce(); - }); - - it("keeps future announcements pending until a later visible toast", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ - enabledProviders: ["copilot"], - maintainerAnnouncements: { enabled: true, home: true }, - minIntervalMs: 60_000, - }), - ); - const provider = { - id: "copilot", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Copilot", 81)), - }; - mocks.getProviders.mockReturnValue([provider]); - mocks.getMaintainerAnnouncementsSummary - .mockReturnValueOnce( - makeAnnouncementSummary({ activeCount: 0, futureCount: 1, activeAnnouncements: [] }), - ) - .mockReturnValue(makeAnnouncementSummary()); - const client = createClient(); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-future", trigger: "question" }); - await flushFireAndForgetWork(); - await runtime.handleTrigger({ sessionID: "session-future", trigger: "question" }); - await flushFireAndForgetWork(); - - expect(provider.fetch).toHaveBeenCalledOnce(); - expect(client.tui.showToast).toHaveBeenCalledTimes(3); - expect(getToastMessage(client, 2)).toBe(ANNOUNCEMENT_TOAST_MESSAGE); - expect(mocks.getMaintainerAnnouncementsSummary).toHaveBeenLastCalledWith( - expect.objectContaining({ enabledProviders: ["copilot"] }), - ); - }); - - it("retries announcement fallback after TUI detection or fallback display fails", async () => { - const config = makeToastConfig({ - enabledProviders: ["copilot"], - maintainerAnnouncements: { enabled: true, home: true }, - minIntervalMs: 0, - }); - mocks.loadConfig.mockResolvedValueOnce(config); - const provider = { - id: "copilot", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Copilot", 81)), - }; - mocks.getProviders.mockReturnValue([provider]); - mocks.inspectTuiConfig.mockRejectedValueOnce(new Error("diagnostics failed")); - const client = createClient(); - client.tui.showToast - .mockResolvedValueOnce({}) - .mockResolvedValueOnce({}) - .mockRejectedValueOnce(new Error("fallback failed")) - .mockResolvedValueOnce({}) - .mockResolvedValueOnce({}); - const { runtime } = await createRuntime(client); - - await runtime.handleTrigger({ sessionID: "session-failure-1", trigger: "question" }); - await flushFireAndForgetWork(); - await runtime.handleTrigger({ sessionID: "session-failure-2", trigger: "question" }); - await flushFireAndForgetWork(); - await runtime.handleTrigger({ sessionID: "session-failure-3", trigger: "question" }); - await flushFireAndForgetWork(); - - expect(mocks.inspectTuiConfig).toHaveBeenCalledTimes(3); - expect( - client.tui.showToast.mock.calls.filter( - (call) => call[0]?.body?.message === ANNOUNCEMENT_TOAST_MESSAGE, - ), - ).toHaveLength(2); - }); - - it("preserves exact primary, announcement-launch, primary-log, reset, and reset-log sequencing", async () => { - mocks.loadConfig.mockResolvedValueOnce( - makeToastConfig({ - enabledProviders: ["copilot"], - resetNotifications: { enabled: true, windows: ["weekly"] }, - maintainerAnnouncements: { enabled: true, home: true }, - }), - ); - mocks.observeQuotaResetNotifications.mockResolvedValue([ - { providerId: "copilot", entryName: "Copilot", window: "weekly" }, - ]); - mocks.formatQuotaResetNotification.mockReturnValue("Copilot weekly quota reset"); - const provider = { - id: "copilot", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue(makeProviderResult("Copilot", 81)), - }; - mocks.getProviders.mockReturnValue([provider]); - - let resolvePrimary!: () => void; - let resolvePrimaryLog!: () => void; - let resolveReset!: () => void; - let resolveAnnouncementDiagnostics!: () => void; - const primary = new Promise((resolve) => { - resolvePrimary = () => resolve({}); - }); - const primaryLog = new Promise((resolve) => { - resolvePrimaryLog = resolve; - }); - const reset = new Promise((resolve) => { - resolveReset = () => resolve({}); - }); - const announcementDiagnostics = new Promise((resolve) => { - resolveAnnouncementDiagnostics = () => - resolve(createPluginTuiConfigInspection(TEST_RUNTIME_ROOT)); - }); - mocks.inspectTuiConfig.mockReturnValueOnce(announcementDiagnostics); - - const client = createClient(); - const showToast = vi.fn(async (body: Record) => { - if (body.variant === "success") return reset; - if (body.message === ANNOUNCEMENT_TOAST_MESSAGE) return {}; - return primary; - }); - const log = vi.fn(async (message: string) => { - if (message === "Displayed quota toast") await primaryLog; - }); - const { runtime } = await createRuntime(client, { showToast, log }); - - const trigger = runtime.handleTrigger({ sessionID: "session-order", trigger: "question" }); - await vi.waitFor(() => expect(showToast).toHaveBeenCalledTimes(1)); - expect(mocks.inspectTuiConfig).not.toHaveBeenCalled(); - expect(log).not.toHaveBeenCalledWith("Displayed quota toast", expect.anything()); - - resolvePrimary(); - await vi.waitFor(() => { - expect(mocks.inspectTuiConfig).toHaveBeenCalledOnce(); - expect(log).toHaveBeenCalledWith( - "Displayed quota toast", - expect.objectContaining({ trigger: "question" }), - ); - }); - expect(showToast).toHaveBeenCalledTimes(1); - - resolvePrimaryLog(); - await vi.waitFor(() => expect(showToast).toHaveBeenCalledTimes(2)); - expect(showToast).toHaveBeenLastCalledWith(expect.objectContaining({ variant: "success" })); - expect(log).not.toHaveBeenCalledWith("Displayed quota reset notification", expect.anything()); - - resolveReset(); - await trigger; - expect(log).toHaveBeenCalledWith( - "Displayed quota reset notification", - expect.objectContaining({ trigger: "question" }), - ); - - resolveAnnouncementDiagnostics(); - await flushFireAndForgetWork(); - }); -}); diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 8181ddf9..1e920257 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -51,11 +51,6 @@ const tuiDiagnosticsMocks = vi.hoisted(() => ({ inspectTuiConfig: vi.fn(), })); -const resetMocks = vi.hoisted(() => ({ - observeQuotaResetNotifications: vi.fn(), - formatQuotaResetNotification: vi.fn(), -})); - vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); vi.mock("../src/providers/registry.js", () => createProvidersRegistryModuleMock(mocks.getProviders), @@ -79,10 +74,6 @@ vi.mock("../src/lib/maintainer-announcements.js", () => ({ }, getMaintainerAnnouncementsSummary: announcementMocks.getMaintainerAnnouncementsSummary, })); -vi.mock("../src/lib/quota-reset-notifications.js", () => ({ - observeQuotaResetNotifications: resetMocks.observeQuotaResetNotifications, - formatQuotaResetNotification: resetMocks.formatQuotaResetNotification, -})); function makeAnnouncementSummary(overrides: Record = {}) { return { @@ -210,8 +201,6 @@ describe("maintainer announcement plugin integration", () => { tuiDiagnosticsMocks.inspectTuiConfig.mockResolvedValue( createPluginTuiConfigInspection(TEST_RUNTIME_ROOT), ); - resetMocks.observeQuotaResetNotifications.mockResolvedValue([]); - resetMocks.formatQuotaResetNotification.mockReturnValue(null); await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); diff --git a/tests/plugin.quota-command.test.ts b/tests/plugin.quota-command.test.ts index 9bc2bcfa..8095cd7c 100644 --- a/tests/plugin.quota-command.test.ts +++ b/tests/plugin.quota-command.test.ts @@ -97,8 +97,6 @@ const mocks = vi.hoisted(() => ({ resolveAlibabaCodingPlanAuthCached: vi.fn(), fetchSessionTokensForDisplay: vi.fn(), reconcileDetectedProvidersInGlobalConfig: vi.fn(), - observeQuotaResetNotifications: vi.fn(), - formatQuotaResetNotification: vi.fn(), disposeQuotaTelemetryOwner: vi.fn(), })); @@ -126,11 +124,6 @@ vi.mock("../src/lib/opencode-config-providers.js", () => ({ reconcileDetectedProvidersInGlobalConfig: mocks.reconcileDetectedProvidersInGlobalConfig, })); -vi.mock("../src/lib/quota-reset-notifications.js", () => ({ - observeQuotaResetNotifications: mocks.observeQuotaResetNotifications, - formatQuotaResetNotification: mocks.formatQuotaResetNotification, -})); - vi.mock("../src/lib/quota-telemetry.js", async (importOriginal) => ({ ...(await importOriginal()), disposeQuotaTelemetryOwner: mocks.disposeQuotaTelemetryOwner, @@ -158,8 +151,6 @@ describe("/quota command behavior", () => { addedProviderIds: [], changed: false, }); - mocks.observeQuotaResetNotifications.mockResolvedValue([]); - mocks.formatQuotaResetNotification.mockReturnValue(null); await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); const { __resetQuotaStateForTests } = await import("../src/lib/quota-state.js"); __resetQuotaStateForTests(); diff --git a/tests/tui-panel-state.test.ts b/tests/tui-panel-state.test.ts deleted file mode 100644 index 63a4091c..00000000 --- a/tests/tui-panel-state.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - type CompactStatusState, - getCompactStatusText, - getSidebarPanelLines, - type HomeBottomState, - type SidebarPanelState, - shouldRenderCompactStatus, - shouldRenderHomeBottom, - shouldRenderSidebarPanel, -} from "../src/lib/tui-panel-state.js"; - -describe("tui panel state helpers", () => { - it("shows a loading placeholder before the first sidebar load resolves", () => { - const panel: SidebarPanelState = { - status: "loading", - lines: [], - }; - - expect(shouldRenderSidebarPanel(panel)).toBe(true); - expect(getSidebarPanelLines(panel)).toEqual(["Loading…"]); - }); - - it("shows an unavailable placeholder after a ready load with no rows", () => { - const panel: SidebarPanelState = { - status: "ready", - lines: [], - }; - - expect(shouldRenderSidebarPanel(panel)).toBe(true); - expect(getSidebarPanelLines(panel)).toEqual(["Unavailable"]); - }); - - it("hides the sidebar panel completely when quota is disabled", () => { - const panel: SidebarPanelState = { - status: "disabled", - lines: [], - }; - - expect(shouldRenderSidebarPanel(panel)).toBe(false); - expect(getSidebarPanelLines(panel)).toEqual([]); - }); - - it("shows a quota loading compact placeholder before compact status resolves", () => { - const panel: CompactStatusState = { - status: "loading", - }; - - expect(shouldRenderCompactStatus(panel)).toBe(true); - expect(getCompactStatusText(panel)).toBe("Quota loading…"); - }); - - it("uses a non-empty loading compact text override", () => { - const panel: CompactStatusState = { - status: "loading", - text: "Waiting for current model", - }; - - expect(shouldRenderCompactStatus(panel)).toBe(true); - expect(getCompactStatusText(panel)).toBe("Waiting for current model"); - }); - - it("renders nothing for ready compact status with empty text", () => { - const panel: CompactStatusState = { - status: "ready", - text: "", - }; - - expect(shouldRenderCompactStatus(panel)).toBe(true); - expect(getCompactStatusText(panel)).toBe(""); - }); - - it("returns compact status text as one sanitized line", () => { - const panel: CompactStatusState = { - status: "ready", - text: "Copilot\u001b[31m\n 82%", - }; - - expect(shouldRenderCompactStatus(panel)).toBe(true); - expect(getCompactStatusText(panel)).toBe("Copilot 82%"); - }); - - it("hides compact status completely when quota is disabled", () => { - const panel: CompactStatusState = { - status: "disabled", - text: "Copilot 82%", - }; - - expect(shouldRenderCompactStatus(panel)).toBe(false); - expect(getCompactStatusText(panel)).toBe(""); - }); - - it("hides home bottom when neither announcements nor compact quota are visible", () => { - const panel: HomeBottomState = { - status: "loading", - compact: { status: "disabled" }, - }; - - expect(shouldRenderHomeBottom(panel)).toBe(false); - }); - - it("shows home bottom while enabled compact quota is loading", () => { - const panel: HomeBottomState = { - status: "loading", - compact: { status: "loading" }, - }; - - expect(shouldRenderHomeBottom(panel)).toBe(true); - }); - - it("shows home bottom for an announcement when compact quota is disabled", () => { - const panel: HomeBottomState = { - status: "ready", - announcementText: "Notice available", - compact: { status: "disabled" }, - }; - - expect(shouldRenderHomeBottom(panel)).toBe(true); - }); -}); From 4607d56652ff613b5c7f088d4b0ff2f7f91710af Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:39:50 +0200 Subject: [PATCH 29/97] fix(tui): resolve the session model and slash arguments on OpenCode 2 - Read the session model from the TUI sync store (data.session.get), which OpenCode 2 keeps current via session.model.selected. The old client lookup read response.data.model, but the OpenCode 2 promise client already unwraps .data, so onlyCurrentModel never saw a model. - Mark argument-taking commands with slash.arguments so OpenCode 2 keeps the typed text and passes it to run(input: string). Accept that string directly; the dialog prompt still opens when no arguments were typed. --- src/tui-v2.tsx | 43 +++--- tests/plugin.command-handled-boundary.test.ts | 10 +- tests/plugin.maintainer-announcements.test.ts | 2 +- tests/plugin.question-hook.test.ts | 10 +- tests/tui-v2-commands.test.ts | 129 ++++++++++++++---- 5 files changed, 132 insertions(+), 62 deletions(-) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 18a46058..7649869a 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -42,10 +42,12 @@ type Toast = { duration?: number; }; type TuiContext = { - client: unknown; location?: { directory: string }; data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void; + session: { + get: (sessionID: string) => { model?: { id: string; providerID: string } } | undefined; + }; location?: { default: () => { directory: string }; provider: { list: (location: { directory: string }) => Array<{ id: string }> | undefined }; @@ -60,8 +62,8 @@ type TuiContext = { title: string; group: string; palette: true; - slash: { name: string }; - run: (input?: unknown) => Promise; + slash: { name: string; arguments?: true }; + run: (input?: string) => Promise; }>; }, ) => void; @@ -104,15 +106,10 @@ function quotaClient(context: TuiContext) { } async function getSessionModelMeta( - client: unknown, + context: TuiContext, sessionID: string, ): Promise { - const session = ( - client as { session?: { get?: (input: { sessionID: string }) => Promise<{ data?: unknown }> } } - ).session; - const response = await session?.get?.({ sessionID }); - const model = (response?.data as { model?: { id?: string; providerID?: string } } | undefined) - ?.model; + const model = context.data.session.get(sessionID)?.model; return model ? { modelID: model.id, providerID: model.providerID } : {}; } @@ -133,7 +130,7 @@ async function getQuotaMessage( client: quotaClient(context), roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, sessionID, - resolveSessionMeta: (id) => getSessionModelMeta(context.client, id), + resolveSessionMeta: (id) => getSessionModelMeta(context, id), includeSessionMeta: (config) => config.onlyCurrentModel, }); const config = runtime.config; @@ -219,7 +216,7 @@ async function getQuotaFooter( client: quotaClient(context), roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, sessionID, - resolveSessionMeta: (id) => getSessionModelMeta(context.client, id), + resolveSessionMeta: (id) => getSessionModelMeta(context, id), includeSessionMeta: (config) => config.onlyCurrentModel && surface === "prompt", }); const config = runtime.config; @@ -303,24 +300,14 @@ function reportFailure(error: unknown): void { console.warn(`[opencode-quota] failed to load quota: ${message}`); } -function getCommandArguments(input: unknown): string | undefined { - if (!input || typeof input !== "object") return undefined; - const record = input as Record; - for (const key of ["arguments", "args", "query"] as const) { - const value = record[key]; - if (typeof value === "string" && value.trim()) return value.trim(); - } - return undefined; -} - async function runQuotaCommand( context: TuiContext, command: QuotaDialogCommandId, sessionID: string | undefined, - input?: unknown, + input?: string, ): Promise { const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.id === command)!; - let argumentsText = getCommandArguments(input); + let argumentsText = input?.trim() || undefined; if (spec.acceptsArguments && argumentsText === undefined) { const value = await context.ui.dialog.prompt({ title: spec.title, @@ -337,7 +324,7 @@ async function runQuotaCommand( client: quotaClient(context), roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, sessionID, - resolveSessionMeta: (id) => getSessionModelMeta(context.client, id), + resolveSessionMeta: (id) => getSessionModelMeta(context, id), }); if (result.state === "noop") return; const alert = context.ui.dialog.alert({ title: result.title, message: result.output }); @@ -360,8 +347,10 @@ function registerQuotaCommands(context: TuiContext, getSessionID: () => string | title: spec.title, group: "OpenCode Quota", palette: true, - slash: { name: spec.slashName }, - run: (input?: unknown) => runQuotaCommand(context, spec.id, getSessionID(), input), + slash: spec.acceptsArguments + ? { name: spec.slashName, arguments: true as const } + : { name: spec.slashName }, + run: (input?: string) => runQuotaCommand(context, spec.id, getSessionID(), input), })), })); } diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 6953aa17..880aba9b 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -12,7 +12,7 @@ vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ function startTui() { const commands = new Map< string, - { run: (input?: unknown) => Promise; slash: { name: string } } + { run: (input?: string) => Promise; slash: { name: string } } >(); const listeners = new Map }) => void>(); const alert = vi.fn().mockResolvedValue(undefined); @@ -21,8 +21,8 @@ function startTui() { const slots = new Map unknown }>(); const context = { location: { directory: process.cwd() }, - client: { session: { get: vi.fn() } }, data: { + session: { get: vi.fn() }, on: vi.fn((name: string, callback: (event: { data?: Record }) => void) => { listeners.set(name, callback); return () => listeners.delete(name); @@ -34,7 +34,7 @@ function startTui() { build: () => { commands: Array<{ id: string; - run: (input?: unknown) => Promise; + run: (input?: string) => Promise; slash: { name: string }; }>; }, @@ -91,13 +91,13 @@ describe("V2 CLI command boundary", () => { }), ); expect(tui.alert).toHaveBeenCalledWith({ title: "Quota", message: "Quota ready" }); - expect(tui.context.client.session.get).not.toHaveBeenCalled(); + expect(tui.context.data.session.get).not.toHaveBeenCalled(); tui.dispose?.(); }); it("passes explicit /tokens_between arguments to the deterministic output builder", async () => { const tui = startTui(); - await tui.commands.get("quota.tokens_between")?.run({ arguments: "not-a-date-range" }); + await tui.commands.get("quota.tokens_between")?.run("not-a-date-range"); expect(mocks.build).toHaveBeenCalledWith( expect.objectContaining({ command: "tokens_between", diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 1e920257..b69561b9 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -132,9 +132,9 @@ async function startCli() { const listeners = new Map }) => void>(); const toast = vi.fn(); const context = { - client: { session: { get: vi.fn().mockResolvedValue({ data: {} }) } }, location: { directory: process.cwd() }, data: { + session: { get: vi.fn(() => undefined) }, on: vi.fn((name: string, listener: (event: { data: Record }) => void) => { listeners.set(name, listener); return () => listeners.delete(name); diff --git a/tests/plugin.question-hook.test.ts b/tests/plugin.question-hook.test.ts index 0b866e33..8106dd6c 100644 --- a/tests/plugin.question-hook.test.ts +++ b/tests/plugin.question-hook.test.ts @@ -10,19 +10,19 @@ import plugin from "../src/tui-v2.tsx"; describe("V2 CLI question-tool accounting boundary", () => { const handlers = new Map }) => void>(); - const client = { session: { get: vi.fn() } }; + const session = { get: vi.fn() }; const toast = vi.fn(); beforeEach(() => { handlers.clear(); - client.session.get.mockReset(); + session.get.mockReset(); toast.mockReset(); resolveQuotaRuntimeContext.mockReset().mockResolvedValue({ config: { enabled: true, enableToast: true, showOnQuestion: false }, }); plugin.setup({ - client, data: { + session, on: (name: string, handler: (event: { data: Record }) => void) => { handlers.set(name, handler); return () => handlers.delete(name); @@ -43,7 +43,7 @@ describe("V2 CLI question-tool accounting boundary", () => { handlers.get("session.tool.input.started")?.({ data: { name: "question", id: "call-1" } }); handlers.get("session.tool.success")?.({ data: { sessionID: "session-1", id: "call-1" } }); await vi.waitFor(() => expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1)); - expect(client.session.get).not.toHaveBeenCalled(); + expect(session.get).not.toHaveBeenCalled(); expect(toast).not.toHaveBeenCalled(); }); @@ -51,7 +51,7 @@ describe("V2 CLI question-tool accounting boundary", () => { handlers.get("session.tool.input.started")?.({ data: { name: "question", id: "call-2" } }); handlers.get("session.tool.failed")?.({ data: { sessionID: "session-1", id: "call-2" } }); expect(resolveQuotaRuntimeContext).not.toHaveBeenCalled(); - expect(client.session.get).not.toHaveBeenCalled(); + expect(session.get).not.toHaveBeenCalled(); expect(toast).not.toHaveBeenCalled(); }); }); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index d551a31a..80fb2eff 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -1,33 +1,71 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const build = vi.hoisted(() => vi.fn()); +vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ + ...(await importOriginal()), + buildQuotaDialogCommandOutput: build, +})); import plugin from "../src/tui-v2.tsx"; -describe("V2 quota TUI commands", () => { - it("registers every quota command as a local slash command", () => { - let layer: - | { commands: Array<{ slash: { name: string }; palette: boolean; run: () => void }> } - | undefined; - const context = { - client: {}, - data: { on: vi.fn(() => vi.fn()) }, - keymap: { - layer: vi.fn((build) => { - layer = build(); - }), +type RegisteredCommand = { + id: string; + slash: { name: string; arguments?: true }; + palette: boolean; + run: (input?: string) => Promise; +}; + +function startTui() { + let layer: { commands: RegisteredCommand[] } | undefined; + const context = { + data: { + on: vi.fn(() => vi.fn()), + session: { + get: vi.fn((sessionID: string) => + sessionID === "ses_known" + ? { model: { id: "claude-sonnet-4-5", providerID: "anthropic" } } + : undefined, + ), }, - ui: { - slot: vi.fn((claim) => { - if (claim.append === "app") claim.render(); - return vi.fn(); - }), - toast: { show: vi.fn() }, - dialog: { alert: vi.fn(), prompt: vi.fn(), set: vi.fn() }, + }, + keymap: { + layer: vi.fn((next) => { + layer = next(); + }), + }, + ui: { + slot: vi.fn((claim) => { + if (claim.append === "app") claim.render(); + return vi.fn(); + }), + toast: { show: vi.fn() }, + dialog: { + alert: vi.fn().mockResolvedValue(undefined), + prompt: vi.fn().mockResolvedValue(undefined), + set: vi.fn(), }, - }; + }, + }; + plugin.setup(context as any); + const commands = layer!.commands; + const command = (id: string) => commands.find((item) => item.id === `quota.${id}`)!; + return { context, commands, command }; +} + +describe("V2 quota TUI commands", () => { + beforeEach(() => { + build.mockReset().mockResolvedValue({ + state: "output", + title: "Tokens", + output: "ok", + dialogSize: "large", + }); + }); - plugin.setup(context as any); + it("registers every quota command as a local slash command", () => { + const { context, commands } = startTui(); - expect(layer?.commands.map((command) => command.slash.name)).toEqual([ + expect(commands.map((command) => command.slash.name)).toEqual([ "quota", "quota_status", "quota_announcements", @@ -41,7 +79,7 @@ describe("V2 quota TUI commands", () => { "tokens_session_all", "tokens_between", ]); - expect(layer?.commands.every((command) => command.palette)).toBe(true); + expect(commands.every((command) => command.palette)).toBe(true); expect(context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "app" })); expect(context.ui.slot).toHaveBeenCalledWith( expect.objectContaining({ append: "sidebar.content" }), @@ -60,4 +98,47 @@ describe("V2 quota TUI commands", () => { "session.tool.failed", ]); }); + + it("asks OpenCode to keep typed slash arguments only for commands that take arguments", () => { + const { commands } = startTui(); + + expect( + commands.filter((command) => command.slash.arguments).map((command) => command.slash.name), + ).toEqual(["quota_announcements", "pricing_refresh", "tokens_between"]); + }); + + it("passes the typed /tokens_between string to the command without prompting", async () => { + const { context, command } = startTui(); + + await command("tokens_between").run(" 2026-09-01 2026-09-25 "); + + expect(context.ui.dialog.prompt).not.toHaveBeenCalled(); + expect(build).toHaveBeenCalledWith( + expect.objectContaining({ command: "tokens_between", arguments: "2026-09-01 2026-09-25" }), + ); + }); + + it("prompts for /tokens_between arguments only when none were typed", async () => { + const { context, command } = startTui(); + + await command("tokens_between").run(""); + await command("tokens_between").run(); + + expect(context.ui.dialog.prompt).toHaveBeenCalledTimes(2); + expect(build).not.toHaveBeenCalled(); + }); + + it("reads the session model from the OpenCode 2 session store", async () => { + const { context, command } = startTui(); + + await command("quota_status").run(); + const { resolveSessionMeta } = build.mock.calls[0][0]; + + await expect(resolveSessionMeta("ses_known")).resolves.toEqual({ + modelID: "claude-sonnet-4-5", + providerID: "anthropic", + }); + await expect(resolveSessionMeta("ses_unknown")).resolves.toEqual({}); + expect(context.data.session.get).toHaveBeenCalledWith("ses_known"); + }); }); From a64dd07b926ea3d5a89885bbb101a8f02f13a009 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:42:28 +0200 Subject: [PATCH 30/97] fix(tui): show idle toasts once per turn Trigger the showOnIdle toast on session.execution.succeeded, which OpenCode 2 emits once when a turn finishes. session.step.ended fires after every model step, so it now only refreshes the sidebar and footers. --- src/tui-v2.tsx | 6 ++-- tests/plugin.maintainer-announcements.test.ts | 2 +- tests/tui-v2-commands.test.ts | 2 +- tests/v4-phase5-cross-surface.test.ts | 33 ++++++++++++++----- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 7649869a..4a2dfb81 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -450,7 +450,9 @@ const plugin = Plugin.define({ }) .catch(reportFailure); }; - const onStepEnded = api.data.on("session.step.ended", (event) => trigger(event, "idle")); + const onExecutionSucceeded = api.data.on("session.execution.succeeded", (event) => + trigger(event, "idle"), + ); const onCompacted = api.data.on("session.compaction.ended", (event) => trigger(event, "compacted"), ); @@ -469,7 +471,7 @@ const plugin = Plugin.define({ if (typeof id === "string") questionToolCalls.delete(id); }); disposeEvents = () => { - onStepEnded(); + onExecutionSucceeded(); onCompacted(); onQuestionStarted(); onQuestionSucceeded(); diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index b69561b9..fe7e490a 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -339,7 +339,7 @@ describe("maintainer announcement plugin integration", () => { ); const cli = await startCli(); - cli.listeners.get("session.step.ended")?.({ data: { sessionID: "session-idle" } }); + cli.listeners.get("session.execution.succeeded")?.({ data: { sessionID: "session-idle" } }); cli.listeners.get("session.tool.input.started")?.({ data: { id: "call-1", name: "question", sessionID: "session-question" }, }); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 80fb2eff..f254c8e9 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -91,7 +91,7 @@ describe("V2 quota TUI commands", () => { expect.objectContaining({ append: "home.footer.status" }), ); expect(context.data.on.mock.calls.map(([event]) => event)).toEqual([ - "session.step.ended", + "session.execution.succeeded", "session.compaction.ended", "session.tool.input.started", "session.tool.success", diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 0be534a9..1d4246e7 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -163,7 +163,10 @@ async function setupV2Surfaces(client: ReturnType, provider } as never); expect(tool?.name).toBe("quota_status"); - const events = new Map void>(); + const events = new Map void>>(); + const emit = (event: string, sessionID: string) => { + for (const callback of events.get(event) ?? []) callback({ data: { sessionID } }); + }; let commands: Array<{ slash: { name: string }; run: (input?: unknown) => Promise }> = []; const alert = vi.fn(async (_input: { title: string; message: string }) => {}); const toast = vi.fn(); @@ -179,8 +182,10 @@ async function setupV2Surfaces(client: ReturnType, provider location: { directory: process.cwd() }, data: { on: (event: string, callback: (event: { data: { sessionID: string } }) => void) => { - events.set(event, callback); - return () => events.delete(event); + const callbacks = events.get(event) ?? new Set(); + callbacks.add(callback); + events.set(event, callbacks); + return () => callbacks.delete(callback); }, location: { default: () => ({ directory: process.cwd() }), @@ -221,7 +226,7 @@ async function setupV2Surfaces(client: ReturnType, provider tool: tool!, alert, toast, - events, + emit, quota: quota!, renderSidebar: (sessionID: string) => renderSurface("sidebar.content", renderedSurfaces.sidebar, { sessionID }), @@ -484,7 +489,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { it("proves V2 CLI command and toast, server diagnostic tool, TUI placement, export, and redaction", async () => { const client = createClient(); const v2 = await setupV2Surfaces(client, PHASE5_RUNTIME_PROVIDER_IDS); - v2.events.get("session.step.ended")?.({ data: { sessionID: "phase5-session" } }); + v2.emit("session.execution.succeeded", "phase5-session"); await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(1)); await v2.quota.run(); expect(v2.alert).toHaveBeenCalledOnce(); @@ -509,7 +514,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(toastOutput).toContain("tree-model"); const callsAfterFirstToast = vi.mocked(globalThis.fetch).mock.calls.length; - v2.events.get("session.step.ended")?.({ data: { sessionID: "phase5-session" } }); + v2.emit("session.execution.succeeded", "phase5-session"); await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(2)); // The failed accounting source is retried on the next V2 CLI event. expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(callsAfterFirstToast + 1); @@ -619,6 +624,16 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(homeCompact).not.toContain("tok "); assertPhase5CanariesRedacted(homeCompact); + // Each model step refreshes the footers; only the finished turn shows the idle toast. + const toastsBeforeSteps = v2.toast.mock.calls.length; + renderedSurfaces.compact.length = 0; + v2.emit("session.step.ended", "phase5-session"); + v2.emit("session.step.ended", "phase5-session"); + await vi.waitFor(() => expect(renderedSurfaces.compact.length).toBeGreaterThanOrEqual(2)); + expect(v2.toast).toHaveBeenCalledTimes(toastsBeforeSteps); + v2.emit("session.execution.succeeded", "phase5-session"); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(toastsBeforeSteps + 1)); + currentConfig = configFor("singleWindow"); const singleWindowSidebar = await v2.renderSidebar("phase5-session"); expect(singleWindowSidebar).toContain("64%"); @@ -775,7 +790,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { ), ).toEqual([-5, -10]); - v2.events.get("session.step.ended")?.({ data: { sessionID: "minimax-session" } }); + v2.emit("session.execution.succeeded", "minimax-session"); await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); const toastOutput = getV2ToastMessage(v2.toast); expect(toastOutput).toContain("MiniMax Token Plan"); @@ -841,7 +856,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(serverOutput).toContain("Fable"); expect(serverOutput).toContain("98% left"); - v2.events.get("session.step.ended")?.({ data: { sessionID: "anthropic-fable-session" } }); + v2.emit("session.execution.succeeded", "anthropic-fable-session"); await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); const toastOutput = getV2ToastMessage(v2.toast); expect(toastOutput).toContain("Fable"); @@ -896,7 +911,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(serverOutput).not.toContain("video"); expect(serverOutput).not.toContain("Invalid normalized provider result"); - v2.events.get("session.step.ended")?.({ data: { sessionID: "minimax-china-session" } }); + v2.emit("session.execution.succeeded", "minimax-china-session"); await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); const toastOutput = getV2ToastMessage(v2.toast); expect(toastOutput).toContain("MiniMax Token Plan"); From 99f15896c54cc69e4e92b0c4b852004d632e8c19 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:48:13 +0200 Subject: [PATCH 31/97] fix(storage): resolve the OpenCode database path once, honoring OPENCODE_DB Credentials honored OPENCODE_DB, but sessions and messages picked the first existing opencode.db from a per-platform data-dir candidate list. On macOS that list included the real ~/.local/share/opencode even when XDG_DATA_HOME pointed elsewhere, so a sandboxed OpenCode 2 run could read the real database. Add getOpenCodeDbPath() with OpenCode 2's rules (packages/cli/src/ database-path.ts): OPENCODE_DB (absolute, or relative to the data dir; :memory: passes through), else /opencode.db, where the data dir is $XDG_DATA_HOME/opencode or ~/.local/share/opencode. Credentials, sessions, token reports, and /quota_status all use it. OpenCode 2 has exactly one data dir on every platform, so the data-dir candidates (APPDATA/LOCALAPPDATA, ~/.local/share, ~/Library/Application Support fallbacks) are removed. /quota_status now shows one "opencode.db: path=... present=..." row. --- src/lib/opencode-auth.ts | 49 ++------- src/lib/opencode-db-path.ts | 16 +++ src/lib/opencode-runtime-paths.ts | 13 +-- src/lib/opencode-storage.ts | 25 +---- src/lib/path-pick.ts | 14 --- src/lib/quota-stats.ts | 2 +- src/lib/quota-status.ts | 35 +------ tests/lib.opencode-auth.test.ts | 4 +- tests/lib.opencode-db-path.test.ts | 125 +++++++++++++++++++++++ tests/lib.opencode-runtime-paths.test.ts | 43 -------- tests/lib.opencode-sqlite.test.ts | 8 +- tests/lib.opencode-storage.test.ts | 13 +-- tests/lib.quota-status.test.ts | 17 ++- 13 files changed, 173 insertions(+), 191 deletions(-) create mode 100644 src/lib/opencode-db-path.ts delete mode 100644 src/lib/path-pick.ts create mode 100644 tests/lib.opencode-db-path.test.ts diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index c4692c20..57af9e52 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -6,12 +6,8 @@ */ import { existsSync } from "fs"; -import { isAbsolute, join, resolve } from "path"; -import { - getOpencodeRuntimeDirCandidates, - getOpencodeRuntimeDirs, -} from "./opencode-runtime-paths.js"; +import { getOpenCodeDbPath } from "./opencode-db-path.js"; import { openOpenCodeSqliteReadOnly, type SqliteConn } from "./opencode-sqlite.js"; import type { AuthData } from "./types.js"; @@ -63,22 +59,12 @@ export function formatCredentialDisplayNames( let authCache: AuthCacheEntry | null = null; /** - * Get candidate OpenCode credential database paths in priority order. - * - * `OPENCODE_DB` overrides the normal runtime data-directory candidates. + * OpenCode credential database paths: the one resolved database, or none when + * `OPENCODE_DB` is `:memory:`. */ export function getCredentialDatabasePaths(): string[] { - const { dataDirs } = getOpencodeRuntimeDirCandidates(); - return getCredentialDatabasePathsForDataDirs(dataDirs); -} - -/** Returns OpenCode's primary credential database path for display/logging. */ -export function getCredentialDatabasePath(): string { - const paths = getCredentialDatabasePaths(); - if (paths[0]) return paths[0]; - return process.env.OPENCODE_DB?.trim() === ":memory:" - ? ":memory:" - : join(getOpencodeRuntimeDirs().dataDir, "opencode.db"); + const path = getOpenCodeDbPath(); + return path === ":memory:" ? [] : [path]; } export async function readAuthFile(): Promise { @@ -91,7 +77,8 @@ export async function readAuthFile(): Promise { } export async function readCredentialRows(): Promise { - return readCredentialRowsFromDatabases(getCredentialDatabasePaths()); + const [path] = getCredentialDatabasePaths(); + return path ? readCredentialDatabase(path) : []; } function canonicalCredentialValueKey(value: unknown): string { @@ -136,28 +123,6 @@ export function selectConnectionCredentialRows( return selected; } -async function readCredentialRowsFromDatabases(paths: string[]): Promise { - for (const path of paths) { - const rows = await readCredentialDatabase(path); - if (rows.length > 0) return rows; - } - - return []; -} - -function getCredentialDatabasePathsForDataDirs(dataDirs: string[]): string[] { - const override = process.env.OPENCODE_DB?.trim(); - if (override) { - if (override === ":memory:") return []; - return [ - isAbsolute(override) - ? override - : resolve(dataDirs[0] ?? getOpencodeRuntimeDirs().dataDir, override), - ]; - } - return dataDirs.map((dataDir) => join(dataDir, "opencode.db")); -} - async function readCredentialDatabase(path: string): Promise { if (!existsSync(path)) return []; diff --git a/src/lib/opencode-db-path.ts b/src/lib/opencode-db-path.ts new file mode 100644 index 00000000..267e5d66 --- /dev/null +++ b/src/lib/opencode-db-path.ts @@ -0,0 +1,16 @@ +import { resolve } from "path"; + +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; + +/** + * OpenCode's SQLite database path, resolved the way OpenCode 2 does + * (`packages/cli/src/database-path.ts`): `OPENCODE_DB` (absolute, or relative + * to the data dir; `:memory:` passes through), else `/opencode.db`. + * + * Credentials, sessions, and messages all live in this one database. + */ +export function getOpenCodeDbPath(): string { + const override = process.env.OPENCODE_DB?.trim(); + if (override === ":memory:") return override; + return resolve(getOpencodeRuntimeDirs().dataDir, override || "opencode.db"); +} diff --git a/src/lib/opencode-runtime-paths.ts b/src/lib/opencode-runtime-paths.ts index 635233cc..86abecae 100644 --- a/src/lib/opencode-runtime-paths.ts +++ b/src/lib/opencode-runtime-paths.ts @@ -9,8 +9,9 @@ export interface OpencodeRuntimeDirs { stateDir: string; } +// No data-dir candidates: OpenCode 2 has exactly one data dir (`dataDir`), and +// its database path comes from `getOpenCodeDbPath()`. export interface OpencodeRuntimeDirCandidates { - dataDirs: string[]; configDirs: string[]; cacheDirs: string[]; stateDirs: string[]; @@ -75,7 +76,6 @@ export function getOpencodeRuntimeDirCandidates(params?: { const windowsRoamingFallback = join(home, "AppData", "Roaming"); const windowsLocalFallback = join(home, "AppData", "Local"); - const dataDirs: string[] = [primary.dataDir]; const configDirs: string[] = [primary.configDir]; const cacheDirs: string[] = [primary.cacheDir]; const stateDirs: string[] = [primary.stateDir]; @@ -87,10 +87,7 @@ export function getOpencodeRuntimeDirCandidates(params?: { const appDataBase = winAppData || windowsRoamingFallback; const localAppDataBase = winLocalAppData || windowsLocalFallback; - // Data and config: include both roaming and local as alternates. - dataDirs.push(join(appDataBase, "opencode")); - dataDirs.push(join(localAppDataBase, "opencode")); - + // Config: include both roaming and local as alternates. configDirs.push(join(appDataBase, "opencode")); configDirs.push(join(localAppDataBase, "opencode")); @@ -99,26 +96,22 @@ export function getOpencodeRuntimeDirCandidates(params?: { stateDirs.push(join(localAppDataBase, "opencode")); } else if (platform === "darwin") { // Preserve compatibility with legacy Linux-style installs on macOS. - dataDirs.push(join(home, ".local", "share", "opencode")); configDirs.push(join(home, ".config", "opencode")); cacheDirs.push(join(home, ".cache", "opencode")); stateDirs.push(join(home, ".local", "state", "opencode")); // macOS canonical dirs (OpenCode xdg-basedir should already map here, // but keep explicit candidates to aid diagnostics and migrations). - dataDirs.push(join(home, "Library", "Application Support", "opencode")); configDirs.push(join(home, "Library", "Application Support", "opencode")); cacheDirs.push(join(home, "Library", "Caches", "opencode")); } else { // Linux / other: add common legacy fallbacks. - dataDirs.push(join(home, ".local", "share", "opencode")); configDirs.push(join(home, ".config", "opencode")); cacheDirs.push(join(home, ".cache", "opencode")); stateDirs.push(join(home, ".local", "state", "opencode")); } return { - dataDirs: dedupe(dataDirs), configDirs: dedupe(configDirs), cacheDirs: dedupe(cacheDirs), stateDirs: dedupe(stateDirs), diff --git a/src/lib/opencode-storage.ts b/src/lib/opencode-storage.ts index 248c9129..4b5fdd34 100644 --- a/src/lib/opencode-storage.ts +++ b/src/lib/opencode-storage.ts @@ -1,9 +1,7 @@ import { existsSync } from "fs"; -import { join } from "path"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpenCodeDbPath } from "./opencode-db-path.js"; import { openOpenCodeSqliteReadOnly } from "./opencode-sqlite.js"; -import { pickFirstExistingPath } from "./path-pick.js"; /** * Error thrown when a session is not found. @@ -68,24 +66,6 @@ export type OpenCodeDbStats = { assistantMessageCount: number; }; -export function getOpenCodeDataDirCandidates(): string[] { - // OpenCode stores data under `${Global.Path.data}` which is `join(xdgData, "opencode")`. - // We return candidate opencode data dirs in priority order. - return getOpencodeRuntimeDirCandidates().dataDirs; -} - -export function getOpenCodeDataDir(): string { - return pickFirstExistingPath(getOpenCodeDataDirCandidates()); -} - -export function getOpenCodeDbPathCandidates(): string[] { - return getOpenCodeDataDirCandidates().map((d) => join(d, "opencode.db")); -} - -export function getOpenCodeDbPath(): string { - return pickFirstExistingPath(getOpenCodeDbPathCandidates()); -} - type ProjectedMessageRow = { id: string; session_id: string; @@ -167,8 +147,7 @@ function openDbOrNull(): { open: () => ReturnType; } | null { const dbPath = getOpenCodeDbPath(); - if (!dbPath) return null; - if (!existsSync(dbPath)) return null; + if (dbPath === ":memory:" || !existsSync(dbPath)) return null; return { dbPath, open: () => openOpenCodeSqliteReadOnly(dbPath), diff --git a/src/lib/path-pick.ts b/src/lib/path-pick.ts deleted file mode 100644 index b1635365..00000000 --- a/src/lib/path-pick.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { existsSync } from "fs"; - -export function pickFirstExistingPath(candidates: string[]): string { - for (const p of candidates) { - try { - if (existsSync(p)) return p; - } catch { - // ignore - } - } - - // Deterministic fallback for diagnostics. - return candidates[0] ?? ""; -} diff --git a/src/lib/quota-stats.ts b/src/lib/quota-stats.ts index a3eaa408..f254a833 100644 --- a/src/lib/quota-stats.ts +++ b/src/lib/quota-stats.ts @@ -12,9 +12,9 @@ import { listProvidersForModelId, lookupCost, } from "./modelsdev-pricing.js"; +import { getOpenCodeDbPath } from "./opencode-db-path.js"; import type { OpenCodeMessage } from "./opencode-storage.js"; import { - getOpenCodeDbPath, iterAssistantMessages, iterAssistantMessagesForSession, iterAssistantMessagesForSessions, diff --git a/src/lib/quota-status.ts b/src/lib/quota-status.ts index 88f81937..6439c163 100644 --- a/src/lib/quota-status.ts +++ b/src/lib/quota-status.ts @@ -30,13 +30,9 @@ import { readPricingRefreshState, hasProvider as snapshotHasProvider, } from "./modelsdev-pricing.js"; -import { getCredentialDatabasePath, getCredentialDatabasePaths } from "./opencode-auth.js"; +import { getOpenCodeDbPath } from "./opencode-db-path.js"; import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; -import { - getOpenCodeDbPath, - getOpenCodeDbPathCandidates, - getOpenCodeDbStats, -} from "./opencode-storage.js"; +import { getOpenCodeDbStats } from "./opencode-storage.js"; import { getQuotaProviderDisplayLabel } from "./provider-metadata.js"; import type { QuotaProviderDefinition } from "./quota-providers.js"; import { isMaintainedQuotaProviderTuning } from "./quota-providers.js"; @@ -799,33 +795,10 @@ export async function buildQuotaStatusReport(params: { key: "opencode_dirs", value: `data=${runtime.dataDir} config=${runtime.configDir} cache=${runtime.cacheDir} state=${runtime.stateDir}`, }); - const authCandidates = getCredentialDatabasePaths(); - const authPresent: string[] = []; - await Promise.all( - authCandidates.map(async (p) => { - try { - await stat(p); - authPresent.push(p); - } catch { - // ignore missing/unreadable - } - }), - ); + const dbPath = getOpenCodeDbPath(); pathsRows.push({ key: "opencode.db", - value: `preferred=${getCredentialDatabasePath()} present=${joinOrNone(authPresent)} candidates=${joinOrNone(authCandidates)}`, - }); - const dbCandidates = getOpenCodeDbPathCandidates(); - const dbSelected = getOpenCodeDbPath(); - const dbPresent: string[] = []; - await Promise.all( - dbCandidates.map(async (p) => { - if (await pathExists(p)) dbPresent.push(p); - }), - ); - pathsRows.push({ - key: "opencode db", - value: `preferred=${dbSelected} present=${joinOrNone(dbPresent)} candidates=${joinOrNone(dbCandidates)}`, + value: `path=${dbPath} present=${(await pathExists(dbPath)) ? "true" : "false"}`, }); appendProviderStatusDetailRows( diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index d927cbd5..29277efa 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -7,7 +7,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { clearReadAuthFileCacheForTests, formatCredentialDisplayNames, - getCredentialDatabasePath, getCredentialDatabasePaths, readAuthFile, readCredentialRows, @@ -120,8 +119,7 @@ describe("OpenCode auth reader", () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); - expect(getCredentialDatabasePaths()).toContain(join(dataDir, "opencode.db")); - expect(getCredentialDatabasePath()).toBe(join(dataDir, "opencode.db")); + expect(getCredentialDatabasePaths()).toEqual([join(dataDir, "opencode.db")]); vi.stubEnv("OPENCODE_DB", "custom.db"); expect(getCredentialDatabasePaths()).toEqual([join(dataDir, "custom.db")]); diff --git a/tests/lib.opencode-db-path.test.ts b/tests/lib.opencode-db-path.test.ts new file mode 100644 index 00000000..70c45c7c --- /dev/null +++ b/tests/lib.opencode-db-path.test.ts @@ -0,0 +1,125 @@ +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + clearReadAuthFileCacheForTests, + getCredentialDatabasePaths, + readAuthFile, +} from "../src/lib/opencode-auth.js"; +import { getOpenCodeDbPath } from "../src/lib/opencode-db-path.js"; +import { getOpenCodeDbStats } from "../src/lib/opencode-storage.js"; + +const temporaryDirectories: string[] = []; +const originalPlatform = process.platform; + +afterEach(async () => { + Object.defineProperty(process, "platform", { value: originalPlatform }); + clearReadAuthFileCacheForTests(); + vi.unstubAllEnvs(); + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function createRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), "opencode-quota-db-path-")); + temporaryDirectories.push(root); + return root; +} + +/** Writes an OpenCode 2 database with one API-key credential and `sessions` sessions. */ +async function createOpenCodeDatabase(path: string, key: string, sessions: number): Promise { + await mkdir(dirname(path), { recursive: true }); + const database = new DatabaseSync(path); + database.exec(` + CREATE TABLE credential ( + id TEXT PRIMARY KEY, + integration_id TEXT, + label TEXT NOT NULL, + value TEXT NOT NULL, + active INTEGER, + time_updated INTEGER NOT NULL + ); + CREATE TABLE "session_v2" (id TEXT PRIMARY KEY); + CREATE TABLE "session_message" (id TEXT PRIMARY KEY, type TEXT NOT NULL, data TEXT NOT NULL); + `); + database + .prepare("INSERT INTO credential VALUES ('deepseek', 'deepseek', 'default', ?, 1, 1)") + .run(JSON.stringify({ type: "key", key })); + for (let i = 0; i < sessions; i++) { + database.prepare(`INSERT INTO "session_v2" VALUES (?)`).run(`ses_${i}`); + } + database.close(); +} + +describe("OpenCode database path", () => { + it("never falls back to the home database on macOS when XDG_DATA_HOME is set", async () => { + const root = await createRoot(); + const home = join(root, "home"); + const sandboxData = join(root, "sandbox-data"); + await createOpenCodeDatabase( + join(home, ".local", "share", "opencode", "opencode.db"), + "home-key", + 3, + ); + Object.defineProperty(process, "platform", { value: "darwin" }); + vi.stubEnv("HOME", home); + vi.stubEnv("XDG_DATA_HOME", sandboxData); + vi.stubEnv("OPENCODE_DB", undefined); + + const expected = join(sandboxData, "opencode", "opencode.db"); + expect(getOpenCodeDbPath()).toBe(expected); + expect(getCredentialDatabasePaths()).toEqual([expected]); + await expect(readAuthFile()).resolves.toBeNull(); + await expect(getOpenCodeDbStats()).resolves.toMatchObject({ + dbPath: expected, + sessionCount: 0, + }); + }); + + it("uses an absolute OPENCODE_DB for credentials and sessions", async () => { + const root = await createRoot(); + const custom = join(root, "elsewhere", "custom.db"); + await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), "default-key", 1); + await createOpenCodeDatabase(custom, "custom-key", 2); + vi.stubEnv("XDG_DATA_HOME", join(root, "data")); + vi.stubEnv("OPENCODE_DB", custom); + + expect(getOpenCodeDbPath()).toBe(custom); + await expect(readAuthFile()).resolves.toMatchObject({ deepseek: { key: "custom-key" } }); + await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: custom, sessionCount: 2 }); + }); + + it("resolves a relative OPENCODE_DB against the data dir for credentials and sessions", async () => { + const root = await createRoot(); + const custom = join(root, "data", "opencode", "profiles", "work.db"); + await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), "default-key", 1); + await createOpenCodeDatabase(custom, "work-key", 4); + vi.stubEnv("XDG_DATA_HOME", join(root, "data")); + vi.stubEnv("OPENCODE_DB", join("profiles", "work.db")); + + expect(getOpenCodeDbPath()).toBe(custom); + await expect(readAuthFile()).resolves.toMatchObject({ deepseek: { key: "work-key" } }); + await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: custom, sessionCount: 4 }); + }); + + it("reads no database file when OPENCODE_DB is :memory:", async () => { + const root = await createRoot(); + await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), "default-key", 1); + vi.stubEnv("XDG_DATA_HOME", join(root, "data")); + vi.stubEnv("OPENCODE_DB", ":memory:"); + + expect(getOpenCodeDbPath()).toBe(":memory:"); + expect(getCredentialDatabasePaths()).toEqual([]); + await expect(readAuthFile()).resolves.toBeNull(); + await expect(getOpenCodeDbStats()).resolves.toMatchObject({ + dbPath: ":memory:", + sessionCount: 0, + }); + }); +}); diff --git a/tests/lib.opencode-runtime-paths.test.ts b/tests/lib.opencode-runtime-paths.test.ts index a770a903..7b84ad40 100644 --- a/tests/lib.opencode-runtime-paths.test.ts +++ b/tests/lib.opencode-runtime-paths.test.ts @@ -5,7 +5,6 @@ import { getOpencodeRuntimeDirCandidates, getOpencodeRuntimeDirs, } from "../src/lib/opencode-runtime-paths.js"; -import { getOpenCodeDbPathCandidates } from "../src/lib/opencode-storage.js"; describe("opencode-runtime-paths", () => { it("builds deterministic dirs from XDG env fallbacks", () => { @@ -74,53 +73,11 @@ describe("opencode-runtime-paths", () => { primary, }); - expect(c.dataDirs[0]).toBe(primary.dataDir); expect(c.configDirs[0]).toBe(primary.configDir); expect(c.cacheDirs[0]).toBe(primary.cacheDir); expect(c.stateDirs[0]).toBe(primary.stateDir); - expect(c.dataDirs).toContain(join(env.APPDATA!, "opencode")); - expect(c.dataDirs).toContain(join(env.LOCALAPPDATA!, "opencode")); - expect(c.configDirs).toContain(join(env.APPDATA!, "opencode")); expect(c.configDirs).toContain(join(env.LOCALAPPDATA!, "opencode")); }); - - it("derives opencode.db candidates from runtime data dirs", () => { - const prev = { - XDG_DATA_HOME: process.env.XDG_DATA_HOME, - XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, - XDG_CACHE_HOME: process.env.XDG_CACHE_HOME, - XDG_STATE_HOME: process.env.XDG_STATE_HOME, - }; - - process.env.XDG_DATA_HOME = "/x/data"; - process.env.XDG_CONFIG_HOME = "/x/config"; - process.env.XDG_CACHE_HOME = "/x/cache"; - process.env.XDG_STATE_HOME = "/x/state"; - - try { - const env: NodeJS.ProcessEnv = { - XDG_DATA_HOME: "/x/data", - XDG_CONFIG_HOME: "/x/config", - XDG_CACHE_HOME: "/x/cache", - XDG_STATE_HOME: "/x/state", - }; - - const dirs = getOpencodeRuntimeDirCandidates({ - platform: "linux", - env, - homeDir: "/home/test", - }); - const candidates = getOpenCodeDbPathCandidates(); - - // Ensure the primary candidate matches runtime primary (order matters). - expect(candidates[0]).toBe(join(dirs.dataDirs[0]!, "opencode.db")); - } finally { - process.env.XDG_DATA_HOME = prev.XDG_DATA_HOME; - process.env.XDG_CONFIG_HOME = prev.XDG_CONFIG_HOME; - process.env.XDG_CACHE_HOME = prev.XDG_CACHE_HOME; - process.env.XDG_STATE_HOME = prev.XDG_STATE_HOME; - } - }); }); diff --git a/tests/lib.opencode-sqlite.test.ts b/tests/lib.opencode-sqlite.test.ts index bb62bcba..e27d2016 100644 --- a/tests/lib.opencode-sqlite.test.ts +++ b/tests/lib.opencode-sqlite.test.ts @@ -6,10 +6,10 @@ import { describe, expect, it, vi } from "vitest"; import { openOpenCodeSqliteReadOnly } from "../src/lib/opencode-sqlite.js"; -const runtimePaths = vi.hoisted(() => ({ dataDirs: [] as string[] })); +const runtimePaths = vi.hoisted(() => ({ dataDir: "" })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ dataDirs: runtimePaths.dataDirs }), + getOpencodeRuntimeDirs: () => ({ dataDir: runtimePaths.dataDir }), })); describe("opencode sqlite adapter", () => { @@ -61,7 +61,7 @@ describe("opencode sqlite adapter", () => { const cutoff = Date.parse("2026-07-16T00:00:00.000Z"); try { - runtimePaths.dataDirs = [dir]; + runtimePaths.dataDir = dir; const writer = new DatabaseSync(dbPath); writer.exec(` CREATE TABLE "session_v2" ( @@ -123,7 +123,7 @@ describe("opencode sqlite adapter", () => { ]); expect(messages.every((message) => typeof message.time?.completed === "number")).toBe(true); } finally { - runtimePaths.dataDirs = []; + runtimePaths.dataDir = ""; await rm(dir, { recursive: true, force: true }); } }); diff --git a/tests/lib.opencode-storage.test.ts b/tests/lib.opencode-storage.test.ts index c741d9c2..64b00c9d 100644 --- a/tests/lib.opencode-storage.test.ts +++ b/tests/lib.opencode-storage.test.ts @@ -16,17 +16,8 @@ vi.mock("fs", async (importOriginal) => { }; }); -vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: ["/tmp/opencode"], - configDirs: ["/tmp/opencode"], - cacheDirs: ["/tmp/opencode"], - stateDirs: ["/tmp/opencode"], - }), -})); - -vi.mock("../src/lib/path-pick.js", () => ({ - pickFirstExistingPath: vi.fn(() => "/tmp/opencode.db"), +vi.mock("../src/lib/opencode-db-path.js", () => ({ + getOpenCodeDbPath: () => "/tmp/opencode.db", })); vi.mock("../src/lib/opencode-sqlite.js", () => ({ diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index 61479842..85ba4948 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -56,7 +56,6 @@ vi.mock("fs/promises", () => ({ })); vi.mock("../src/lib/opencode-auth.js", () => ({ - getCredentialDatabasePath: () => "/tmp/opencode.db", getCredentialDatabasePaths: () => ["/tmp/opencode.db"], readAuthFileCached: vi.fn(async () => ({})), })); @@ -137,9 +136,11 @@ vi.mock("../src/lib/version.js", () => ({ getPackageVersion: vi.fn(async () => "1.2.3"), })); -vi.mock("../src/lib/opencode-storage.js", () => ({ +vi.mock("../src/lib/opencode-db-path.js", () => ({ getOpenCodeDbPath: () => "/tmp/opencode.db", - getOpenCodeDbPathCandidates: () => ["/tmp/opencode.db"], +})); + +vi.mock("../src/lib/opencode-storage.js", () => ({ getOpenCodeDbStats: vi.fn(async () => ({ sessionCount: 0, messageCount: 0, @@ -513,9 +514,7 @@ describe("buildQuotaStatusReport", () => { ); expect(report).toContain("- quota_plugin_configured: true"); expect(report).toContain("- quota_plugin_paths: /tmp/project/tui.jsonc"); - expect(report).toContain( - "- opencode.db: preferred=/tmp/opencode.db present=(none) candidates=/tmp/opencode.db", - ); + expect(report).toContain("- opencode.db: path=/tmp/opencode.db present=false"); expect(report).toContain( "- pricing: source=test active_source=bundled generated_at=2026-01-01T00:00:00.000Z units=usd_per_1m_tokens", ); @@ -1503,8 +1502,7 @@ describe("buildQuotaStatusReport", () => { paths: - opencode_dirs: data=/tmp/data config=/tmp/config cache=/tmp/cache state=/tmp/state - - opencode.db: preferred=/tmp/opencode.db present=(none) candidates=/tmp/opencode.db - - opencode db: preferred=/tmp/opencode.db present=(none) candidates=/tmp/opencode.db + - opencode.db: path=/tmp/opencode.db present=false - alibaba auth configured: false - alibaba_api_key_source: (none) - alibaba_api_key_checked_paths: (none) @@ -1534,7 +1532,8 @@ describe("buildQuotaStatusReport", () => { - billing_cycle_start_day: (calendar month) minimax: - " + + minimax_china:" `); const titles = report From a161fb1d8ed50b162a77662640609d4194a21d09 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:53:37 +0200 Subject: [PATCH 32/97] refactor(cli)!: stop writing tui.json OpenCode 2 never reads tui.json or tui.jsonc. It copies tui.json into cli.json once, and its TUI loads the ./tui export of every plugin listed in opencode.json. One "plugin" entry in opencode.json is now enough. - init no longer plans, creates, or edits tui.json(c). Web installs no longer remove quota entries from an existing tui.json either. - update only rewrites the plugin array in opencode.json(c); it ignores tui.json(c) and the old tui.plugin key. - Existing tui.json files are left untouched. The TUI / Web / Both question stays: Web still skips the TUI display, command display, and announcement questions and writes quota UI "none". BREAKING CHANGE: init and update no longer write tui.json. --- src/lib/init-installer.ts | 181 +------------------------------ src/lib/scoped-update.ts | 10 +- tests/lib.init-installer.test.ts | 175 +++++++----------------------- tests/lib.scoped-update.test.ts | 29 ++++- 4 files changed, 67 insertions(+), 328 deletions(-) diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index 18acf659..b444d7d0 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -11,7 +11,6 @@ import { type ConfigFileFormat, dedupeNonEmptyStrings, type EditableConfigPath, - extractPluginSpecsFromParsedConfig, findGitWorktreeRoot, getPluginSpecFromEntry, isQuotaPluginSpec, @@ -42,7 +41,6 @@ import type { QuotaToastConfig, SessionTokenScope, TuiCommandDisplay } from "./t const QUOTA_PLUGIN_SPEC = "@slkiser/opencode-quota@latest"; const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json"; -const TUI_SCHEMA_URL = "https://opencode.ai/tui.json"; const GITHUB_REPO_URL = "https://github.com/slkiser/opencode-quota"; const GITHUB_STAR_NOTE = `if this helps, stars are appreciated: ${GITHUB_REPO_URL}`; const TUI_COMMAND_DISPLAY_COMMENT = @@ -76,7 +74,7 @@ export interface InitInstallerQuickSetupNote { } export interface PlannedConfigEdit { - kind: "opencode" | "tui" | "quota"; + kind: "opencode" | "quota"; path: string; existed: boolean; format: ConfigFileFormat; @@ -130,7 +128,6 @@ type PromptOption = { type NormalizedQuotaUiIntent = { choices: InitQuotaUiChoice[]; enableToast: boolean; - installTuiPlugin: boolean; enableSidebarPanel: boolean; enableCompactStatus: boolean; }; @@ -211,7 +208,6 @@ function normalizeQuotaUiIntent(selections: InitInstallerSelections): Normalized return { choices, enableToast: choices.includes("toast"), - installTuiPlugin: selections.interfaces !== "web", enableSidebarPanel, enableCompactStatus, }; @@ -289,12 +285,11 @@ function ensureSchema(root: JsonObject, schemaUrl: string, edit: PlannedConfigEd function appendQuotaPluginIfMissing(params: { container: unknown[]; pathLabel: string; - kind: "opencode" | "tui"; edit: PlannedConfigEdit; }): void { const alreadyConfigured = params.container.some((entry) => { const spec = getPluginSpecFromEntry(entry); - return typeof spec === "string" && isQuotaPluginSpec(spec, params.kind); + return typeof spec === "string" && isQuotaPluginSpec(spec, "opencode"); }); if (alreadyConfigured) { @@ -325,51 +320,6 @@ function ensureTopLevelPluginArray(root: JsonObject, edit: PlannedConfigEdit): u return root.plugin; } -function ensureTuiPluginArray( - root: JsonObject, - edit: PlannedConfigEdit, -): { - container: unknown[]; - pathLabel: string; -} { - if (isPlainObject(root.tui) && hasOwnKey(root.tui, "plugin")) { - const tuiRoot = root.tui as JsonObject; - if (!Array.isArray(tuiRoot.plugin)) { - throw new InitInstallerError( - `Cannot update ${edit.kind} config because tui.plugin is not an array.`, - { path: edit.path }, - ); - } - - return { - container: tuiRoot.plugin, - pathLabel: "tui.plugin", - }; - } - - if (hasOwnKey(root, "plugin")) { - if (!Array.isArray(root.plugin)) { - throw new InitInstallerError( - `Cannot update ${edit.kind} config because plugin is not an array.`, - { path: edit.path }, - ); - } - - return { - container: root.plugin, - pathLabel: "plugin", - }; - } - - const next: unknown[] = []; - root.plugin = next; - edit.changed = true; - return { - container: next, - pathLabel: "plugin", - }; -} - function addSettingIfMissing( target: JsonObject, key: string, @@ -699,7 +649,6 @@ async function planOpencodeEdit(params: { appendQuotaPluginIfMissing({ container: plugin, pathLabel: "plugin", - kind: "opencode", edit, }); @@ -939,128 +888,6 @@ async function planQuotaConfigEdit(params: { return edit; } -function getCanonicalQuotaPackageSpecForRemoval(entry: unknown): string | undefined { - const spec = - typeof entry === "string" - ? entry - : Array.isArray(entry) && typeof entry[0] === "string" - ? entry[0] - : undefined; - if (spec === "@slkiser/opencode-quota") return spec; - if ( - spec !== undefined && - /^@slkiser\/opencode-quota@(?:v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?|[A-Za-z][0-9A-Za-z._-]*)$/.test( - spec, - ) - ) { - return spec; - } - return undefined; -} - -function removeQuotaPluginsFromTui(root: JsonObject, edit: PlannedConfigEdit): void { - const containers: Array<{ value: unknown; pathLabel: string }> = [ - { value: root.plugin, pathLabel: "plugin" }, - ]; - if (isPlainObject(root.tui)) { - containers.push({ value: root.tui.plugin, pathLabel: "tui.plugin" }); - } - - for (const container of containers) { - if (!Array.isArray(container.value)) continue; - const retained = container.value.filter((entry) => { - const spec = getCanonicalQuotaPackageSpecForRemoval(entry); - if (spec !== undefined) { - edit.updatedKeys.push(`${container.pathLabel}: remove ${spec}`); - return false; - } - return true; - }); - if (retained.length !== container.value.length) { - container.value.splice(0, container.value.length, ...retained); - edit.changed = true; - } - } -} - -async function planTuiEdit(params: { - selections: InitInstallerSelections; - baseDir: string; -}): Promise { - const target = resolveEditableConfigPath({ - dir: params.baseDir, - kind: "tui", - preferredFormat: params.selections.configFormat ?? "jsonc", - convertJsonToJsonc: params.selections.interfaces !== "web", - }); - const edit: PlannedConfigEdit = { - kind: "tui", - path: target.path, - existed: target.existed, - format: target.format, - changed: false, - addedPlugins: [], - addedKeys: [], - updatedKeys: [], - valueChanges: [], - skippedValues: [], - warnings: [], - }; - - if (params.selections.interfaces === "web" && !target.existed) { - return edit; - } - - const root = target.existed - ? await readExistingConfig({ - path: target.sourcePath, - format: target.sourcePath.endsWith(".jsonc") ? "jsonc" : "json", - }) - : {}; - if (!target.existed) { - ensureSchema(root, TUI_SCHEMA_URL, edit); - } - - if (params.selections.interfaces === "web") { - removeQuotaPluginsFromTui(root, edit); - } else { - const existingPluginSpecs = extractPluginSpecsFromParsedConfig(root); - if (existingPluginSpecs.some((spec) => isQuotaPluginSpec(spec, "tui"))) { - edit.skippedValues.push(`tui config already includes ${QUOTA_PLUGIN_SPEC}`); - } else { - const pluginTarget = ensureTuiPluginArray(root, edit); - appendQuotaPluginIfMissing({ - container: pluginTarget.container, - pathLabel: pluginTarget.pathLabel, - kind: "tui", - edit, - }); - } - } - - const documentEdit = await planConfigDocumentEdit({ - target, - desiredData: root, - managedComments: - params.selections.interfaces === "web" - ? [] - : [ - { - path: ["plugin"], - text: TUI_COMMAND_DISPLAY_COMMENT, - }, - { - path: ["plugin"], - text: "// OpenCode Quota: loads the TUI sidebar, compact status, and local commands.", - }, - ], - }); - edit.changed = documentEdit.changed; - edit.documentEdit = documentEdit; - - return edit; -} - function buildPlanSummary(plan: InitInstallerPlan): string[] { const quotaUiIntent = normalizeQuotaUiIntent(plan.selections); const opencodeFormat = @@ -1224,10 +1051,6 @@ export async function planInitInstaller(params: { }), quotaEdit, ]; - const tuiEdit = await planTuiEdit({ selections, baseDir }); - if (selections.interfaces !== "web" || tuiEdit.existed) { - edits.push(tuiEdit); - } const quickSetupNotes = buildQuickSetupNotes(selections); const warnings = edits.flatMap((edit) => edit.warnings); diff --git a/src/lib/scoped-update.ts b/src/lib/scoped-update.ts index 915494e5..8c00b899 100644 --- a/src/lib/scoped-update.ts +++ b/src/lib/scoped-update.ts @@ -104,10 +104,8 @@ export function sanitizeOpenCodePackageSpec( } function selectedConfigPaths(root: string): string[] { - return (["opencode", "tui"] as const).flatMap((kind) => { - const path = resolveExistingConfigPath(root, kind); - return path ? [path] : []; - }); + const path = resolveExistingConfigPath(root, "opencode"); + return path ? [path] : []; } async function dedupeByRealPath(paths: string[]): Promise { @@ -127,10 +125,6 @@ function pluginArrays(config: unknown): Array<{ path: (string | number)[]; entri const root = config as Record; const arrays: Array<{ path: (string | number)[]; entries: unknown[] }> = []; if (Array.isArray(root.plugin)) arrays.push({ path: ["plugin"], entries: root.plugin }); - if (root.tui && typeof root.tui === "object" && !Array.isArray(root.tui)) { - const tui = root.tui as Record; - if (Array.isArray(tui.plugin)) arrays.push({ path: ["tui", "plugin"], entries: tui.plugin }); - } return arrays; } diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index d5ee8750..da7b6316 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -129,7 +129,7 @@ describe("init installer planning and merge behavior", () => { expect(plan.baseDir).toBe(projectDir); expect(plan.summaryLines).toContain("Session token scope: Current session and descendants"); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); expect(plan.quickSetupNotes).toEqual([ { providerId: "anthropic", @@ -142,8 +142,8 @@ describe("init installer planning and merge behavior", () => { expect(result.writtenPaths).toEqual([ join(projectDir, "opencode.jsonc"), join(projectDir, "opencode-quota", "quota-toast.jsonc"), - join(projectDir, "tui.jsonc"), ]); + expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); const config = readJson(join(projectDir, "opencode.jsonc")); expect(config).toMatchObject({ @@ -211,13 +211,12 @@ describe("init installer planning and merge behavior", () => { "// Quota presentation and reset-period choices.", ); - for (const hostPath of [join(projectDir, "opencode.jsonc"), join(projectDir, "tui.jsonc")]) { - const raw = readFileSync(hostPath, "utf8"); - expect(raw).toContain( - "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog.", - ); - expect(() => parseJsonOrJsonc(raw, true)).not.toThrow(); - } + const hostRaw = readFileSync(join(projectDir, "opencode.jsonc"), "utf8"); + expect(hostRaw).toContain( + "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog.", + ); + expect(() => parseJsonOrJsonc(hostRaw, true)).not.toThrow(); + expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); }); it("writes legacy experimental.quotaToast only when explicitly requested", async () => { @@ -282,18 +281,6 @@ describe("init installer planning and merge behavior", () => { "utf8", ); - writeFileSync( - join(projectDir, "tui.json"), - JSON.stringify({ - plugin: ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.js"], - tui: { - plugin: [["some-other-plugin", { debug: true }]], - }, - theme: "dark", - }), - "utf8", - ); - const plan = await planInitInstaller({ cwd: projectDir, selections: installerSelections({ @@ -305,7 +292,6 @@ describe("init installer planning and merge behavior", () => { }); const opencodeEdit = plan.edits.find((edit) => edit.kind === "opencode"); - const tuiEdit = plan.edits.find((edit) => edit.kind === "tui"); expect(opencodeEdit?.warnings).toEqual([]); expect(opencodeEdit?.addedPlugins).toEqual([]); expect(opencodeEdit?.addedKeys).toEqual([]); @@ -324,10 +310,6 @@ describe("init installer planning and merge behavior", () => { expect(quotaEdit?.updatedKeys).toEqual( expect.arrayContaining(["quotaToast.showSessionTokens", "quotaToast.enabledProviders"]), ); - expect(tuiEdit?.addedPlugins).toEqual([]); - expect(tuiEdit?.skippedValues).toContain( - "tui config already includes @slkiser/opencode-quota@latest", - ); await applyInitInstallerPlan(plan); @@ -354,12 +336,6 @@ describe("init installer planning and merge behavior", () => { showSessionTokens: false, enabledProviders: ["cursor", "opencode-go"], }); - - const tui = readJson(join(projectDir, "tui.jsonc")); - expect(tui.$schema).toBeUndefined(); - expect(tui.theme).toBe("dark"); - expect(tui.plugin).toHaveLength(1); - expect(tui.tui.plugin).toHaveLength(1); }); it("adds the server plugin when opencode config only references the tui entrypoint", async () => { @@ -388,7 +364,7 @@ describe("init installer planning and merge behavior", () => { ]); }); - it("writes sidebar disabled when selected UI omits sidebar and tui config already has the plugin", async () => { + it("leaves an existing tui.json untouched and writes sidebar disabled when sidebar is not selected", async () => { const projectDir = join(tempDir, "project"); mkdirSync(projectDir, { recursive: true }); @@ -402,54 +378,29 @@ describe("init installer planning and merge behavior", () => { const plan = await planSelections({}, projectDir); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); - const tui = readJson(join(projectDir, "tui.json")); - expect(tui).toEqual({ plugin: ["@slkiser/opencode-quota"] }); + expect(readFileSync(join(projectDir, "tui.json"), "utf8")).toBe( + JSON.stringify({ plugin: ["@slkiser/opencode-quota"] }), + ); + expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); const quotaConfig = readJson(join(projectDir, "opencode-quota", "quota-toast.json")); expect(quotaConfig.tuiSidebarPanel).toEqual({ enabled: false }); }); - it("adds the tui plugin when tui config only references the server entrypoint", async () => { - const projectDir = join(tempDir, "project"); - mkdirSync(projectDir, { recursive: true }); - - writeFileSync( - join(projectDir, "tui.json"), - JSON.stringify({ - plugin: ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/index.js"], - }), - "utf8", - ); - - const plan = await planSelections({ quotaUi: ["sidebar"] }, projectDir); - - const tuiEdit = plan.edits.find((edit) => edit.kind === "tui"); - expect(tuiEdit?.addedPlugins).toEqual(["plugin: @slkiser/opencode-quota@latest"]); - - await applyInitInstallerPlan(plan); - - const tui = readJson(join(projectDir, "tui.jsonc")); - expect(tui.plugin).toEqual([ - "file:///Users/test/Downloads/GitHub/opencode-quota/dist/index.js", - "@slkiser/opencode-quota@latest", - ]); - }); - - it("creates both opencode and tui targets for sidebar mode and appends missing plugins", async () => { + it("creates only opencode and quota configs for sidebar mode", async () => { const projectDir = join(tempDir, "project"); mkdirSync(projectDir, { recursive: true }); const plan = await planSelections({ quotaUi: ["sidebar"] }, projectDir); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); const opencode = readJson(join(projectDir, "opencode.jsonc")); - const tui = readJson(join(projectDir, "tui.jsonc")); expect(opencode.plugin).toEqual(["@slkiser/opencode-quota@latest"]); expect(opencode.experimental).toBeUndefined(); @@ -462,10 +413,7 @@ describe("init installer planning and merge behavior", () => { showSessionTokens: true, tuiSidebarPanel: { enabled: true }, }); - expect(tui).toEqual({ - $schema: "https://opencode.ai/tui.json", - plugin: ["@slkiser/opencode-quota@latest"], - }); + expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); }); it("leaves compact TUI status alone when not selected for fresh sidebar installs", async () => { @@ -475,12 +423,12 @@ describe("init installer planning and merge behavior", () => { const plan = await planSelections({ quotaUi: ["sidebar"] }, projectDir); expect(plan.summaryLines).not.toContain("Compact status mode: Home bottom + session prompt"); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); expect(existsSync(join(projectDir, "opencode.jsonc"))).toBe(true); - expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(true); + expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); const quotaConfig = readJson(join(projectDir, "opencode-quota", "quota-toast.json")); expect(quotaConfig.tuiSidebarPanel).toEqual({ enabled: true }); expect(quotaConfig.tuiCompactStatus).toBeUndefined(); @@ -500,7 +448,7 @@ describe("init installer planning and merge behavior", () => { await applyInitInstallerPlan(plan); - expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(true); + expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); const quotaConfig = readJson(join(projectDir, "opencode-quota", "quota-toast.json")); expect(quotaConfig.tuiSidebarPanel).toEqual({ enabled: true }); expect(quotaConfig.tuiCompactStatus).toEqual({ @@ -519,7 +467,7 @@ describe("init installer planning and merge behavior", () => { expect(plan.selections.quotaUi).toEqual(["compact_status"]); expect(plan.summaryLines).toContain("TUI surfaces: Compact status"); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); @@ -581,7 +529,7 @@ describe("init installer planning and merge behavior", () => { expect(plan.summaryLines).toContain("TUI surfaces: Manual commands only"); expect(plan.summaryLines).toContain("Maintainer announcements: Disabled"); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); @@ -606,7 +554,7 @@ describe("init installer planning and merge behavior", () => { expect(plan.summaryLines).not.toContain( "TUI plugin: install for maintainer announcement home notices only", ); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); @@ -1010,7 +958,7 @@ describe("init installer planning and merge behavior", () => { expect(quotaConfig.maintainerAnnouncements).toEqual({ enabled: false }); }); - it("creates both opencode and tui targets for toast + sidebar mode with popup toasts enabled", async () => { + it("creates only opencode and quota configs for toast + sidebar mode with popup toasts enabled", async () => { const projectDir = join(tempDir, "project"); mkdirSync(projectDir, { recursive: true }); @@ -1022,12 +970,11 @@ describe("init installer planning and merge behavior", () => { expect(plan.summaryLines).toContain("TUI surfaces: Sidebar + Toast"); expect(plan.summaryLines).toContain("Quota reset periods: Single window"); expect(plan.summaryLines).toContain("Quota percentage meaning: Remaining"); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); const opencode = readJson(join(projectDir, "opencode.jsonc")); - const tui = readJson(join(projectDir, "tui.jsonc")); expect(opencode.plugin).toEqual(["@slkiser/opencode-quota@latest"]); expect(opencode.experimental).toBeUndefined(); @@ -1040,19 +987,16 @@ describe("init installer planning and merge behavior", () => { showSessionTokens: true, tuiSidebarPanel: { enabled: true }, }); - expect(tui).toEqual({ - $schema: "https://opencode.ai/tui.json", - plugin: ["@slkiser/opencode-quota@latest"], - }); + expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); }); - it("does not touch tui config for none mode and disables popup toasts when missing", async () => { + it("disables popup toasts for none mode when missing", async () => { const projectDir = join(tempDir, "project"); mkdirSync(projectDir, { recursive: true }); const plan = await planSelections({ quotaUi: ["none"] }, projectDir); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); @@ -1170,37 +1114,14 @@ describe("init installer planning and merge behavior", () => { expect(secondPlan.edits.find((edit) => edit.kind === "opencode")?.changed).toBe(false); }); - it("installs Web server-only and removes only canonical quota package entries", async () => { + it("installs Web server-only and leaves an existing tui.jsonc untouched", async () => { const projectDir = join(tempDir, "web-only"); mkdirSync(join(projectDir, "opencode-quota"), { recursive: true }); - writeFileSync( - join(projectDir, "tui.jsonc"), - `{ + const tuiRaw = `{ // keep this comment - "plugin": [ - "@slkiser/opencode-quota", - "@slkiser/opencode-quota@latest", - ["@slkiser/opencode-quota@4.0.0", { "source": "tuple" }], - "@slkiser/opencode-quota-helper", - "@slkiser/opencode-quota/latest", - "custom:@slkiser/opencode-quota", - "file:///tmp/opencode-quota/dist/tui.js", - "./opencode-quota", - ["node", "./opencode-quota.js"], - { "package": "@slkiser/opencode-quota", "command": "run" }, - "other-tui-plugin" - ], - "tui": { - "plugin": [ - "@slkiser/opencode-quota@4.0.0-beta.1", - ["bun", "custom-command"], - { "spec": "@slkiser/opencode-quota@latest" } - ] - }, - "theme": "dark" - }`, - "utf8", - ); + "plugin": ["@slkiser/opencode-quota"], + }`; + writeFileSync(join(projectDir, "tui.jsonc"), tuiRaw, "utf8"); const plan = await planInitInstaller({ cwd: projectDir, @@ -1214,31 +1135,11 @@ describe("init installer planning and merge behavior", () => { expect(plan.summaryLines).toContain("Interface: Web"); expect(plan.summaryLines.some((line) => line.startsWith("TUI surfaces:"))).toBe(false); expect(plan.summaryLines.some((line) => line.startsWith("Command display:"))).toBe(false); - expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota", "tui"]); + expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); - const tuiRaw = readFileSync(join(projectDir, "tui.jsonc"), "utf8"); - expect(tuiRaw).toContain("// keep this comment"); - expect(tuiRaw).toContain('["node", "./opencode-quota.js"]'); - expect(tuiRaw).toContain('{ "package": "@slkiser/opencode-quota", "command": "run" }'); - expect(tuiRaw).toContain('["bun", "custom-command"]'); - expect(readJson(join(projectDir, "tui.jsonc"))).toEqual({ - plugin: [ - "@slkiser/opencode-quota-helper", - "@slkiser/opencode-quota/latest", - "custom:@slkiser/opencode-quota", - "file:///tmp/opencode-quota/dist/tui.js", - "./opencode-quota", - ["node", "./opencode-quota.js"], - { package: "@slkiser/opencode-quota", command: "run" }, - "other-tui-plugin", - ], - tui: { - plugin: [["bun", "custom-command"], { spec: "@slkiser/opencode-quota@latest" }], - }, - theme: "dark", - }); + expect(readFileSync(join(projectDir, "tui.jsonc"), "utf8")).toBe(tuiRaw); expect(readJson(join(projectDir, "opencode.jsonc")).plugin).toEqual([ "@slkiser/opencode-quota@latest", ]); @@ -1398,11 +1299,13 @@ describe("init installer planning and merge behavior", () => { mkdirSync(projectDir, { recursive: true }); const plan = await planSelections({ quotaUi: ["sidebar"] }, projectDir); - writeFileSync(join(projectDir, "tui.jsonc"), '{"theme":"raced"}\n', "utf8"); + const quotaPath = join(projectDir, "opencode-quota", "quota-toast.jsonc"); + mkdirSync(join(projectDir, "opencode-quota"), { recursive: true }); + writeFileSync(quotaPath, '{"raced":true}\n', "utf8"); await expect(applyInitInstallerPlan(plan)).rejects.toThrow("changed since preview"); expect(existsSync(join(projectDir, "opencode.jsonc"))).toBe(false); - expect(readFileSync(join(projectDir, "tui.jsonc"), "utf8")).toBe('{"theme":"raced"}\n'); + expect(readFileSync(quotaPath, "utf8")).toBe('{"raced":true}\n'); }); it("fails when an existing plugin container is not an array", async () => { diff --git a/tests/lib.scoped-update.test.ts b/tests/lib.scoped-update.test.ts index 3b51a88b..4c1423c4 100644 --- a/tests/lib.scoped-update.test.ts +++ b/tests/lib.scoped-update.test.ts @@ -135,9 +135,28 @@ describe("scoped update config planning", () => { expect(readFileSync(ignoredJson, "utf8")).toContain("@1.0.0"); }); + it("leaves tui.json files untouched", async () => { + const f = fixture(); + const projectTui = join(f.project, "tui.json"); + const globalTui = join(f.global, "tui.jsonc"); + const original = `{"plugin":["@slkiser/opencode-quota@3.11.1"]}`; + write(projectTui, original); + write(globalTui, original); + const plan = await planScopedUpdate({ + cwd: f.project, + env: f.env, + homeDir: join(f.root, "home"), + platform: "linux", + }); + expect(plan.configPaths).toEqual([]); + expect(plan.configEdits).toEqual([]); + expect(readFileSync(projectTui, "utf8")).toBe(original); + expect(readFileSync(globalTui, "utf8")).toBe(original); + }); + it("honors OPENCODE_CONFIG_DIR and deduplicates project/global real paths", async () => { const f = fixture(); - const config = join(f.project, "tui.jsonc"); + const config = join(f.project, "opencode.jsonc"); write(config, `{"plugin":["@slkiser/opencode-quota"]}`); const plan = await planScopedUpdate({ cwd: f.project, @@ -151,7 +170,7 @@ describe("scoped update config planning", () => { const f = fixture(); const valid = join(f.project, "opencode.json"); write(valid, `{"plugin":["@slkiser/opencode-quota@3.11.1"]}`); - write(join(f.global, "tui.jsonc"), `{ nope`); + write(join(f.global, "opencode.jsonc"), `{ nope`); await expect( planScopedUpdate({ cwd: f.project, env: f.env, homeDir: join(f.root, "home") }), ).rejects.toThrow("unparseable"); @@ -291,7 +310,7 @@ describe("scoped update config planning", () => { it("preserves package-first then explicit migration-only snapshot order", async () => { const f = fixture(); const projectConfig = join(f.project, "opencode.json"); - const globalConfig = join(f.global, "tui.json"); + const globalConfig = join(f.global, "opencode.json"); const globalSidecar = join(f.global, "opencode-quota", "quota-toast.json"); const workspaceSidecar = join(f.project, "opencode-quota", "quota-toast.jsonc"); write(projectConfig, `{"plugin":["@slkiser/opencode-quota@3.11.1"]}`); @@ -661,7 +680,7 @@ describe("scoped update application safety", () => { it("preflights every snapshot before the first write", async () => { const f = fixture(); const first = join(f.project, "opencode.json"); - const second = join(f.global, "tui.json"); + const second = join(f.global, "opencode.json"); const original = `{"plugin":["@slkiser/opencode-quota@3.11.1"]}`; write(first, original); write(second, original); @@ -720,7 +739,7 @@ describe("scoped update application safety", () => { ] as const)("reports earlier writes when a later config %s fails", async (failureKind) => { const f = fixture(); const first = join(f.project, "opencode.json"); - const second = join(f.global, "tui.json"); + const second = join(f.global, "opencode.json"); write(first, `{"plugin":["@slkiser/opencode-quota@3.11.1"]}`); write(second, `{"plugin":["@slkiser/opencode-quota@3.11.1"]}`); const plan = await planScopedUpdate({ From f3696efe3e6dc3eaba1ffd304649e8ecd1f94b7f Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:57:20 +0200 Subject: [PATCH 33/97] refactor(config)!: remove tuiCommandDisplay and suppressWhenNativeProviderQuota OpenCode 2 slash commands only open dialogs, so tuiCommandDisplay has no effect. tuiCompactStatus.suppressWhenNativeProviderQuota was only read by the deleted V1 TUI runtime. Drop both from the config loader, defaults, types, and the init installer (including the slash-command display question and its opencode.jsonc comment). Old keys left in user configs are ignored, like any other unknown key. --- src/lib/config.ts | 38 ------------ src/lib/init-installer.ts | 56 +---------------- src/lib/types.ts | 8 --- tests/lib.config.security.test.ts | 5 -- tests/lib.config.test.ts | 34 ---------- tests/lib.init-installer.test.ts | 69 +++------------------ tests/provider-surfaces.opencode-go.test.ts | 2 - tests/v4-phase5-cross-surface.test.ts | 4 -- 8 files changed, 8 insertions(+), 208 deletions(-) diff --git a/src/lib/config.ts b/src/lib/config.ts index 0ebb7335..77084e14 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -27,7 +27,6 @@ import type { QuotaResetWindow, QuotaToastConfig, SessionTokenScope, - TuiCommandDisplay, } from "./types.js"; import { DEFAULT_CONFIG } from "./types.js"; @@ -42,7 +41,6 @@ export const QUOTA_TOAST_SETTING_SOURCE_KEYS = [ "enableToast", "resetNotifications.enabled", "resetNotifications.windows", - "tuiCommandDisplay", "formatStyle", "percentDisplayMode", "quotaProjection", @@ -77,7 +75,6 @@ export const QUOTA_TOAST_SETTING_SOURCE_KEYS = [ "tuiCompactStatus.enabled", "tuiCompactStatus.homeBottom", "tuiCompactStatus.sessionPrompt", - "tuiCompactStatus.suppressWhenNativeProviderQuota", "tuiCompactStatus.maxWidth", "tuiCompactStatus.formatStyle", "tuiPromptBar.enabled", @@ -156,7 +153,6 @@ type ValidatedQuotaToastPatch = { enabled?: boolean; enableToast?: boolean; resetNotifications?: QuotaResetNotificationsPatch; - tuiCommandDisplay?: TuiCommandDisplay; formatStyle?: QuotaToastConfig["formatStyle"]; percentDisplayMode?: PercentDisplayMode; quotaProjection?: "runway"; @@ -250,10 +246,6 @@ function isValidAccountingDetail(value: unknown): value is QuotaToastConfig["acc return value === "summary" || value === "detailed"; } -function isValidTuiCommandDisplay(value: unknown): value is TuiCommandDisplay { - return value === "inline" || value === "dialog"; -} - function isValidSessionTokenScope(value: unknown): value is SessionTokenScope { return value === "current" || value === "tree"; } @@ -516,13 +508,6 @@ function extractTuiCompactStatusPatch(value: unknown): TuiCompactStatusPatch | u patch.sessionPrompt = value.sessionPrompt; } - if ( - hasOwnKey(value, "suppressWhenNativeProviderQuota") && - typeof value.suppressWhenNativeProviderQuota === "boolean" - ) { - patch.suppressWhenNativeProviderQuota = value.suppressWhenNativeProviderQuota; - } - if (hasOwnKey(value, "maxWidth") && isPositiveNumber(value.maxWidth)) { patch.maxWidth = value.maxWidth; } @@ -648,14 +633,6 @@ function extractValidatedQuotaToastPatch( if (resetNotifications) patch.resetNotifications = resetNotifications; } - if (hasOwnKey(quotaToastConfig, "tuiCommandDisplay")) { - if (isValidTuiCommandDisplay(quotaToastConfig.tuiCommandDisplay)) { - patch.tuiCommandDisplay = quotaToastConfig.tuiCommandDisplay; - } else { - reportIssue?.("tuiCommandDisplay", 'expected "inline" or "dialog"'); - } - } - const formatStyle = getConfiguredFormatStyle(quotaToastConfig as Partial); if (formatStyle) { patch.formatStyle = formatStyle; @@ -938,11 +915,6 @@ function applyValidatedQuotaToastPatch( } } - if (hasOwnKey(patch, "tuiCommandDisplay")) { - config.tuiCommandDisplay = patch.tuiCommandDisplay!; - applySettingSource(settingSources, "tuiCommandDisplay", sourcePath); - } - if (hasOwnKey(patch, "formatStyle")) { config.formatStyle = patch.formatStyle!; applySettingSource(settingSources, "formatStyle", sourcePath); @@ -1116,16 +1088,6 @@ function applyValidatedQuotaToastPatch( applySettingSource(settingSources, "tuiCompactStatus.sessionPrompt", sourcePath); } - if (hasOwnKey(patch.tuiCompactStatus, "suppressWhenNativeProviderQuota")) { - config.tuiCompactStatus.suppressWhenNativeProviderQuota = - patch.tuiCompactStatus.suppressWhenNativeProviderQuota!; - applySettingSource( - settingSources, - "tuiCompactStatus.suppressWhenNativeProviderQuota", - sourcePath, - ); - } - if (hasOwnKey(patch.tuiCompactStatus, "maxWidth")) { config.tuiCompactStatus.maxWidth = patch.tuiCompactStatus.maxWidth!; applySettingSource(settingSources, "tuiCompactStatus.maxWidth", sourcePath); diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index b444d7d0..65fe4285 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -37,14 +37,12 @@ import { resolveQuotaFormatStyle, } from "./quota-format-style.js"; import { QUOTA_PROVIDERS_AGGREGATE_ID } from "./quota-providers.js"; -import type { QuotaToastConfig, SessionTokenScope, TuiCommandDisplay } from "./types.js"; +import type { QuotaToastConfig, SessionTokenScope } from "./types.js"; const QUOTA_PLUGIN_SPEC = "@slkiser/opencode-quota@latest"; const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json"; const GITHUB_REPO_URL = "https://github.com/slkiser/opencode-quota"; const GITHUB_STAR_NOTE = `if this helps, stars are appreciated: ${GITHUB_REPO_URL}`; -const TUI_COMMAND_DISPLAY_COMMENT = - "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog."; export type InitInstallerInterface = "tui" | "web" | "both"; export type InitInstallerScope = "project" | "global"; @@ -62,7 +60,6 @@ export interface InitInstallerSelections { percentDisplayMode: QuotaToastConfig["percentDisplayMode"]; showSessionTokens: boolean; sessionTokenScope?: SessionTokenScope; - tuiCommandDisplay?: TuiCommandDisplay; maintainerAnnouncements?: boolean; configFormat?: ConfigFileFormat; } @@ -478,13 +475,6 @@ function planTuiCompactStatusConfig(params: { `${pathLabel}.sessionPrompt`, params.edit, ); - setInstallerOwnedSetting( - tuiCompactStatus, - "suppressWhenNativeProviderQuota", - true, - `${pathLabel}.suppressWhenNativeProviderQuota`, - params.edit, - ); } async function readExistingConfig(params: { @@ -664,10 +654,6 @@ async function planOpencodeEdit(params: { target, desiredData: root, managedComments: [ - { - path: ["plugin"], - text: TUI_COMMAND_DISPLAY_COMMENT, - }, { path: ["plugin"], text: "// OpenCode Quota: loads the server plugin for slash commands and quota checks.", @@ -780,15 +766,6 @@ async function planQuotaConfigEdit(params: { "quotaToast.enableToast", edit, ); - if (params.selections.tuiCommandDisplay !== undefined) { - setInstallerOwnedSetting( - quotaToast, - "tuiCommandDisplay", - params.selections.tuiCommandDisplay, - "quotaToast.tuiCommandDisplay", - edit, - ); - } setInstallerOwnedSetting( quotaToast, "showSessionTokens", @@ -902,9 +879,6 @@ function buildPlanSummary(plan: InitInstallerPlan): string[] { if (plan.selections.interfaces !== "web") { lines.push(`TUI surfaces: ${getUiLabel(quotaUiIntent.choices)}`); - lines.push( - `Command display: ${plan.selections.tuiCommandDisplay === "inline" ? "Inline with messages" : "Popup dialog"}`, - ); } lines.push( @@ -1026,10 +1000,6 @@ export async function planInitInstaller(params: { ...requestedSelections, configFormat: params.selections.configFormat ?? "jsonc", quotaUi: quotaUiIntent.choices, - tuiCommandDisplay: - params.selections.interfaces === "web" - ? undefined - : (params.selections.tuiCommandDisplay ?? "inline"), maintainerAnnouncements: params.selections.maintainerAnnouncements, manualProviders: params.selections.providerMode === "manual" @@ -1126,7 +1096,6 @@ type ExistingInstallerAnswers = { percentDisplayMode?: QuotaToastConfig["percentDisplayMode"]; showSessionTokens?: boolean; sessionTokenScope?: SessionTokenScope; - tuiCommandDisplay?: TuiCommandDisplay; maintainerAnnouncements?: boolean; }; @@ -1184,9 +1153,6 @@ async function readExistingInstallerAnswers(baseDir: string): Promise 1)) break; prompts.log.error("Manual commands only cannot be combined with automatic surfaces."); } - - tuiCommandDisplay = await prompts.select({ - message: "Where should slash commands (e.g. /quota) appear?", - initialValue: existing.tuiCommandDisplay ?? "inline", - options: [ - { - label: "Inline with messages", - value: "inline", - hint: "persist output in the message transcript", - }, - { - label: "Popup dialog", - value: "dialog", - hint: "show output in a temporary TUI popup", - }, - ], - }); - if (prompts.isCancel(tuiCommandDisplay)) return null; } const providerMode = await prompts.select({ @@ -1426,7 +1373,6 @@ async function promptForSelections( percentDisplayMode: percentDisplayMode as QuotaToastConfig["percentDisplayMode"], showSessionTokens: showSessionTokens === "yes", sessionTokenScope: sessionTokenScope as SessionTokenScope, - tuiCommandDisplay: interfaces === "web" ? undefined : (tuiCommandDisplay as TuiCommandDisplay), maintainerAnnouncements: interfaces === "web" ? undefined : maintainerAnnouncements !== false, configFormat: configFormat as ConfigFileFormat, }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 3c73d55c..e24400ae 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -50,7 +50,6 @@ export interface TuiCompactStatusConfig { enabled: boolean; homeBottom: boolean; sessionPrompt: boolean; - suppressWhenNativeProviderQuota: boolean; maxWidth: number; /** Per-surface formatStyle override. Falls back to root formatStyle when absent. */ formatStyle?: QuotaFormatStyle; @@ -81,8 +80,6 @@ export interface MaintainerAnnouncementsConfig { home: boolean; } -export type TuiCommandDisplay = "inline" | "dialog"; - /** Request timeout in milliseconds */ export const REQUEST_TIMEOUT_MS = 5000; @@ -96,9 +93,6 @@ export interface QuotaToastConfig { /** Opt-in, persisted notifications when selected quota windows reset. */ resetNotifications: QuotaResetNotificationsConfig; - /** Where deterministic native TUI command output appears. */ - tuiCommandDisplay: TuiCommandDisplay; - /** * Shared quota-row formatting style for popup toasts and the TUI sidebar. * @@ -228,7 +222,6 @@ export const DEFAULT_CONFIG: QuotaToastConfig = { enabled: false, windows: ["weekly"], }, - tuiCommandDisplay: "inline", formatStyle: DEFAULT_QUOTA_FORMAT_STYLE, percentDisplayMode: "remaining", accountingDetail: "summary", @@ -267,7 +260,6 @@ export const DEFAULT_CONFIG: QuotaToastConfig = { enabled: false, homeBottom: true, sessionPrompt: true, - suppressWhenNativeProviderQuota: true, maxWidth: 96, }, tuiPromptBar: { diff --git a/tests/lib.config.security.test.ts b/tests/lib.config.security.test.ts index 1ccf944d..652b0c08 100644 --- a/tests/lib.config.security.test.ts +++ b/tests/lib.config.security.test.ts @@ -390,7 +390,6 @@ describe("loadConfig layered precedence", () => { quotaToast: { tuiCompactStatus: { homeBottom: false, - suppressWhenNativeProviderQuota: false, maxWidth: 0, }, }, @@ -406,7 +405,6 @@ describe("loadConfig layered precedence", () => { enabled: true, homeBottom: false, sessionPrompt: false, - suppressWhenNativeProviderQuota: false, maxWidth: 80, }); expect(meta.settingSources["tuiCompactStatus.enabled"]).toBe( @@ -421,9 +419,6 @@ describe("loadConfig layered precedence", () => { expect(meta.settingSources["tuiCompactStatus.homeBottom"]).toBe( quotaConfigSource(workspaceDir), ); - expect(meta.settingSources["tuiCompactStatus.suppressWhenNativeProviderQuota"]).toBe( - quotaConfigSource(workspaceDir), - ); }); it("preserves the previous valid layer when workspace values are invalid", async () => { diff --git a/tests/lib.config.test.ts b/tests/lib.config.test.ts index 73096d07..bc174dd4 100644 --- a/tests/lib.config.test.ts +++ b/tests/lib.config.test.ts @@ -63,35 +63,6 @@ describe("loadConfig", () => { return { config, meta }; } - it("defaults and validates native TUI command display with provenance", async () => { - const defaults = await loadSdkConfig({}); - expect(defaults.config.tuiCommandDisplay).toBe("inline"); - expect(defaults.meta.settingSources).toEqual({}); - - const dialog = await loadSdkConfig({ tuiCommandDisplay: "dialog" }); - expect(dialog.config.tuiCommandDisplay).toBe("dialog"); - expect(dialog.meta.settingSources).toEqual({ - tuiCommandDisplay: "client.config.get", - }); - expect(dialog.meta.configIssues).toEqual([]); - - const invalid = await loadSdkConfig({ tuiCommandDisplay: "both" }); - expect(invalid.config.tuiCommandDisplay).toBe("inline"); - expect(invalid.meta.settingSources).toEqual({}); - expect(invalid.meta.configIssues).toEqual([ - { - path: "client.config.get", - key: "tuiCommandDisplay", - message: 'expected "inline" or "dialog"', - }, - ]); - - const removedKey = await loadSdkConfig({ tuiQuotaCommandDisplay: "dialog" }); - expect(removedKey.config.tuiCommandDisplay).toBe("inline"); - expect(removedKey.meta.settingSources).toEqual({}); - expect(removedKey.meta.configIssues).toEqual([]); - }); - it("loads accounting detail with provenance and diagnoses the removed Zen key without translating it", async () => { const defaults = await loadSdkConfig({}); expect(defaults.config.accountingDetail).toBe("summary"); @@ -377,7 +348,6 @@ describe("loadConfig", () => { enabled: true, homeBottom: false, sessionPrompt: false, - suppressWhenNativeProviderQuota: false, maxWidth: 72, }, }); @@ -385,14 +355,12 @@ describe("loadConfig", () => { enabled: true, homeBottom: false, sessionPrompt: false, - suppressWhenNativeProviderQuota: false, maxWidth: 72, }); expect(explicit.meta.settingSources).toEqual({ "tuiCompactStatus.enabled": "client.config.get", "tuiCompactStatus.homeBottom": "client.config.get", "tuiCompactStatus.sessionPrompt": "client.config.get", - "tuiCompactStatus.suppressWhenNativeProviderQuota": "client.config.get", "tuiCompactStatus.maxWidth": "client.config.get", }); expect(explicit.meta.networkSettingSources).toEqual({}); @@ -402,7 +370,6 @@ describe("loadConfig", () => { enabled: true, homeBottom: "no", sessionPrompt: null, - suppressWhenNativeProviderQuota: 0, maxWidth: -1, }, }); @@ -500,7 +467,6 @@ describe("loadConfig", () => { enabled: false, homeBottom: true, sessionPrompt: true, - suppressWhenNativeProviderQuota: true, maxWidth: 96, }); expect(DEFAULT_CONFIG.tuiPromptBar).toEqual({ enabled: false }); diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index da7b6316..728059a9 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -36,7 +36,6 @@ const DEFAULT_PROMPT_SELECT_VALUES = [ "tui", "project", "jsonc", - "inline", "auto", "singleWindow", "remaining", @@ -160,11 +159,10 @@ describe("init installer planning and merge behavior", () => { percentDisplayMode: "used", showSessionTokens: false, sessionTokenScope: "tree", - tuiCommandDisplay: "inline", }); expect(quotaConfig).not.toHaveProperty("tuiQuotaCommandDisplay"); expect(readFileSync(join(projectDir, "opencode.jsonc"), "utf8")).toContain( - "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog.", + "// OpenCode Quota: loads the server plugin for slash commands and quota checks.", ); }); @@ -184,14 +182,13 @@ describe("init installer planning and merge behavior", () => { expect(() => JSON.parse(raw)).not.toThrow(); expect(raw).not.toMatch(/^\s*\/\//m); const quotaRaw = readFileSync(join(projectDir, "opencode-quota", "quota-toast.json"), "utf8"); - expect(JSON.parse(quotaRaw).tuiCommandDisplay).toBe("inline"); expect(JSON.parse(quotaRaw).sessionTokenScope).toBe("current"); expect(plan.summaryLines).toContain("Session token scope: Current session"); expect(quotaRaw).not.toContain("//"); }); - it("writes Dialog selection and explanatory comments only to generated JSONC host configs", async () => { - const projectDir = join(tempDir, "dialog-jsonc"); + it("writes explanatory comments to generated JSONC configs", async () => { + const projectDir = join(tempDir, "comments-jsonc"); mkdirSync(projectDir, { recursive: true }); const plan = await planInitInstaller({ @@ -199,21 +196,18 @@ describe("init installer planning and merge behavior", () => { selections: installerSelections({ configFormat: "jsonc", quotaUi: ["sidebar"], - tuiCommandDisplay: "dialog", }), }); await applyInitInstallerPlan(plan); - expect(plan.summaryLines).toContain("Command display: Popup dialog"); const quotaPath = join(projectDir, "opencode-quota", "quota-toast.jsonc"); - expect(readJson(quotaPath).tuiCommandDisplay).toBe("dialog"); expect(readFileSync(quotaPath, "utf8")).toContain( "// Quota presentation and reset-period choices.", ); const hostRaw = readFileSync(join(projectDir, "opencode.jsonc"), "utf8"); expect(hostRaw).toContain( - "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog.", + "// OpenCode Quota: loads the server plugin for slash commands and quota checks.", ); expect(() => parseJsonOrJsonc(hostRaw, true)).not.toThrow(); expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); @@ -455,7 +449,6 @@ describe("init installer planning and merge behavior", () => { enabled: true, homeBottom: true, sessionPrompt: true, - suppressWhenNativeProviderQuota: true, }); }); @@ -478,7 +471,6 @@ describe("init installer planning and merge behavior", () => { enabled: true, homeBottom: true, sessionPrompt: true, - suppressWhenNativeProviderQuota: true, }); }); @@ -658,10 +650,7 @@ describe("init installer planning and merge behavior", () => { const quotaEdit = plan.edits.find((edit) => edit.kind === "quota"); expect(quotaEdit?.addedKeys).toEqual( - expect.arrayContaining([ - "quotaToast.tuiCompactStatus.homeBottom", - "quotaToast.tuiCompactStatus.suppressWhenNativeProviderQuota", - ]), + expect.arrayContaining(["quotaToast.tuiCompactStatus.homeBottom"]), ); expect(quotaEdit?.updatedKeys).toEqual( expect.arrayContaining([ @@ -690,7 +679,6 @@ describe("init installer planning and merge behavior", () => { sessionPrompt: true, maxWidth: 40, homeBottom: true, - suppressWhenNativeProviderQuota: true, }); }); @@ -720,10 +708,7 @@ describe("init installer planning and merge behavior", () => { const quotaEdit = plan.edits.find((edit) => edit.kind === "quota"); expect(quotaEdit?.addedKeys).not.toEqual( - expect.arrayContaining([ - "quotaToast.tuiCompactStatus.homeBottom", - "quotaToast.tuiCompactStatus.suppressWhenNativeProviderQuota", - ]), + expect.arrayContaining(["quotaToast.tuiCompactStatus.homeBottom"]), ); expect(quotaEdit?.updatedKeys).toEqual( expect.arrayContaining([ @@ -794,25 +779,6 @@ describe("init installer planning and merge behavior", () => { expect.objectContaining({ label: "Compact status line (TUI)", value: "compact_status" }), expect.objectContaining({ label: "Manual commands only", value: "none" }), ]); - expect( - prompts.selectCalls.find( - (call) => call.message === "Where should slash commands (e.g. /quota) appear?", - ), - ).toMatchObject({ - initialValue: "inline", - options: [ - { - label: "Inline with messages", - value: "inline", - hint: "persist output in the message transcript", - }, - { - label: "Popup dialog", - value: "dialog", - hint: "show output in a temporary TUI popup", - }, - ], - }); expect( prompts.selectCalls.find( (call) => call.message === "How should pre-configured providers be selected?", @@ -878,7 +844,6 @@ describe("init installer planning and merge behavior", () => { enabled: true, homeBottom: true, sessionPrompt: true, - suppressWhenNativeProviderQuota: true, }); }); @@ -894,24 +859,13 @@ describe("init installer planning and merge behavior", () => { "percentDisplayMode": "remaining", "showSessionTokens": false, "sessionTokenScope": "tree", - "tuiCommandDisplay": "inline", "tuiSidebarPanel": { "enabled": true }, "maintainerAnnouncements": { "enabled": true } }`, "utf8", ); const prompts = createPromptStub({ - selectValues: [ - "tui", - "project", - "jsonc", - "dialog", - "manual", - "allWindows", - "used", - "yes", - "tree", - ], + selectValues: ["tui", "project", "jsonc", "manual", "allWindows", "used", "yes", "tree"], multiselectValues: [["toast"], ["anthropic"]], confirmValues: [false, false], }); @@ -921,11 +875,6 @@ describe("init installer planning and merge behavior", () => { expect(code).toBe(0); expect(prompts.multiselectCalls[0]).toMatchObject({ initialValues: ["sidebar"] }); expect(prompts.multiselectCalls[1]).toMatchObject({ initialValues: ["openai"] }); - expect( - prompts.selectCalls.find( - (call) => call.message === "Where should slash commands (e.g. /quota) appear?", - ), - ).toMatchObject({ initialValue: "inline" }); expect( prompts.selectCalls.find( (call) => call.message === "How should pre-configured providers be selected?", @@ -1134,7 +1083,6 @@ describe("init installer planning and merge behavior", () => { expect(plan.summaryLines).toContain("Interface: Web"); expect(plan.summaryLines.some((line) => line.startsWith("TUI surfaces:"))).toBe(false); - expect(plan.summaryLines.some((line) => line.startsWith("Command display:"))).toBe(false); expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); @@ -1145,7 +1093,6 @@ describe("init installer planning and merge behavior", () => { ]); const quotaConfig = readJson(join(projectDir, "opencode-quota", "quota-toast.jsonc")); expect(quotaConfig.enableToast).toBe(false); - expect(quotaConfig.tuiCommandDisplay).toBeUndefined(); }); it("preserves unrelated JSONC comments and trailing commas during an in-place rerun edit", async () => { @@ -1161,7 +1108,6 @@ describe("init installer planning and merge behavior", () => { "formatStyle": "singleWindow", "percentDisplayMode": "remaining", "showSessionTokens": false, - "tuiCommandDisplay": "inline", "tuiSidebarPanel": { "enabled": true }, // keep unrelated section comment "unrelated": { @@ -1179,7 +1125,6 @@ describe("init installer planning and merge behavior", () => { manualProviders: ["anthropic"], formatStyle: "allWindows", percentDisplayMode: "used", - tuiCommandDisplay: "dialog", maintainerAnnouncements: false, }), }); diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 4969411d..2f850018 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -68,7 +68,6 @@ function createConfig() { showSessionTokens: false, telemetry: { enabled: false }, maintainerAnnouncements: { enabled: false, home: false }, - tuiCommandDisplay: "dialog", tuiSidebarPanel: { enabled: true, defaultExpanded: true, @@ -80,7 +79,6 @@ function createConfig() { sessionPrompt: true, maxWidth: 240, formatStyle: "allWindows", - suppressWhenNativeProviderQuota: false, }, tuiPromptBar: { enabled: true }, }); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 1d4246e7..e1724d9c 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -261,7 +261,6 @@ function configFor(formatStyle: "allWindows" | "singleWindow") { telemetry: { enabled: true, }, - tuiCommandDisplay: "dialog", tuiSidebarPanel: { enabled: true, defaultExpanded: false, @@ -273,7 +272,6 @@ function configFor(formatStyle: "allWindows" | "singleWindow") { sessionPrompt: true, maxWidth: 240, formatStyle, - suppressWhenNativeProviderQuota: false, }, }); } @@ -295,7 +293,6 @@ function configForSingleProvider(providerId = "minimax-coding-plan") { telemetry: { enabled: false, }, - tuiCommandDisplay: "dialog", tuiSidebarPanel: { enabled: true, defaultExpanded: false, @@ -307,7 +304,6 @@ function configForSingleProvider(providerId = "minimax-coding-plan") { sessionPrompt: true, maxWidth: 240, formatStyle: "allWindows", - suppressWhenNativeProviderQuota: false, }, }); } From f4a9774969fa326d43c5e00ac08d41929e13562d Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:01:32 +0200 Subject: [PATCH 34/97] refactor(status)!: drop tui.json diagnostics and fix OpenCode 2 setup text OpenCode 2 never reads tui.json, so the tui: section of /quota_status and the CLI status reported quota_plugin_configured: false on every fresh install. Remove inspectTuiConfig and its module; keep the workspace_root and config_root lines in the toast section. init no longer claims Web slash commands appear inline: Web and Desktop show no quota UI, so point users to `npx @slkiser/opencode-quota show` or the assistant's quota_status tool, and the Web-only outro no longer says to run /quota. --- src/lib/config-file-utils.ts | 8 +- src/lib/init-installer.ts | 8 +- src/lib/quota-dialog-commands.ts | 4 +- src/lib/quota-status.ts | 34 +-- src/lib/tui-config-diagnostics.ts | 109 --------- tests/helpers/plugin-test-harness.ts | 27 --- tests/lib.init-installer.test.ts | 17 +- tests/lib.quota-status.test.ts | 25 +-- tests/plugin.maintainer-announcements.test.ts | 11 - tests/plugin.quota-status-command.test.ts | 17 +- tests/tui-config-diagnostics.test.ts | 206 ------------------ 11 files changed, 36 insertions(+), 430 deletions(-) delete mode 100644 src/lib/tui-config-diagnostics.ts delete mode 100644 tests/tui-config-diagnostics.test.ts diff --git a/src/lib/config-file-utils.ts b/src/lib/config-file-utils.ts index 4dfd5881..e2d5d56d 100644 --- a/src/lib/config-file-utils.ts +++ b/src/lib/config-file-utils.ts @@ -1,7 +1,7 @@ import { existsSync } from "fs"; import { dirname, isAbsolute, join, resolve } from "path"; -export type ConfigFileKind = "opencode" | "tui"; +export type ConfigFileKind = "opencode"; export type ConfigFileFormat = "json" | "jsonc"; export interface EditableConfigPath { @@ -208,7 +208,7 @@ export function extractProviderIdsFromParsedConfig(parsed: unknown): string[] { return dedupeNonEmptyStrings(Object.keys(root.provider)); } -export function isQuotaPluginSpec(spec: string, kind: ConfigFileKind): boolean { +export function isQuotaPluginSpec(spec: string): boolean { const normalized = spec.replace(/\\/g, "/").toLowerCase(); if (normalized.includes("@slkiser/opencode-quota")) { @@ -219,7 +219,5 @@ export function isQuotaPluginSpec(spec: string, kind: ConfigFileKind): boolean { return true; } - return kind === "tui" - ? normalized.includes("opencode-quota/dist/tui.js") - : normalized.includes("opencode-quota/dist/index.js"); + return normalized.includes("opencode-quota/dist/index.js"); } diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index 65fe4285..a93db320 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -286,7 +286,7 @@ function appendQuotaPluginIfMissing(params: { }): void { const alreadyConfigured = params.container.some((entry) => { const spec = getPluginSpecFromEntry(entry); - return typeof spec === "string" && isQuotaPluginSpec(spec, "opencode"); + return typeof spec === "string" && isQuotaPluginSpec(spec); }); if (alreadyConfigured) { @@ -1182,7 +1182,7 @@ async function promptForSelections( if (interfaces === "web") { prompts.log.info( - "Web slash commands appear inline. TUI-only surfaces and popup dialogs are unavailable.", + "Web and Desktop show no quota UI. Run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to use its quota_status tool.", ); } @@ -1464,7 +1464,9 @@ export async function runInitInstaller(params?: { `Interfaces: ${interfaceLabel}`, "Configured paths:", ...configuredPaths.map((path) => `- ${path}`), - "Restart OpenCode and run /quota.", + plan.selections.interfaces === "web" + ? "Run `npx @slkiser/opencode-quota show` in a terminal to check quota." + : "Restart OpenCode and run /quota.", `If OpenCode Quota helps, please consider a star: ${GITHUB_REPO_URL}`, ].join("\n"), ); diff --git a/src/lib/quota-dialog-commands.ts b/src/lib/quota-dialog-commands.ts index 4f5b91b1..a4884492 100644 --- a/src/lib/quota-dialog-commands.ts +++ b/src/lib/quota-dialog-commands.ts @@ -48,7 +48,6 @@ import { } from "./quota-stats.js"; import { formatQuotaStatsReport } from "./quota-stats-format.js"; import { buildQuotaStatusReport, type SessionTokenError } from "./quota-status.js"; -import { inspectTuiConfig } from "./tui-config-diagnostics.js"; import type { PricingSnapshotSource } from "./types.js"; import { getPackageVersion } from "./version.js"; @@ -555,7 +554,6 @@ export async function buildStatusReportData(params: { } } - const tuiDiagnostics = await inspectTuiConfig({ roots: params.runtime.roots }); const announcementProviderIds = availability .filter((item) => item.enabled && item.available) .map((item) => item.id); @@ -564,7 +562,7 @@ export async function buildStatusReportData(params: { }); const output = await buildQuotaStatusReport({ - tuiDiagnostics, + runtimeRoots: params.runtime.roots, configSource: params.runtime.configMeta.source, configPaths: params.runtime.configMeta.paths, globalConfigPaths: params.runtime.configMeta.globalConfigPaths, diff --git a/src/lib/quota-status.ts b/src/lib/quota-status.ts index 6439c163..6225eb20 100644 --- a/src/lib/quota-status.ts +++ b/src/lib/quota-status.ts @@ -6,6 +6,7 @@ import { QUOTA_TOAST_SETTING_SOURCE_KEYS, type QuotaToastSettingSources, } from "./config.js"; +import type { RuntimeContextRoots } from "./config-file-utils.js"; import { sanitizeQuotaProviderResult, sanitizeSingleLineDisplaySnippet, @@ -670,16 +671,7 @@ export async function buildQuotaStatusReport(params: { workspaceConfigPaths?: string[]; settingSources?: QuotaToastSettingSources; configIssues?: LoadConfigIssue[]; - tuiDiagnostics?: { - workspaceRoot: string; - configRoot: string; - configured: boolean; - inferredSelectedPath: string | null; - presentPaths: string[]; - candidatePaths: string[]; - quotaPluginConfigured: boolean; - quotaPluginConfigPaths: string[]; - }; + runtimeRoots?: RuntimeContextRoots; enabledProviders: string[] | "auto"; anthropicBinaryPath?: string; cursorPlan: CursorQuotaPlan; @@ -732,25 +724,9 @@ export async function buildQuotaStatusReport(params: { ); } } - if (params.tuiDiagnostics) { - toastLines.push(""); - toastLines.push("tui:"); - toastLines.push(`- workspace_root: ${params.tuiDiagnostics.workspaceRoot}`); - toastLines.push(`- config_root: ${params.tuiDiagnostics.configRoot}`); - toastLines.push(`- config_configured: ${params.tuiDiagnostics.configured ? "true" : "false"}`); - toastLines.push( - `- inferred_selected_config_path: ${params.tuiDiagnostics.inferredSelectedPath ?? "(none)"}`, - ); - toastLines.push(`- present_config_paths: ${joinOrNone(params.tuiDiagnostics.presentPaths)}`); - toastLines.push( - `- candidate_config_paths: ${joinOrNone(params.tuiDiagnostics.candidatePaths)}`, - ); - toastLines.push( - `- quota_plugin_configured: ${params.tuiDiagnostics.quotaPluginConfigured ? "true" : "false"}`, - ); - toastLines.push( - `- quota_plugin_paths: ${joinOrNone(params.tuiDiagnostics.quotaPluginConfigPaths)}`, - ); + if (params.runtimeRoots) { + toastLines.push(`- workspace_root: ${params.runtimeRoots.workspaceRoot}`); + toastLines.push(`- config_root: ${params.runtimeRoots.configRoot}`); } toastLines.push("- providers:"); for (const p of params.providerAvailability) { diff --git a/src/lib/tui-config-diagnostics.ts b/src/lib/tui-config-diagnostics.ts deleted file mode 100644 index 3f74282f..00000000 --- a/src/lib/tui-config-diagnostics.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { existsSync } from "fs"; -import { readFile } from "fs/promises"; -import { join } from "path"; - -import { - dedupeNonEmptyStrings, - extractPluginSpecsFromParsedConfig, - findGitWorktreeRoot, - getConfigFileCandidatePaths, - isQuotaPluginSpec, - type RuntimeContextRootHints, - type RuntimeContextRoots, - resolveRuntimeContextRoots, -} from "./config-file-utils.js"; -import { parseJsonOrJsonc } from "./jsonc.js"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; - -export interface TuiConfigDiagnostics { - workspaceRoot: string; - configRoot: string; - configured: boolean; - inferredSelectedPath: string | null; - presentPaths: string[]; - candidatePaths: string[]; - quotaPluginConfigured: boolean; - quotaPluginConfigPaths: string[]; -} - -export interface InspectTuiConfigParams { - cwd?: string; - roots?: RuntimeContextRootHints | RuntimeContextRoots; -} - -function resolveTuiConfigRoots(params?: InspectTuiConfigParams): RuntimeContextRoots { - const cwd = params?.cwd ?? process.cwd(); - const providedRoots = params?.roots; - if (providedRoots) { - if ("fallbackDirectory" in providedRoots) { - return resolveRuntimeContextRoots(providedRoots); - } - - return { - workspaceRoot: providedRoots.workspaceRoot, - configRoot: providedRoots.configRoot, - }; - } - - return resolveRuntimeContextRoots({ - worktreeRoot: findGitWorktreeRoot(cwd), - activeDirectory: cwd, - fallbackDirectory: cwd, - }); -} - -function getTuiConfigCandidatePaths(roots: RuntimeContextRoots): string[] { - const { configDirs } = getOpencodeRuntimeDirCandidates(); - const searchRoots = dedupeNonEmptyStrings([ - ...configDirs, - roots.workspaceRoot, - join(roots.workspaceRoot, ".opencode"), - roots.configRoot, - join(roots.configRoot, ".opencode"), - ]); - - return searchRoots.flatMap((dir) => getConfigFileCandidatePaths(dir, "tui")); -} - -async function readConfigJson(path: string): Promise { - try { - const content = await readFile(path, "utf-8"); - return parseJsonOrJsonc(content, path.endsWith(".jsonc")); - } catch { - return null; - } -} - -async function findQuotaPluginConfigPaths(paths: string[]): Promise { - const quotaPluginConfigPaths: string[] = []; - - for (const path of paths) { - const parsed = await readConfigJson(path); - const specs = extractPluginSpecsFromParsedConfig(parsed); - if (specs.some((spec) => isQuotaPluginSpec(spec, "tui"))) { - quotaPluginConfigPaths.push(path); - } - } - - return quotaPluginConfigPaths; -} - -export async function inspectTuiConfig( - params?: InspectTuiConfigParams, -): Promise { - const roots = resolveTuiConfigRoots(params); - const candidatePaths = getTuiConfigCandidatePaths(roots); - const presentPaths = candidatePaths.filter((path) => existsSync(path)); - const quotaPluginConfigPaths = await findQuotaPluginConfigPaths(presentPaths); - - return { - workspaceRoot: roots.workspaceRoot, - configRoot: roots.configRoot, - configured: presentPaths.length > 0, - inferredSelectedPath: presentPaths[presentPaths.length - 1] ?? null, - presentPaths, - candidatePaths, - quotaPluginConfigured: quotaPluginConfigPaths.length > 0, - quotaPluginConfigPaths, - }; -} diff --git a/tests/helpers/plugin-test-harness.ts b/tests/helpers/plugin-test-harness.ts index b46f029f..667dffa6 100644 --- a/tests/helpers/plugin-test-harness.ts +++ b/tests/helpers/plugin-test-harness.ts @@ -67,17 +67,6 @@ interface PluginRuntimePathsMockOptions { includeCandidates?: boolean; } -interface PluginTuiConfigInspectionOverrides { - candidatePaths?: string[]; - configRoot?: string; - configured?: boolean; - inferredSelectedPath?: string | null; - presentPaths?: string[]; - quotaPluginConfigPaths?: string[]; - quotaPluginConfigured?: boolean; - workspaceRoot?: string; -} - export function createConfigModuleMock(loadConfig: MockFunction) { return { loadConfig, @@ -156,22 +145,6 @@ export function createPluginRuntimePathsMockModule( }; } -export function createPluginTuiConfigInspection( - root: string, - overrides: PluginTuiConfigInspectionOverrides = {}, -) { - return { - workspaceRoot: overrides.workspaceRoot ?? root, - configRoot: overrides.configRoot ?? root, - configured: overrides.configured ?? false, - inferredSelectedPath: overrides.inferredSelectedPath ?? null, - presentPaths: overrides.presentPaths ?? [], - candidatePaths: overrides.candidatePaths ?? [], - quotaPluginConfigured: overrides.quotaPluginConfigured ?? false, - quotaPluginConfigPaths: overrides.quotaPluginConfigPaths ?? [], - }; -} - export function resetPluginTestState(): void { // Per-test module resets clear in-memory plugin/cache singletons. } diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index 728059a9..ee96d7b8 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -54,6 +54,7 @@ function createPromptStub(params: { const selectCalls: { message: string; options: unknown[] }[] = []; const multiselectCalls: { message: string; required?: boolean; options: unknown[] }[] = []; const outroCalls: string[] = []; + const infoCalls: string[] = []; const confirmCalls: { message: string; initialValue?: boolean }[] = []; return { @@ -75,13 +76,16 @@ function createPromptStub(params: { }, isCancel: (value: unknown) => value === Symbol.for("cancel"), log: { - info: () => {}, + info: (message: string) => { + infoCalls.push(message); + }, success: () => {}, error: () => {}, }, selectCalls, multiselectCalls, outroCalls, + infoCalls, confirmCalls, }; } @@ -1063,6 +1067,17 @@ describe("init installer planning and merge behavior", () => { expect(secondPlan.edits.find((edit) => edit.kind === "opencode")?.changed).toBe(false); }); + it("tells Web users that Web and Desktop show no quota UI", async () => { + const prompts = createPromptStub({ selectValues: ["web", Symbol.for("cancel")] }); + + const code = await runInitInstaller({ cwd: tempDir, prompts: prompts as any }); + + expect(code).toBe(0); + expect(prompts.infoCalls).toContain( + "Web and Desktop show no quota UI. Run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to use its quota_status tool.", + ); + }); + it("installs Web server-only and leaves an existing tui.jsonc untouched", async () => { const projectDir = join(tempDir, "web-only"); mkdirSync(join(projectDir, "opencode-quota"), { recursive: true }); diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index 85ba4948..cb9b982b 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -442,20 +442,9 @@ describe("buildQuotaStatusReport", () => { showOnBothFail: "/tmp/config/opencode.json (experimental.quotaToast)", "layout.maxWidth": "/tmp/project/opencode.jsonc (experimental.quotaToast)", }, - tuiDiagnostics: { + runtimeRoots: { workspaceRoot: "/tmp/workspace", configRoot: "/tmp/project", - configured: true, - inferredSelectedPath: "/tmp/project/tui.jsonc", - presentPaths: ["/tmp/config/tui.json", "/tmp/project/tui.jsonc"], - candidatePaths: [ - "/tmp/config/tui.json", - "/tmp/config/tui.jsonc", - "/tmp/project/tui.json", - "/tmp/project/tui.jsonc", - ], - quotaPluginConfigured: true, - quotaPluginConfigPaths: ["/tmp/project/tui.jsonc"], }, enabledProviders: ["copilot"], anthropicBinaryPath: "/opt/claude/bin/claude", @@ -501,19 +490,9 @@ describe("buildQuotaStatusReport", () => { expect(report).toContain( "- setting_sources: enabled<=/tmp/config/opencode.json (experimental.quotaToast) | enableToast<=/tmp/config/opencode.json (experimental.quotaToast) | minIntervalMs<=/tmp/project/opencode.jsonc (experimental.quotaToast) | enabledProviders<=/tmp/project/opencode.jsonc (experimental.quotaToast) | pricingSnapshot.source<=/tmp/config/opencode.json (experimental.quotaToast) | pricingSnapshot.autoRefresh<=/tmp/project/opencode.jsonc (experimental.quotaToast) | showOnIdle<=/tmp/config/opencode.json (experimental.quotaToast) | showOnQuestion<=/tmp/project/opencode.jsonc (experimental.quotaToast) | showOnCompact<=/tmp/project/opencode.jsonc (experimental.quotaToast) | showOnBothFail<=/tmp/config/opencode.json (experimental.quotaToast) | layout.maxWidth<=/tmp/project/opencode.jsonc (experimental.quotaToast)", ); - expect(report).toContain("tui:"); + expect(report).not.toContain("tui:"); expect(report).toContain("- workspace_root: /tmp/workspace"); expect(report).toContain("- config_root: /tmp/project"); - expect(report).toContain("- config_configured: true"); - expect(report).toContain("- inferred_selected_config_path: /tmp/project/tui.jsonc"); - expect(report).toContain( - "- present_config_paths: /tmp/config/tui.json | /tmp/project/tui.jsonc", - ); - expect(report).toContain( - "- candidate_config_paths: /tmp/config/tui.json | /tmp/config/tui.jsonc | /tmp/project/tui.json | /tmp/project/tui.jsonc", - ); - expect(report).toContain("- quota_plugin_configured: true"); - expect(report).toContain("- quota_plugin_paths: /tmp/project/tui.jsonc"); expect(report).toContain("- opencode.db: path=/tmp/opencode.db present=false"); expect(report).toContain( "- pricing: source=test active_source=bundled generated_at=2026-01-01T00:00:00.000Z units=usd_per_1m_tokens", diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index fe7e490a..49873a5e 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -6,7 +6,6 @@ import { createPluginTestClient as createClient, createConfigModuleMock, createPluginRuntimePathsMockModule, - createPluginTuiConfigInspection, createPricingModuleMock, createProvidersRegistryModuleMock, makeQuotaToastTestConfig, @@ -47,10 +46,6 @@ const announcementMocks = vi.hoisted(() => ({ getMaintainerAnnouncementsSummary: vi.fn(), })); -const tuiDiagnosticsMocks = vi.hoisted(() => ({ - inspectTuiConfig: vi.fn(), -})); - vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); vi.mock("../src/providers/registry.js", () => createProvidersRegistryModuleMock(mocks.getProviders), @@ -62,9 +57,6 @@ vi.mock("../src/lib/alibaba-auth.js", () => vi.mock("../src/lib/opencode-runtime-paths.js", () => createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT, { includeCandidates: true }), ); -vi.mock("../src/lib/tui-config-diagnostics.js", () => ({ - inspectTuiConfig: tuiDiagnosticsMocks.inspectTuiConfig, -})); vi.mock("../src/lib/maintainer-announcements.js", () => ({ BUNDLED_MAINTAINER_ANNOUNCEMENTS: [TEST_ANNOUNCEMENT], formatMaintainerAnnouncementHomeCountLine: (activeCount: number) => { @@ -198,9 +190,6 @@ describe("maintainer announcement plugin integration", () => { resetPluginState: true, }); announcementMocks.getMaintainerAnnouncementsSummary.mockReturnValue(makeAnnouncementSummary()); - tuiDiagnosticsMocks.inspectTuiConfig.mockResolvedValue( - createPluginTuiConfigInspection(TEST_RUNTIME_ROOT), - ); await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); diff --git a/tests/plugin.quota-status-command.test.ts b/tests/plugin.quota-status-command.test.ts index 9be582ff..36bd906e 100644 --- a/tests/plugin.quota-status-command.test.ts +++ b/tests/plugin.quota-status-command.test.ts @@ -5,7 +5,6 @@ import { createAlibabaAuthModuleMock, createPluginTestClient as createClient, createConfigModuleMock, - createPluginTuiConfigInspection, createPricingModuleMock, createProvidersRegistryModuleMock, createSessionTokensModuleMock, @@ -26,7 +25,6 @@ const mocks = vi.hoisted(() => ({ fetchSessionTokensForDisplay: vi.fn(), collectQuotaStatusLiveProbes: vi.fn(), buildQuotaStatusReport: vi.fn(), - inspectTuiConfig: vi.fn(), })); vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); @@ -57,10 +55,6 @@ vi.mock("../src/lib/quota-status.js", () => ({ buildQuotaStatusReport: mocks.buildQuotaStatusReport, })); -vi.mock("../src/lib/tui-config-diagnostics.js", () => ({ - inspectTuiConfig: mocks.inspectTuiConfig, -})); - async function buildQuotaStatusDialogOutput(params: { client: ReturnType; sessionID?: string; @@ -106,7 +100,6 @@ describe("/quota_status command behavior", () => { resetModules: true, resetPluginState: true, }); - mocks.inspectTuiConfig.mockResolvedValue(createPluginTuiConfigInspection(process.cwd())); mocks.collectQuotaStatusLiveProbes.mockResolvedValue([ { providerId: "openai", @@ -259,12 +252,6 @@ describe("/quota_status command behavior", () => { }); expect(mocks.collectQuotaStatusLiveProbes).toHaveBeenCalledTimes(1); - expect(mocks.inspectTuiConfig).toHaveBeenCalledWith({ - roots: { - workspaceRoot: process.cwd(), - configRoot: process.cwd(), - }, - }); expect(mocks.collectQuotaStatusLiveProbes).toHaveBeenCalledWith( expect.objectContaining({ client, @@ -276,6 +263,10 @@ describe("/quota_status command behavior", () => { ); expect(mocks.buildQuotaStatusReport).toHaveBeenCalledWith( expect.objectContaining({ + runtimeRoots: { + workspaceRoot: process.cwd(), + configRoot: process.cwd(), + }, globalConfigPaths: [], workspaceConfigPaths: [], settingSources: {}, diff --git a/tests/tui-config-diagnostics.test.ts b/tests/tui-config-diagnostics.test.ts deleted file mode 100644 index deb8b1e1..00000000 --- a/tests/tui-config-diagnostics.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const runtimeMocks = vi.hoisted(() => ({ - getOpencodeRuntimeDirCandidates: vi.fn(), -})); - -vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: runtimeMocks.getOpencodeRuntimeDirCandidates, -})); - -describe("inspectTuiConfig", () => { - let tempDir: string; - let projectDir: string; - let globalDir: string; - let savedConfigDir: string | undefined; - - beforeEach(() => { - savedConfigDir = process.env.OPENCODE_CONFIG_DIR; - delete process.env.OPENCODE_CONFIG_DIR; - tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-tui-diag-")); - projectDir = join(tempDir, "project"); - globalDir = join(tempDir, "global", "opencode"); - - mkdirSync(projectDir, { recursive: true }); - mkdirSync(globalDir, { recursive: true }); - - runtimeMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [globalDir], - }); - }); - - afterEach(() => { - if (savedConfigDir !== undefined) process.env.OPENCODE_CONFIG_DIR = savedConfigDir; - else delete process.env.OPENCODE_CONFIG_DIR; - rmSync(tempDir, { recursive: true, force: true }); - vi.clearAllMocks(); - }); - - it("reports candidate paths and detects the quota plugin across tui config layers", async () => { - mkdirSync(join(projectDir, ".opencode"), { recursive: true }); - - writeFileSync( - join(globalDir, "tui.json"), - JSON.stringify({ plugin: ["some-other-plugin"] }), - "utf8", - ); - writeFileSync( - join(projectDir, "tui.jsonc"), - `{ - // local project tui config - "plugin": ["@slkiser/opencode-quota"] - }`, - "utf8", - ); - writeFileSync( - join(projectDir, ".opencode", "tui.json"), - JSON.stringify({ theme: "dark" }), - "utf8", - ); - - const { inspectTuiConfig } = await import("../src/lib/tui-config-diagnostics.js"); - const diagnostics = await inspectTuiConfig({ cwd: projectDir }); - - expect(diagnostics.workspaceRoot).toBe(projectDir); - expect(diagnostics.configRoot).toBe(projectDir); - expect(diagnostics.configured).toBe(true); - expect(diagnostics.inferredSelectedPath).toBe(join(projectDir, ".opencode", "tui.json")); - expect(diagnostics.presentPaths).toEqual([ - join(globalDir, "tui.json"), - join(projectDir, "tui.jsonc"), - join(projectDir, ".opencode", "tui.json"), - ]); - expect(diagnostics.quotaPluginConfigured).toBe(true); - expect(diagnostics.quotaPluginConfigPaths).toEqual([join(projectDir, "tui.jsonc")]); - }); - - it("supports local file plugin specs that point at dist/tui.js", async () => { - writeFileSync( - join(projectDir, "tui.json"), - JSON.stringify({ - plugin: [ - ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/tui.js", { debug: true }], - ], - }), - "utf8", - ); - - const { inspectTuiConfig } = await import("../src/lib/tui-config-diagnostics.js"); - const diagnostics = await inspectTuiConfig({ cwd: projectDir }); - - expect(diagnostics.quotaPluginConfigured).toBe(true); - expect(diagnostics.quotaPluginConfigPaths).toEqual([join(projectDir, "tui.json")]); - }); - - it("does not treat the server dist/index.js entrypoint as a tui plugin", async () => { - writeFileSync( - join(projectDir, "tui.json"), - JSON.stringify({ - plugin: ["file:///Users/test/Downloads/GitHub/opencode-quota/dist/index.js"], - }), - "utf8", - ); - - const { inspectTuiConfig } = await import("../src/lib/tui-config-diagnostics.js"); - const diagnostics = await inspectTuiConfig({ cwd: projectDir }); - - expect(diagnostics.quotaPluginConfigured).toBe(false); - expect(diagnostics.quotaPluginConfigPaths).toEqual([]); - }); - - it("detects worktree-root tui config when cwd is nested", async () => { - const nestedDir = join(projectDir, "packages", "feature"); - mkdirSync(nestedDir, { recursive: true }); - mkdirSync(join(projectDir, ".git"), { recursive: true }); - mkdirSync(join(projectDir, ".opencode"), { recursive: true }); - - writeFileSync( - join(projectDir, ".opencode", "tui.json"), - JSON.stringify({ plugin: ["@slkiser/opencode-quota"] }), - "utf8", - ); - - const { inspectTuiConfig } = await import("../src/lib/tui-config-diagnostics.js"); - const diagnostics = await inspectTuiConfig({ cwd: nestedDir }); - - expect(diagnostics.workspaceRoot).toBe(projectDir); - expect(diagnostics.configRoot).toBe(projectDir); - expect(diagnostics.inferredSelectedPath).toBe(join(projectDir, ".opencode", "tui.json")); - expect(diagnostics.quotaPluginConfigured).toBe(true); - expect(diagnostics.quotaPluginConfigPaths).toEqual([join(projectDir, ".opencode", "tui.json")]); - }); - - it("resolves relative OPENCODE_CONFIG_DIR from discovered worktree root", async () => { - const nestedDir = join(projectDir, "packages", "feature"); - mkdirSync(nestedDir, { recursive: true }); - mkdirSync(join(projectDir, ".git"), { recursive: true }); - mkdirSync(join(projectDir, ".opencode"), { recursive: true }); - process.env.OPENCODE_CONFIG_DIR = ".opencode"; - - writeFileSync( - join(projectDir, ".opencode", "tui.json"), - JSON.stringify({ plugin: ["@slkiser/opencode-quota"] }), - "utf8", - ); - - const { inspectTuiConfig } = await import("../src/lib/tui-config-diagnostics.js"); - const diagnostics = await inspectTuiConfig({ cwd: nestedDir }); - - expect(diagnostics.workspaceRoot).toBe(projectDir); - expect(diagnostics.configRoot).toBe(join(projectDir, ".opencode")); - expect(diagnostics.inferredSelectedPath).toBe(join(projectDir, ".opencode", "tui.json")); - expect(diagnostics.quotaPluginConfigured).toBe(true); - expect(diagnostics.quotaPluginConfigPaths).toEqual([join(projectDir, ".opencode", "tui.json")]); - }); - - it("honors caller-supplied shared roots instead of rediscovering the git worktree", async () => { - const nestedDir = join(projectDir, "packages", "feature"); - mkdirSync(nestedDir, { recursive: true }); - mkdirSync(join(projectDir, ".git"), { recursive: true }); - mkdirSync(join(projectDir, ".opencode"), { recursive: true }); - - writeFileSync( - join(projectDir, ".opencode", "tui.json"), - JSON.stringify({ plugin: ["@slkiser/opencode-quota"] }), - "utf8", - ); - - const { inspectTuiConfig } = await import("../src/lib/tui-config-diagnostics.js"); - const diagnostics = await inspectTuiConfig({ - cwd: nestedDir, - roots: { - workspaceRoot: nestedDir, - configRoot: nestedDir, - }, - }); - - expect(diagnostics.workspaceRoot).toBe(nestedDir); - expect(diagnostics.configRoot).toBe(nestedDir); - expect(diagnostics.inferredSelectedPath).toBeNull(); - expect(diagnostics.presentPaths).toEqual([]); - expect(diagnostics.candidatePaths).toContain(join(nestedDir, "tui.json")); - expect(diagnostics.candidatePaths).toContain(join(nestedDir, ".opencode", "tui.json")); - expect(diagnostics.candidatePaths).not.toContain(join(projectDir, ".opencode", "tui.json")); - expect(diagnostics.quotaPluginConfigured).toBe(false); - expect(diagnostics.quotaPluginConfigPaths).toEqual([]); - }); - - it("reports missing tui config cleanly", async () => { - const { inspectTuiConfig } = await import("../src/lib/tui-config-diagnostics.js"); - const diagnostics = await inspectTuiConfig({ cwd: projectDir }); - - expect(diagnostics.workspaceRoot).toBe(projectDir); - expect(diagnostics.configRoot).toBe(projectDir); - expect(diagnostics.configured).toBe(false); - expect(diagnostics.inferredSelectedPath).toBeNull(); - expect(diagnostics.presentPaths).toEqual([]); - expect(diagnostics.quotaPluginConfigured).toBe(false); - expect(diagnostics.quotaPluginConfigPaths).toEqual([]); - expect(diagnostics.candidatePaths).toContain(join(globalDir, "tui.json")); - expect(diagnostics.candidatePaths).toContain(join(projectDir, "tui.jsonc")); - expect(diagnostics.candidatePaths).toContain(join(projectDir, ".opencode", "tui.json")); - }); -}); From 65a6d362632b247ef2f4628d0317087bd7582ba3 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:03:10 +0200 Subject: [PATCH 35/97] feat(announcements): announce 5.0 on OpenCode 2 Replace the opencode-2-feedback message with the 5.0 wording: "OpenCode Quota 5.0 runs on OpenCode 2. Ideas for the next version? Tell us!" URL (issue #293), startsAt, and endsAt are unchanged. The id stays "opencode-2-feedback": announcement seen/dismissed state is not persisted anywhere. The TUI only shows the count toast once per session (in-memory flag), so every user sees the new text regardless of the id. --- src/lib/maintainer-announcements.ts | 3 +-- tests/lib.maintainer-announcements.test.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/maintainer-announcements.ts b/src/lib/maintainer-announcements.ts index 2fa54d05..d544bbcd 100644 --- a/src/lib/maintainer-announcements.ts +++ b/src/lib/maintainer-announcements.ts @@ -51,8 +51,7 @@ export const BUNDLED_MAINTAINER_ANNOUNCEMENTS: readonly MaintainerAnnouncement[] }, { id: "opencode-2-feedback", - message: - "OpenCode 2 support is coming in OpenCode Quota 5.0, and 4.10.3 is the last release for OpenCode 1. Tell us what you want from the OpenCode 2 version.", + message: "OpenCode Quota 5.0 runs on OpenCode 2. Ideas for the next version? Tell us!", url: "https://github.com/slkiser/opencode-quota/issues/293", startsAt: "2026-09-25T00:00:00.000Z", endsAt: "2026-11-25T00:00:00.000Z", diff --git a/tests/lib.maintainer-announcements.test.ts b/tests/lib.maintainer-announcements.test.ts index 17deb333..fb42b7c9 100644 --- a/tests/lib.maintainer-announcements.test.ts +++ b/tests/lib.maintainer-announcements.test.ts @@ -28,8 +28,7 @@ const ecosystemAnnouncement = { } satisfies MaintainerAnnouncement; const openCode2FeedbackAnnouncement = { id: "opencode-2-feedback", - message: - "OpenCode 2 support is coming in OpenCode Quota 5.0, and 4.10.3 is the last release for OpenCode 1. Tell us what you want from the OpenCode 2 version.", + message: "OpenCode Quota 5.0 runs on OpenCode 2. Ideas for the next version? Tell us!", url: "https://github.com/slkiser/opencode-quota/issues/293", startsAt: "2026-09-25T00:00:00.000Z", endsAt: "2026-11-25T00:00:00.000Z", From 2ef0f228a4bce80ae21b65693e69006943c07754 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:17:16 +0200 Subject: [PATCH 36/97] docs!: describe OpenCode 2 support Document 5.0.0 on OpenCode 2.0.16+: breaking-changes block, requirement line (Node 22.13+ for the CLI), credentials only from opencode.db, no Web or Desktop quota UI (use `npx @slkiser/opencode-quota show` or the quota_status tool), one opencode.json entry with no tui.json step, removed tuiCommandDisplay and suppressWhenNativeProviderQuota, TUI dialog slash commands, multi-connection rows, and the OpenCode Go Console-first read. Restore the Kimi Code (CN) rows and regional notes, fix OpenCode 2 login commands for OpenCode Go, add a "Moving to OpenCode 2" guide, and update the CONTRIBUTING guardrails to the V2 surfaces. Align two doc-text test assertions with the new README. --- CONTRIBUTING.md | 10 +++--- README.md | 26 ++++++++------ docs/readme/configuration.md | 36 +++++++++---------- docs/readme/manual-install.md | 34 ++++-------------- docs/readme/providers.md | 10 ++++-- docs/readme/troubleshooting.md | 28 ++++++++------- docs/readme/updating.md | 20 ++++++++--- tests/google-gemini-cli-org-only-docs.test.ts | 3 -- tests/package-manifest.test.ts | 2 +- 9 files changed, 84 insertions(+), 85 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf286fe8..e3983451 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,11 +78,11 @@ Recommended settings for `main`: - Never invoke an LLM/model API to compute toast/report output. Everything must remain local and deterministic. - Rich accounting currency quantities must preserve the provider's uppercase ISO code and render through the shared formatter (for example, `USD 12.50`), never a provider-formatted currency string, bare symbol, decorative glyph, conversion, or cross-currency sum. -- The server plugin is the sole owner of deterministic slash commands for TUI and Desktop/server. It registers each `cfg.command` once, injects exactly one ignored/no-reply output message with `session.prompt({ noReply: true, ignored: true })`, and must throw `handled()` so OpenCode does not continue into `prompt(...)`. -- The TUI plugin owns only Sidebar, Compact status, home-bottom, prompt-wrapper, refresh, and resource-lifecycle surfaces. It must not register keymap commands or render native slash-command dialogs. -- Slash commands (`/quota`, `/quota_status`, `/quota_announcements`, `/pricing_refresh`, `/tokens_*`) must route through `buildQuotaDialogCommandOutput()`; do not duplicate command-output logic in `src/plugin.ts`. -- The handled-sentinel path can surface popup/log noise until upstream adds a clean cancellation API; keep docs aligned with anomalyco/opencode#18554 and anomalyco/opencode#18559. -- `injectRawOutput()` is shared by inline slash commands and the server `tool.quota_status` compatibility path. +- OpenCode Quota supports only OpenCode 2. Do not add OpenCode 1 code paths, plugin APIs, or database tables. +- The TUI plugin (`src/tui-v2.tsx`) owns every visible surface: slash commands as local TUI dialogs, toasts, the Sidebar panel, the compact line and prompt bar below the prompt, and the Home footer line. Commands never write to the session transcript or call a model. +- The server plugin (`src/plugin.ts`) only registers the `quota_status` tool, a diagnostics tool the assistant can call. OpenCode 2 gives plugins no Web or Desktop UI hooks, so there are no Web slash commands or toasts. +- Slash commands (`/quota`, `/quota_status`, `/quota_announcements`, `/pricing_refresh`, `/tokens_*`) and the `quota_status` tool must route through `buildQuotaDialogCommandOutput()`; do not duplicate command-output logic in `src/plugin.ts` or `src/tui-v2.tsx`. +- Read OpenCode credentials only from the `credential` table in OpenCode 2's `opencode.db`, never from `auth.json`. - Keep `tests/plugin.command-handled-boundary.test.ts` aligned with these invariants. Additional boundary tests to keep healthy when touching plugin/provider logic: diff --git a/README.md b/README.md index 0a2782d9..8f338561 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ npx @slkiser/opencode-quota init ``` > [!IMPORTANT] -> Node.js `>= 22` is required. +> Requires OpenCode `2.0.16` or newer. Node.js `>= 22.13` is required for `npx @slkiser/opencode-quota ...`. Upgrading from v3? Read the [v4 migration guide](docs/readme/v4-migration.md). After installation: 1. Restart OpenCode. -2. Run a slash command in OpenCode, or use `opencode-quota show` from your terminal. +2. Run `/quota` in the OpenCode TUI, or `opencode-quota show` in a terminal. 3. If you enabled the sidebar, open the session sidebar and look for `Quota`. 4. If you enabled the compact status line, look at the bottom of Home or below the message input. @@ -56,12 +56,13 @@ After installation: The updater prints the complete preview before its own config or cache changes. `--yes` authorizes only the previewed safe config edits and manifest-verified cache cleanup; it never moves or deletes secrets. See [Updating safely](docs/readme/updating.md) for detailed behavior and manual credential steps. -**Breaking changes in 4.10.3:** +**Breaking changes in 5.0.0:** -- Qwen Code was removed because Qwen ended its OAuth free tier; use Alibaba Coding Plan. -- Google Antigravity was removed because its companion plugin is archived and Google rejects it; use Google AGY. -- Personal Google accounts can no longer use Gemini CLI because Google ended them on 2026-06-18; use Google AGY. -- OpenCode Zen now needs the new Console cookie in `consoleSessionCookie`; `authCookie` no longer works. See [OpenCode Zen setup](docs/readme/providers.md#opencode-zen). +- OpenCode 1 is no longer supported. Stay on `4.10.3` if you still use OpenCode 1, or follow [Moving to OpenCode 2](docs/readme/updating.md#moving-to-opencode-2). +- Logins are read only from OpenCode 2's database (`opencode.db`), never from `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. +- Web and Desktop show no quota slash commands, toasts, or panels, because OpenCode 2 gives plugins no Web UI hooks. Run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to run `quota_status`, a diagnostics tool it can call. +- One `"plugin"` entry in `opencode.json` now loads both the server and the TUI; no `tui.json` entry is needed. TUI slash commands open a dialog. +- `tuiCommandDisplay` and `tuiCompactStatus.suppressWhenNativeProviderQuota` were removed. Old keys are ignored. ## Choose your setup @@ -94,7 +95,8 @@ The updater prints the complete preview before its own config or cache changes. More ways to use it: -- Check quota anywhere: use `opencode-quota show` in a terminal or the same slash commands in the TUI, Web, and Desktop. +- Check quota outside the TUI: run `npx @slkiser/opencode-quota show` in a terminal. Web and Desktop have no quota UI; there the assistant can call the `quota_status` diagnostics tool. +- See every OpenCode login: each connection for a provider gets its own rows, and `*` marks the active one. - Automate quota checks with JSON output for scripts, status bars, and CI. Optional OpenTelemetry metrics support monitoring tools. - Customize the display with [`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings), OpenCode Go's preferred collapsed-sidebar window, spaced reset countdowns by default with a `resetTimeSpaced: false` dense opt-out, decimal reset precision, bare percent labels, and [`accountingDetail`](docs/readme/configuration.md#show-accounting-detail). - Optionally estimate **Runs out ≈ 1h 50m** for supported fixed windows with [`quotaProjection: "runway"`](docs/readme/configuration.md#estimate-when-fixed-quota-runs-out). It is off by default and leaves JSON output unchanged. @@ -107,6 +109,8 @@ See [Configuration](docs/readme/configuration.md) for UI options and [Manual ins ### Core slash commands +These run in the OpenCode TUI and show results in a dialog. Type arguments after the command, such as `/tokens_between 2026-09-01 2026-09-25`, or leave them out and a prompt asks for them. + | Command | Use when | | --------------------------------------- | --------------------------------------------------------------- | | `/quota` | Show current quota | @@ -195,6 +199,7 @@ Gemini CLI works only with Gemini Code Assist Standard or Enterprise (organizati | Alibaba Personal Token Plan | [Needs setup](docs/readme/providers.md#alibaba-personal-token-plan) | Official CLI | Quota | | DeepSeek | Automatic | Remote API | Balance and status | | Kimi Code | Automatic | Remote API | Quota | +| Kimi Code (CN) | Automatic | Remote API | Quota | | MiniMax Token Plan | Automatic | Remote API | Quota | | MiniMax Token Plan (CN) | Automatic | Remote API | Quota | | Xiaomi MiMo | [Needs setup](docs/readme/providers.md#xiaomi-mimo) | Dashboard API | Quota and balance | @@ -209,6 +214,7 @@ Gemini CLI works only with Gemini Code Assist Standard or Enterprise (organizati | Provider | Auth/setup | Data from | Reports | | ------------------------ | ---------- | ---------- | ------- | | Kimi Code | Automatic | Remote API | Quota | +| Kimi Code (CN) | Automatic | Remote API | Quota | | MiniMax Token Plan | Automatic | Remote API | Quota | | MiniMax Token Plan (CN) | Automatic | Remote API | Quota | | Zhipu Coding Plan | Automatic | Remote API | Quota | @@ -231,8 +237,8 @@ The guided setup previews the change before saving. See the [custom-provider gui If quota or token data looks wrong: -1. Run `/quota_status` in OpenCode, or `opencode-quota status` from a terminal for the same diagnostics. Use `opencode-quota show` for a quick quota glance. -2. Confirm the expected provider appears in the detected provider list. +1. Run `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal for the same diagnostics. Use `opencode-quota show` for a quick quota glance. +2. Confirm the expected provider appears in the detected provider list. If it is missing, log in to it again in OpenCode 2. 3. Confirm companion auth plugins are before `@slkiser/opencode-quota` in `opencode.json`. 4. If token reports are empty, start OpenCode once so it creates `opencode.db`, then run a session with model usage. 5. Check [Troubleshooting](docs/readme/troubleshooting.md) for common symptoms and provider-specific fixes. diff --git a/docs/readme/configuration.md b/docs/readme/configuration.md index 1cd01935..ca815ee5 100644 --- a/docs/readme/configuration.md +++ b/docs/readme/configuration.md @@ -14,7 +14,7 @@ OpenCode Quota normally keeps its settings in one separate file: Strict `.json` files also work. Run `/quota_status` if you are unsure which file is active. -`opencode.jsonc` loads the main plugin. `tui.jsonc` loads TUI features. Put the settings below in `quota-toast.jsonc`, not `tui.jsonc`. +`opencode.jsonc` loads the plugin for both the server and the TUI. Put the settings below in `quota-toast.jsonc`. ## Common changes @@ -28,8 +28,6 @@ Strict `.json` files also work. Run `/quota_status` if you are unsure which file | Show percentages without `left` or `used` | `percentLabelStyle: "bare"` | | Use dense reset countdown units | `resetTimeSpaced: false` | | Show supplementary accounting facts | `accountingDetail: "detailed"` | -| Show slash results with messages | `tuiCommandDisplay: "inline"` | -| Show slash results in a TUI popup | `tuiCommandDisplay: "dialog"` | | Turn the TUI sidebar on or off | `tuiSidebarPanel.enabled` | | Prefer an OpenCode Go collapsed row | `tuiSidebarPanel.opencodeGoPreferredWindow` | | Turn popup quota notifications on or off | `enableToast` | @@ -58,9 +56,6 @@ The installer chooses `allWindows` by default. If the setting is absent, the bui "resetTimeSpaced": false, "accountingDetail": "summary", - // Keep TUI slash-command results with normal messages. - "tuiCommandDisplay": "inline", - // Show the sidebar and prefer OpenCode Go's Five-hour row while collapsed. "tuiSidebarPanel": { "enabled": true, @@ -89,7 +84,7 @@ This setting is independent of `formatStyle`, which selects quota windows, and ` ### Estimate when fixed quota runs out -`quotaProjection: "runway"` is optional and off by default. It adds a **Runs out** estimate to eligible percentage rows on shared Web/Desktop and TUI `/quota`, terminal `show`, popup toasts, the expanded Sidebar, Compact status, and the prompt bar. +`quotaProjection: "runway"` is optional and off by default. It adds a **Runs out** estimate to eligible percentage rows on TUI `/quota`, terminal `show`, popup toasts, the expanded Sidebar, Compact status, and the prompt bar. ```jsonc { @@ -142,7 +137,7 @@ Session totals use only the current session by default. To include the current s } ``` -This scope applies to the embedded session-token output in `/quota` on Web and TUI, popup toasts, the TUI sidebar, and the compact line below the message input. It does not change `/tokens_session` or `/tokens_session_all`; those commands keep their explicit current-session and session-tree meanings. +This scope applies to the embedded session-token output in TUI `/quota`, popup toasts, the TUI sidebar, and the compact line below the message input. It does not change `/tokens_session` or `/tokens_session_all`; those commands keep their explicit current-session and session-tree meanings. ## Custom providers @@ -303,7 +298,7 @@ Leave it unset to use the default exact-to-minute display.
Choose reset spacing and shorten percent labels -Exact compound countdowns use spaces by default. Set `resetTimeSpaced` to `false` when a dense layout is more important. For example, `2d 5h 14m` becomes `2d5h14m`, and `3h 45m` becomes `3h45m`. Minute-only values such as `14m`, expired values shown as `reset`, and partial-minute rounding stay unchanged. This setting applies to `/quota` in Web, Desktop, and the TUI, popup toasts, terminal `show`, the expanded and collapsed Sidebar, Compact status, and the prompt bar. +Exact compound countdowns use spaces by default. Set `resetTimeSpaced` to `false` when a dense layout is more important. For example, `2d 5h 14m` becomes `2d5h14m`, and `3h 45m` becomes `3h45m`. Minute-only values such as `14m`, expired values shown as `reset`, and partial-minute rounding stay unchanged. This setting applies to TUI `/quota`, popup toasts, terminal `show`, the expanded and collapsed Sidebar, Compact status, and the prompt bar. `resetTimeDecimals` keeps its existing largest-unit decimal format and takes precedence over spacing on the displays where decimal countdowns apply. @@ -377,7 +372,7 @@ Use this only if another tool needs `experimental.quotaToast` mirrored into `ope Most settings go in the same `opencode-quota/quota-toast.jsonc` or `.json` sidecar described above. The guided editor maintains `quotaProviders` in that authoritative sidecar when one exists; otherwise it uses the global OpenCode `experimental.quotaToast` section; do not duplicate it in a second file. -Existing `experimental.quotaToast` settings remain supported. Quota settings do not live in `tui.json`. +Existing `experimental.quotaToast` settings remain supported.
All settings @@ -386,7 +381,7 @@ Existing `experimental.quotaToast` settings remain supported. Quota settings do | Option | Default | Meaning | | ----------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `enabled` | `true` | Master switch for quota collection and handled slash commands. When `false`, `/quota`, `/quota_status`, `/pricing_refresh`, and `/tokens_*` are handled as no-ops. | +| `enabled` | `true` | Master switch for quota collection and slash commands. When `false`, `/quota`, `/quota_status`, `/pricing_refresh`, and `/tokens_*` do nothing. | | `resetNotifications.enabled` | `false` | Emit a one-shot success toast when an observed configured quota window resets. Requires popup toasts to be enabled and adds no provider requests. | | `resetNotifications.windows` | `["weekly"]` | Window classes eligible for reset notifications: `fiveHour`, `hourly`, `daily`, `weekly`, `monthly`, or `yearly`. | | `enabledProviders` | `"auto"` | Auto-detect providers, or set an explicit provider list. Use the aggregate ID `quota-providers` for configured definitions. | @@ -410,10 +405,10 @@ Existing `experimental.quotaToast` settings remain supported. Quota settings do | Option | Default | Meaning | | ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `enableToast` | `true` | Show TUI popup toasts. OpenCode 1.17.20 Web does not surface these events. Disabling this does not disable terminal checks, other UI surfaces, or `/quota`. | +| `enableToast` | `true` | Show TUI popup toasts. Toasts appear only in the TUI, not in Web or Desktop. Disabling this does not disable terminal checks, other UI surfaces, or `/quota`. | | `toastDurationMs` | `9000` | Toast duration in milliseconds. | -| `showOnIdle` | `true` | Show a toast on the idle trigger. | -| `showOnQuestion` | `true` | Show a toast after a question/assistant response. | +| `showOnIdle` | `true` | Show a toast once after each finished reply. | +| `showOnQuestion` | `true` | Show a toast after the assistant's `question` tool finishes. | | `showOnCompact` | `true` | Show a toast after session compaction. | | `showOnBothFail` | `true` | Show a fallback toast when providers attempted quota reads and all failed. | | `layout.maxWidth` | `50` | Toast formatting width target. | @@ -425,14 +420,12 @@ Existing `experimental.quotaToast` settings remain supported. Quota settings do | Option | Default | Meaning | | -------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `tuiCommandDisplay` | `"inline"` | Choose where deterministic native TUI command output appears. `inline` adds an ignored/no-reply plain-text message to the active transcript and uses a dialog on Home; `dialog` always opens the local popup. | | `tuiSidebarPanel.enabled` | `true` | Show the Sidebar `Quota` panel when the TUI plugin is installed. Click the panel header to toggle its collapsed/expanded window layout; OpenCode remembers the last state. This is not an `accountingDetail` override. | | `tuiSidebarPanel.formatStyle` | (root `formatStyle`) | Override `formatStyle` for the Sidebar panel only. Useful when you want `allWindows` detail in the sidebar but a different style elsewhere. | | `tuiSidebarPanel.opencodeGoPreferredWindow` | unset | Prefer `rolling` (Five-hour), `weekly`, or `monthly` for OpenCode Go only while the sidebar is collapsed. If unset or unavailable, the lowest-remaining window is used. Expanded rows still follow `opencodeGoWindows`. | | `tuiCompactStatus.enabled` | `false` | Opt in to Compact status line UI surfaces. | | `tuiCompactStatus.homeBottom` | `true` | Show the Compact status line at the home bottom location. | -| `tuiCompactStatus.sessionPrompt` | `true` | Show the Compact status line by wrapping the TUI session prompt. Disable this if you only want the home-bottom line. | -| `tuiCompactStatus.suppressWhenNativeProviderQuota` | `true` | Hide the Compact status line when OpenCode exposes native provider-quota support. | +| `tuiCompactStatus.sessionPrompt` | `true` | Show the Compact status line below the TUI session prompt. Disable this if you only want the home-bottom line. | | `tuiCompactStatus.maxWidth` | `96` | Maximum Compact status line text width. | | `tuiCompactStatus.formatStyle` | (root `formatStyle`) | Override `formatStyle` for the Compact status line only. Useful when you want `singleWindow` on the compact line while the sidebar shows `allWindows`. | | `tuiPromptBar.enabled` | `false` | Show one opt-in primary quota/accounting result below the TUI prompt and replace the Compact line there. Percentage rows use a provider plus window label such as `OpenAI 5h`. The fill is a fixed 12-cell bar with no width setting; long labels truncate with an ellipsis instead of dropping the provider name. Rich results use the first projected primary row; legacy-only results keep the existing 5h percentage preference. Basis and supplementary rows are omitted. | @@ -442,7 +435,7 @@ Existing `experimental.quotaToast` settings remain supported. Quota settings do | Option | Default | Meaning | | --------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `maintainerAnnouncements.enabled` | `true` | Enable bundled maintainer announcements. | -| `maintainerAnnouncements.home` | `true` | Show the count-only notice on TUI home when the quota TUI plugin is configured, or as a one-shot toast fallback after a visible quota toast when it is not. | +| `maintainerAnnouncements.home` | `true` | Show a one-time count of active notices as a toast after the first visible quota toast. Needs `enableToast`. | ### Provider-specific settings @@ -455,6 +448,13 @@ Existing `experimental.quotaToast` settings remain supported. Quota settings do | `cursorIncludedApiUsd` | unset | Override Cursor monthly included API budget in USD. | | `cursorBillingCycleStartDay` | unset | Local billing-cycle anchor day `1..28`; when unset, Cursor usage resets on the local calendar month. | +Kimi has no `quota-toast.json` credential setting. Each regional plan resolves its own key: + +- **Kimi Code (Global):** `KIMI_GLOBAL_API_KEY` → trusted user/global `provider.kimi-code-plan-global.options.apiKey` → strict `kimi-code-plan-global` API-key entry in OpenCode `opencode.db`. +- **Kimi Code (CN):** `KIMI_CN_API_KEY` → `KIMI_API_KEY` → `KIMI_CODE_API_KEY` → trusted user/global config under `kimi-code-plan-cn`, `kimi-for-coding`, `kimi-code`, or `kimi` → strict API-key entry in `opencode.db` under those same ids. + +Project-local `opencode.json` and `opencode.jsonc` are never read for these secrets. Global credentials are sent only to `api.kimi.ai`; CN and legacy credentials are sent only to `api.kimi.com`. A failed request is not retried against the other host. + Kilo Gateway has no `quota-toast.json` credential setting. Use `KILO_API_KEY`, trusted user/global `provider.kilo.options.apiKey`, or a strict `kilo` API-key entry in OpenCode `opencode.db`; project-local OpenCode config is not read for this secret. See [Kilo Gateway setup](providers.md#kilo-gateway). Ollama Cloud has no `quota-toast.json` credential setting. Use `OLLAMA_API_KEY`, trusted user/global `provider.ollama-cloud.options.apiKey`, or a strict `ollama-cloud` API-key entry in OpenCode `opencode.db`; project-local OpenCode config is not read for this secret. See [Ollama Cloud setup](providers.md#ollama-cloud). diff --git a/docs/readme/manual-install.md b/docs/readme/manual-install.md index 76f7c2f8..f16066f7 100644 --- a/docs/readme/manual-install.md +++ b/docs/readme/manual-install.md @@ -18,9 +18,9 @@ Use this guide only if you want to edit OpenCode files yourself. Use `.jsonc` files if you want comments. Use `.json` files if another tool requires strict JSON. -## 1. Add the main plugin +## 1. Add the plugin -Add OpenCode Quota to `opencode.jsonc` or `opencode.json`. This is required for both TUI and Web: +Add OpenCode Quota to `opencode.jsonc` or `opencode.json`. This one entry loads both the server plugin and the TUI; no `tui.json` entry is needed: ```jsonc { @@ -31,22 +31,7 @@ Add OpenCode Quota to `opencode.jsonc` or `opencode.json`. This is required for Keep any existing plugins and settings. -## 2. Add the TUI plugin - -Skip this step if you use Web only. - -Add OpenCode Quota to `tui.jsonc` or `tui.json`. This enables TUI slash commands, the sidebar, toasts, and the compact line: - -```jsonc -{ - "$schema": "https://opencode.ai/tui.json", - "plugin": ["@slkiser/opencode-quota"], -} -``` - -Keep any existing TUI plugins and settings. - -## 3. Add quota settings +## 2. Add quota settings Create `opencode-quota/quota-toast.jsonc` beside the OpenCode config for your chosen scope: @@ -55,9 +40,6 @@ Create `opencode-quota/quota-toast.jsonc` beside the OpenCode config for your ch // Find providers from OpenCode configuration and authentication. "enabledProviders": "auto", - // Show slash-command results with normal TUI messages. - "tuiCommandDisplay": "inline", - // Show the detailed Quota panel in the TUI sidebar. "tuiSidebarPanel": { "enabled": true, @@ -69,7 +51,7 @@ Create `opencode-quota/quota-toast.jsonc` beside the OpenCode config for your ch "enabled": false, }, - // Show bundled maintainer notices on TUI Home. + // Show a one-time count of bundled maintainer notices after the first quota toast. "maintainerAnnouncements": { "enabled": true, "home": true, @@ -79,7 +61,7 @@ Create `opencode-quota/quota-toast.jsonc` beside the OpenCode config for your ch Use `quota-toast.json` instead if you need strict JSON. Remove the comments and trailing commas. -Restart OpenCode, then run: +Restart OpenCode, then run in the TUI: ```text /quota @@ -90,7 +72,7 @@ Restart OpenCode, then run: ## Change what appears in the TUI -Put these settings in `quota-toast.jsonc`, not `tui.jsonc`. +Put these settings in `quota-toast.jsonc`. | You want | Setting | | --------------------------- | ---------------------------------------- | @@ -98,11 +80,9 @@ Put these settings in `quota-toast.jsonc`, not `tui.jsonc`. | Popup quota notifications | `enableToast: true` | | Compact quota line | `tuiCompactStatus.enabled: true` | | Quota bar under the prompt | `tuiPromptBar.enabled: true` | -| Slash results with messages | `tuiCommandDisplay: "inline"` | -| Slash results in a popup | `tuiCommandDisplay: "dialog"` | | Manual slash commands only | Disable sidebar, toast, and compact line | -Web slash commands always appear with messages. TUI popup dialogs and automatic TUI displays are not available in Web. +TUI slash commands always open a dialog. Web and Desktop show none of these; run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to run its `quota_status` tool. See [Configuration](configuration.md) for more examples and every setting. diff --git a/docs/readme/providers.md b/docs/readme/providers.md index b0f021b8..0508d29b 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -12,7 +12,7 @@ ## Pre-configured providers -Most providers work automatically. `Automatic` means OpenCode Quota reuses the credential saved through OpenCode's `/connect`. If a provider has a “Needs setup” link, open that setup note only if you use that provider. Providers can appear in both audience sections when the vendor supports both. +Most providers work automatically. `Automatic` means OpenCode Quota reuses the credential saved through OpenCode's `/connect`, read from OpenCode 2's `opencode.db`. If you have several logins for one provider, each one gets its own rows and `*` marks the active one. If a provider has a “Needs setup” link, open that setup note only if you use that provider. Providers can appear in both audience sections when the vendor supports both. ### American providers @@ -71,6 +71,7 @@ Business placement describes vendor plan availability. Except for configured Cop | Alibaba Personal Token Plan | [Needs setup](#alibaba-personal-token-plan) | Official CLI | Quota | | DeepSeek | Automatic | Remote API | Balance and status | | Kimi Code | Automatic | Remote API | Quota | +| Kimi Code (CN) | Automatic | Remote API | Quota | | MiniMax Token Plan | Automatic | Remote API | Quota | | MiniMax Token Plan (CN) | Automatic | Remote API | Quota | | Xiaomi MiMo | [Needs setup](#xiaomi-mimo) | Dashboard API | Quota and balance | @@ -85,6 +86,7 @@ Business placement describes vendor plan availability. Except for configured Cop | Provider | Auth/setup | Data from | Reports | | ------------------------ | ---------- | ---------- | ------- | | Kimi Code | Automatic | Remote API | Quota | +| Kimi Code (CN) | Automatic | Remote API | Quota | | MiniMax Token Plan | Automatic | Remote API | Quota | | MiniMax Token Plan (CN) | Automatic | Remote API | Quota | | Zhipu Coding Plan | Automatic | Remote API | Quota | @@ -93,6 +95,8 @@ These vendors offer team or business plans, but the current integrations report
+Kimi plans are region-bound: **Kimi Code** uses `api.kimi.ai`, while **Kimi Code (CN)** uses `api.kimi.com`. A key is sent only to its selected regional host. Legacy provider ids `kimi-for-coding`, `kimi-code`, and `kimi` belong to the CN plan. + The friendly `Quota` label covers quota and rate-limit windows; JSON distinguishes them. ### Rich accounting rows @@ -596,12 +600,12 @@ Project-local `opencode.json` and `opencode.jsonc` files are not read for this s ### OpenCode Go -OpenCode Go reads subscription quota from the official `https://opencode.ai/zen/go/v1/usage` API. OpenCode Quota automatically resolves the API key in this order: +If you are signed in to the OpenCode Console in OpenCode 2, OpenCode Go first reads the Console's `/api/go/status`; if you are not signed in, the sign-in expired, or that call fails, it reads the official `https://opencode.ai/zen/go/v1/usage` API with your API key. If the Console reports no Go subscription, no Go rows appear. OpenCode Quota automatically resolves the API key in this order: 1. `OPENCODE_API_KEY` 2. Trusted user/global OpenCode config: `provider.opencode-go.options.apiKey` 3. Trusted user/global fallback: `provider.opencode.options.apiKey` -4. A strict `opencode-go` API-key entry in OpenCode `opencode.db`: `{ "type": "api", "key": "..." }`. This is the key the OpenCode CLI writes via `opencode auth login -p opencode-go`. +4. A strict `opencode-go` API-key entry in OpenCode `opencode.db`: `{ "type": "api", "key": "..." }`. This is the key the OpenCode CLI writes via `opencode auth login opencode-go`. 5. A strict legacy `opencode` API-key entry in `opencode.db` as the final fallback. Project-local `opencode.json` and `opencode.jsonc` files are not read for this secret. Use `opencodeGoWindows` to choose which validated API results appear across surfaces and in the expanded sidebar: **Five-hour**, **Weekly**, and/or **Monthly**. To keep those rows expanded but prefer one while the sidebar is collapsed, set `tuiSidebarPanel.opencodeGoPreferredWindow` to `rolling`, `weekly`, or `monthly`; an unset or unavailable preference keeps the lowest-remaining selection. These settings do not change authentication or the API request. diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 619e3493..1b8eb315 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -2,31 +2,32 @@ # Troubleshooting -Start with `/quota_status` in OpenCode, or `opencode-quota status` from a terminal. Both show which config, providers, authentication, and local files OpenCode Quota found. +Start with `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal. Both show which config, providers, authentication, and local files OpenCode Quota found. ## First checks -1. Run `/quota_status` in OpenCode, or `opencode-quota status` from a terminal. +1. Run `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal. 2. Find the provider or feature that is failing. 3. Follow the matching fix below. 4. Restart OpenCode after changing config or authentication. -If every provider is missing, confirm OpenCode Quota is listed in `opencode.jsonc` or `.json`. For TUI commands and displays, also confirm it is listed in `tui.jsonc` or `.json`. +If every provider is missing, confirm OpenCode Quota is listed in `opencode.jsonc` or `.json`. That one entry also loads the TUI; no `tui.json` entry is needed. + +OpenCode Quota reads logins only from OpenCode 2's `opencode.db`, never `auth.json`. OpenCode 2 copies `auth.json` once, the first time it starts. If a provider is missing, log in to it again in OpenCode 2. `/quota_status` shows the `opencode.db` path in use; `OPENCODE_DB` and `XDG_DATA_HOME` change it. ## Common problems | Problem | Try this | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Slash commands are missing | Check the plugin entries above, then restart OpenCode. | -| TUI command results appear in the wrong place | Use `tuiCommandDisplay: "inline"` for normal messages or `"dialog"` for a popup. Home always uses a popup because there is no session transcript. | +| Slash commands are missing | They exist only in the TUI. Check the plugin entry above, then restart OpenCode. | | `/quota` shows no providers | Run `/quota_status` or `opencode-quota status`, then check provider detection and authentication. | -| Sidebar is missing | Confirm the TUI plugin is installed and `tuiSidebarPanel.enabled` is `true`. | -| Compact line is missing | Confirm the TUI plugin is installed and `tuiCompactStatus.enabled` is `true`. If needed, check `suppressWhenNativeProviderQuota`. | +| Sidebar is missing | Confirm the plugin is installed and `tuiSidebarPanel.enabled` is `true`. | +| Compact line is missing | Confirm the plugin is installed and `tuiCompactStatus.enabled` is `true`. | | Compact line appears on Home only | Set `tuiCompactStatus.sessionPrompt` to `true`. | -| TUI toast is missing | Check `enableToast`, `showOnIdle`, `showOnQuestion`, and `showOnCompact`. Toasts are not available in Web. | +| TUI toast is missing | Check `enableToast`, `showOnIdle`, `showOnQuestion`, and `showOnCompact`. Toasts appear only in the TUI. | | Token reports are empty | Start OpenCode once, then use a model so `opencode.db` contains usage. | | Pricing looks old | Run `/pricing_refresh`. | -| Web shows `Failed to send command` after correct output | The command already worked. Do not retry. This is a known OpenCode 1.18.2 notification problem; no model was called. | +| Web or Desktop shows no quota | Expected: OpenCode 2 gives plugins no Web UI. Run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to run `quota_status`. | ## Update safely @@ -51,7 +52,7 @@ The updater preserves unrelated settings, comments, and plugins where targeted e | Update result | What to do | | --- | --- | -| Obsolete OpenCode Go source | Configure `OPENCODE_API_KEY`, trusted global `provider.opencode-go.options.apiKey`, fallback `provider.opencode.options.apiKey`, or `opencode auth login -p opencode-go`. Verify it, then manually remove the reported old variable/file. Workspace/cookie material cannot become an API key. | +| Obsolete OpenCode Go source | Configure `OPENCODE_API_KEY`, trusted global `provider.opencode-go.options.apiKey`, fallback `provider.opencode.options.apiKey`, or `opencode auth login opencode-go`. Verify it, then manually remove the reported old variable/file. Workspace/cookie material cannot become an API key. | | Ambiguous OpenCode Zen environment names | Decide whether the names belong to Zen or OpenCode's workspace feature. If they are Zen credentials, create and protect the supported global `opencode-quota/opencode.json` manually. Never paste the values into output or reports. | | Unsupported display migration | Fix the reported invalid, duplicate, or ambiguous config manually. Use root `accountingDetail: "summary"` or `"detailed"`; do not share the rejected value. | | Update race or partial-write failure | No package cache was deleted. Read the error's exact changed-path list, inspect those files, fix the cause, and rerun `update --dry-run` for a fresh plan. Do not restore over concurrent edits blindly. | @@ -174,7 +175,8 @@ These providers use trusted env vars, trusted user/global OpenCode config, or na | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | MiniMax Token Plan | Use `MINIMAX_CODING_PLAN_API_KEY` or `MINIMAX_API_KEY` for the international endpoint. Runtime/config ids like `minimax` and `minimax-coding-plan` use this provider. Repo-local provider secrets are ignored. | | MiniMax Token Plan (CN) | Use `MINIMAX_CHINA_CODING_PLAN_API_KEY` or trusted user/global OpenCode config under `minimax-china-coding-plan`, `minimax-cn-coding-plan`, `minimax-cn`, or `minimax-china`. Runtime id `minimax-cn-coding-plan` uses this provider. | -| Kimi Code | Use `KIMI_API_KEY` or `KIMI_CODE_API_KEY`; repo-local provider secrets are ignored. | +| Kimi Code | Check `kimi:`. Use `KIMI_GLOBAL_API_KEY`, trusted global `provider.kimi-code-plan-global.options.apiKey`, or strict `kimi-code-plan-global` auth. Requests go only to `api.kimi.ai`; repo-local secrets are ignored. | +| Kimi Code (CN) | Check `kimi_cn:`. Use `KIMI_CN_API_KEY`, then `KIMI_API_KEY` or `KIMI_CODE_API_KEY`, or trusted CN/legacy config/auth ids. Requests go only to `api.kimi.com`; repo-local secrets are ignored. | | Chutes AI | Use `CHUTES_API_KEY`, trusted user/global config, or OpenCode auth. | | Synthetic | Use `SYNTHETIC_API_KEY`, trusted user/global config, or OpenCode auth. | | Z.ai Coding Plan | Use `ZAI_API_KEY` or `ZAI_CODING_PLAN_API_KEY`; malformed fallback auth is surfaced as an auth error. | @@ -240,7 +242,7 @@ Run `/quota_status` and check the `xiaomi` section. Diagnostics show state, sour
OpenCode Go -Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*` diagnostics, the selected display windows, normalized API usage, and `live_fetch_error` without exposing the API key. +Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*` diagnostics, the selected display windows, normalized API usage, and `live_fetch_error` without exposing the API key. When you are signed in to the OpenCode Console, `go_source` shows whether the Console (`console`) or the API key (`legacy_key`) answered, and `console_error` shows why the Console failed. `opencode_go_state: not_subscribed` means the Console reports no Go subscription. | Symptom | Fix | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -257,7 +259,7 @@ Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*
Token reports -Run `/quota_status` and check pricing snapshot health plus OpenCode database paths. +Run `/quota_status` and check pricing snapshot health plus the `opencode.db` path. | Symptom | Fix | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------- | diff --git a/docs/readme/updating.md b/docs/readme/updating.md index 8008c531..9c1beadf 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -38,6 +38,16 @@ The updater builds one plan, prints it in full, and then either stops or applies opencode-quota status ``` +## Moving to OpenCode 2 + +`5.0.0` runs only on OpenCode `2.0.16` or newer. + +1. Install OpenCode 2 ([OpenCode docs](https://opencode.ai/docs/)) and start it once. On first start it copies your old `auth.json` logins into its database, `opencode.db`. It does this only once. +2. Run the update steps above. +3. Run `/quota_status` in the OpenCode TUI. If a provider you use is missing, log in to it again in OpenCode 2 (`/connect`, or `opencode auth login `). OpenCode Quota never reads `auth.json`. + +The `opencode.json` plugin entry loads both the server and the TUI; no `tui.json` entry is needed. Web and Desktop have no quota slash commands, toasts, or panels on OpenCode 2. Use `npx @slkiser/opencode-quota show` in a terminal instead. + ## Read the preview The preview can contain three sections: @@ -72,7 +82,7 @@ Unsupported or invalid display values, invalid replacement values, duplicate key ## What stays manual -Credential findings are report-only. The audit detects known obsolete sources by variable-name or file-path presence without retrieving environment values or opening credential files. It never prints, copies, or deletes secret values, and it does not edit environment declarations, shell startup files, `auth.json`, supported credential files, or legacy credential files. +Credential findings are report-only. The audit detects known obsolete sources by variable-name or file-path presence without retrieving environment values or opening credential files. It never prints, copies, or deletes secret values, and it does not edit environment declarations, shell startup files, OpenCode's `opencode.db`, supported credential files, or legacy credential files. ### OpenCode Go findings @@ -81,13 +91,13 @@ OpenCode Go now uses an official API key. Configure one supported source in this 1. `OPENCODE_API_KEY` 2. Trusted user/global OpenCode config: `provider.opencode-go.options.apiKey` 3. Trusted user/global fallback: `provider.opencode.options.apiKey` -4. A strict `opencode-go` API-key entry in OpenCode `auth.json` -5. A strict legacy `opencode` API-key entry in `auth.json` as the final fallback +4. A strict `opencode-go` API-key entry in OpenCode `opencode.db` +5. A strict legacy `opencode` API-key entry in `opencode.db` as the final fallback -You can create the canonical `auth.json` entry with: +You can create the canonical `opencode.db` entry with: ```bash -opencode auth login -p opencode-go +opencode auth login opencode-go ``` Verify the supported key with `/quota_status` or terminal `opencode-quota status`. Only after it works, manually remove obsolete declarations for `OPENCODE_GO_WORKSPACE_ID` and `OPENCODE_GO_AUTH_COOKIE`, plus any obsolete global `opencode-quota/opencode-go.json` file. diff --git a/tests/google-gemini-cli-org-only-docs.test.ts b/tests/google-gemini-cli-org-only-docs.test.ts index 17e63a4c..8757ef6b 100644 --- a/tests/google-gemini-cli-org-only-docs.test.ts +++ b/tests/google-gemini-cli-org-only-docs.test.ts @@ -48,9 +48,6 @@ describe("Gemini CLI organization-only documentation", () => { }); it("states organization-only support and points personal users to Google AGY", () => { - expect(readme).toContain( - "- Personal Google accounts can no longer use Gemini CLI because Google ended them on 2026-06-18; use Google AGY.", - ); expect(readme).toContain(`Gemini CLI works only with ${ORG_ONLY}.`); const providerSection = section(providers, '', ''); diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index 06131012..6de6e806 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -157,7 +157,7 @@ describe("package manifest compatibility", () => { expect(pkg.devDependencies?.["@opencode/plugin"]).toBe("2.0.15"); expect(pkg.dependencies?.["@opentui/core"]).toBe("^0.5.10"); expect(pkg.dependencies?.["@opentui/solid"]).toBe("^0.5.10"); - expect(readme).toContain("Node.js `>= 22` is required."); + expect(readme).toContain("Node.js `>= 22.13` is required"); expect(readme).not.toContain("OpenCode `>= 1.4.3`"); expect(pkg.engines).not.toHaveProperty("opencode"); }); From ed1c948dc43b29d6fba732710f869c003c23175b Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:25:38 +0200 Subject: [PATCH 37/97] fix(tui): restore Home announcements, compact format style and toast debug on OpenCode 2 Wire three documented settings that the V2 TUI ignored, mirroring v4 (b8e47873c src/lib/tui-runtime.ts and src/lib/quota-toast-runtime.ts): - maintainerAnnouncements.home: show the count-only notice on Home again, in the home.footer.status slot above the compact line, using the v4 provider filtering. The V2-only one-time announcement toast is removed; in v4 that toast was only a fallback when the TUI plugin was absent. - tuiCompactStatus.formatStyle: the compact line (Home and below the message input) picks allWindows/singleWindow data like v4. - debug: toasts get the v4 debug footer / debug info, including the "enabledProviders empty" and "no entries" cases. Docs: maintainerAnnouncements.home now describes the Home notice. --- docs/readme/configuration.md | 2 +- src/tui-v2.tsx | 246 +++++++++++++----- tests/plugin.maintainer-announcements.test.ts | 101 +++---- tests/v4-phase5-cross-surface.test.ts | 63 ++++- 4 files changed, 290 insertions(+), 122 deletions(-) diff --git a/docs/readme/configuration.md b/docs/readme/configuration.md index ca815ee5..761be197 100644 --- a/docs/readme/configuration.md +++ b/docs/readme/configuration.md @@ -435,7 +435,7 @@ Existing `experimental.quotaToast` settings remain supported. | Option | Default | Meaning | | --------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `maintainerAnnouncements.enabled` | `true` | Enable bundled maintainer announcements. | -| `maintainerAnnouncements.home` | `true` | Show a one-time count of active notices as a toast after the first visible quota toast. Needs `enableToast`. | +| `maintainerAnnouncements.home` | `true` | Show the count of active notices at the bottom of Home. | ### Provider-specific settings diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 4a2dfb81..573e05fa 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -11,16 +11,24 @@ import { BUNDLED_MAINTAINER_ANNOUNCEMENTS, formatMaintainerAnnouncementHomeCountLine, getMaintainerAnnouncementsSummary, + getMaintainerAnnouncementTargetProviderIds, } from "./lib/maintainer-announcements.js"; +import { getQuotaProviderShape, normalizeQuotaProviderId } from "./lib/provider-metadata.js"; import { buildQuotaDialogCommandOutput, QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, } from "./lib/quota-dialog-commands.js"; import { resolveQuotaFormatStyle } from "./lib/quota-format-style.js"; -import { collectQuotaRenderData } from "./lib/quota-render-data.js"; import { + type CollectQuotaRenderDataResult, + collectConcreteEnabledProviderIds, + collectQuotaRenderData, +} from "./lib/quota-render-data.js"; +import { + createQuotaProviderRuntimeContext, createQuotaRuntimeRequestContext, + type QuotaRuntimeContext, type QuotaSessionModelContext, resolveQuotaRuntimeContext, } from "./lib/quota-runtime-context.js"; @@ -31,6 +39,7 @@ import { resolvePromptBarLabel, } from "./lib/tui-prompt-bar-format.js"; import { buildSidebarQuotaPanelLines } from "./lib/tui-sidebar-format.js"; +import type { QuotaToastConfig } from "./lib/types.js"; const terminalForeground = RGBA.defaultForeground(); @@ -117,15 +126,7 @@ async function getQuotaMessage( context: TuiContext, sessionID: string, surface: "sidebar" | "idle" | "compacted" | "question", -): Promise< - | { - message: string; - duration: number; - activeProviderCount: number; - announcementCount: number; - } - | undefined -> { +): Promise<{ message: string; duration: number; activeProviderCount: number } | undefined> { const runtime = await resolveQuotaRuntimeContext({ client: quotaClient(context), roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, @@ -148,6 +149,26 @@ async function getQuotaMessage( return; } + if ( + surface !== "sidebar" && + config.debug && + config.enabledProviders !== "auto" && + config.enabledProviders.length === 0 + ) { + return { + message: sanitizeDisplayText( + formatQuotaToastDebugInfo({ + trigger: surface, + reason: "enabledProviders empty", + config, + configMeta: runtime.configMeta, + }), + ), + duration: config.toastDurationMs, + activeProviderCount: 0, + }; + } + const rootFormatStyle = resolveQuotaFormatStyle(config.formatStyle); const formatStyle = surface === "sidebar" && config.tuiSidebarPanel.formatStyle @@ -175,43 +196,119 @@ async function getQuotaMessage( }, }).join("\n") : undefined - : data?.entries.length || data?.sessionTokens - ? formatQuotaRows({ - version: "2.0.0", - layout: config.layout, - entries: data?.entries ?? [], - errors: data?.errors ?? [], - style: rootFormatStyle, - percentDisplayMode: config.percentDisplayMode, - resetTimeDecimals: config.resetTimeDecimals, - sessionTokens: data?.sessionTokens, - }) - : config.showOnBothFail && data?.errors.length - ? data.errors.map((error) => `${error.label}: ${error.message}`).join("\n") - : undefined; + : getToastMessage({ trigger: surface, runtime, result, style: rootFormatStyle }); return message ? { message: sanitizeDisplayText(message), duration: config.toastDurationMs, activeProviderCount: result.active.length, - announcementCount: - surface !== "sidebar" && - config.maintainerAnnouncements.enabled && - config.maintainerAnnouncements.home - ? getMaintainerAnnouncementsSummary({ - announcements: BUNDLED_MAINTAINER_ANNOUNCEMENTS, - enabledProviders: result.active.map((provider) => provider.id), - }).activeCount - : 0, } : undefined; } +function getToastMessage(params: { + trigger: string; + runtime: QuotaRuntimeContext; + result: CollectQuotaRenderDataResult; + style: ReturnType; +}): string | undefined { + const config = params.runtime.config; + const { availability, active, hasExplicitProviderIssues, data } = params.result; + const debugInfo = (reason: string) => + formatQuotaToastDebugInfo({ + trigger: params.trigger, + reason, + config, + configMeta: params.runtime.configMeta, + currentModel: params.result.selection?.currentModel, + availability: availability.map((item) => ({ id: item.provider.id, ok: item.ok })), + }); + + if (data?.entries.length || data?.sessionTokens) { + const formatted = formatQuotaRows({ + version: "2.0.0", + layout: config.layout, + entries: data?.entries ?? [], + errors: data?.errors ?? [], + style: params.style, + percentDisplayMode: config.percentDisplayMode, + resetTimeDecimals: config.resetTimeDecimals, + sessionTokens: data?.sessionTokens, + }); + if (!config.debug) return formatted; + const debugFooter = `\n\n[debug] src=${params.runtime.configMeta.source} providers=${config.enabledProviders === "auto" ? "(auto)" : config.enabledProviders.join(",") || "(none)"} avail=${availability + .map((item) => `${item.provider.id}:${item.ok ? "ok" : "no"}`) + .join(" ")}`; + return formatted + debugFooter; + } + + if (config.showOnBothFail && data?.errors.length) { + const errorLines = data.errors.map((error) => `${error.label}: ${error.message}`).join("\n"); + if (!config.debug) return errorLines; + return `${errorLines}\n\n${debugInfo( + hasExplicitProviderIssues ? "providers missing/unavailable" : "all providers failed", + )}`; + } + + if (!config.debug) return undefined; + return debugInfo(active.length === 0 ? "no enabled providers available" : "no entries"); +} + +function formatQuotaToastDebugInfo(params: { + trigger: string; + reason: string; + config: QuotaToastConfig; + configMeta: Pick; + currentModel?: string; + availability?: Array<{ id: string; ok: boolean }>; +}): string { + const availability = params.availability + ? params.availability.map((item) => `${item.id}=${item.ok ? "ok" : "no"}`).join(" ") + : "unknown"; + + const providers = + params.config.enabledProviders === "auto" + ? "(auto)" + : params.config.enabledProviders.length > 0 + ? params.config.enabledProviders.join(",") + : "(none)"; + + const modelPart = params.currentModel ? ` model=${params.currentModel}` : ""; + const paths = params.configMeta.paths.length > 0 ? params.configMeta.paths.join(" | ") : "(none)"; + + return [ + "Quota Toast Debug (opencode-quota)", + `trigger=${params.trigger} reason=${params.reason}`, + `configSource=${params.configMeta.source} paths=${paths}`, + `enabled=${params.config.enabled} providers=${providers}${modelPart}`, + `available=${availability}`, + ].join("\n"); +} + +async function getHomeAnnouncementText(runtime: QuotaRuntimeContext): Promise { + const announcements = BUNDLED_MAINTAINER_ANNOUNCEMENTS; + const targetProviderIds = new Set(getMaintainerAnnouncementTargetProviderIds({ announcements })); + const announcementProviders = runtime.providers.filter((provider) => { + const shape = getQuotaProviderShape(normalizeQuotaProviderId(provider.id)); + return shape ? targetProviderIds.has(shape.id) : false; + }); + const providerIds = await collectConcreteEnabledProviderIds({ + providers: announcementProviders, + ctx: createQuotaProviderRuntimeContext(runtime), + enabledProviders: runtime.config.enabledProviders, + }); + const summary = getMaintainerAnnouncementsSummary({ + enabledProviders: providerIds, + announcements, + }); + return formatMaintainerAnnouncementHomeCountLine(summary.activeCount); +} + async function getQuotaFooter( context: TuiContext, sessionID: string | undefined, surface: "prompt" | "home", -): Promise { +): Promise { const runtime = await resolveQuotaRuntimeContext({ client: quotaClient(context), roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, @@ -220,33 +317,42 @@ async function getQuotaFooter( includeSessionMeta: (config) => config.onlyCurrentModel && surface === "prompt", }); const config = runtime.config; - if (!config.enabled) return ""; + if (!config.enabled) return []; if ( surface === "prompt" && !config.tuiPromptBar.enabled && !(config.tuiCompactStatus.enabled && config.tuiCompactStatus.sessionPrompt) ) - return ""; - if ( + return []; + const announcementEnabled = surface === "home" && - !(config.tuiCompactStatus.enabled && config.tuiCompactStatus.homeBottom) - ) - return ""; - const { data } = await collectQuotaRenderData({ + config.maintainerAnnouncements.enabled && + config.maintainerAnnouncements.home; + const homeCompactEnabled = config.tuiCompactStatus.enabled && config.tuiCompactStatus.homeBottom; + if (surface === "home" && !announcementEnabled && !homeCompactEnabled) return []; + const announcement = announcementEnabled ? await getHomeAnnouncementText(runtime) : ""; + if (surface === "home" && !homeCompactEnabled) return announcement ? [announcement] : []; + const rootFormatStyle = resolveQuotaFormatStyle(config.formatStyle); + const compactFormatStyle = config.tuiCompactStatus.formatStyle + ? resolveQuotaFormatStyle(config.tuiCompactStatus.formatStyle) + : rootFormatStyle; + const result = await collectQuotaRenderData({ client: runtime.client, resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, config, configMeta: runtime.configMeta, request: createQuotaRuntimeRequestContext(runtime), surfaceExplicitProviderIssues: true, + formatStyle: rootFormatStyle, providers: runtime.providers, includeAllWindowsData: true, }); - if (!data) return ""; + const data = result.data; + if (!data) return announcement ? [announcement] : []; if (surface === "prompt" && config.tuiPromptBar.enabled) { const entry = pickPromptBarEntry(data); - if (!entry) return ""; - return sanitizeDisplayText( + if (!entry) return []; + const promptBar = sanitizeDisplayText( [ resolvePromptBarLabel(entry), entry.percentRemaining === undefined @@ -263,14 +369,24 @@ async function getQuotaFooter( .filter(Boolean) .join(" | "), ); + return promptBar ? [promptBar] : []; } - return buildCompactQuotaStatusLine({ - data, - maxWidth: config.tuiCompactStatus.maxWidth, - percentDisplayMode: config.percentDisplayMode, - accountingDetail: config.accountingDetail, - resetTimeSpaced: config.resetTimeSpaced, - }); + const compactData = + compactFormatStyle === "allWindows" && result.allWindowsData + ? result.allWindowsData + : compactFormatStyle === "singleWindow" && result.singleWindowData !== undefined + ? result.singleWindowData + : data; + const compact = compactData + ? buildCompactQuotaStatusLine({ + data: compactData, + maxWidth: config.tuiCompactStatus.maxWidth, + percentDisplayMode: config.percentDisplayMode, + accountingDetail: config.accountingDetail, + resetTimeSpaced: config.resetTimeSpaced, + }) + : ""; + return [announcement, compact].filter(Boolean); } function QuotaFooter(props: { @@ -278,10 +394,10 @@ function QuotaFooter(props: { sessionID?: string; surface: "prompt" | "home"; }): JSX.Element { - const [line, setLine] = createSignal(""); + const [lines, setLines] = createSignal([]); const refresh = () => void getQuotaFooter(props.context, props.sessionID, props.surface) - .then(setLine) + .then(setLines) .catch(reportFailure); refresh(); const stop = props.context.data.on("session.step.ended", (event) => { @@ -289,8 +405,12 @@ function QuotaFooter(props: { }); onCleanup(stop); return ( - - {line()} + + + {lines().map((line) => ( + {line} + ))} + ); } @@ -363,8 +483,7 @@ function SidebarQuotaView(props: { props.setActiveSessionID(props.sessionID); const [open, setOpen] = createSignal(true); const [quota, setQuota] = createSignal< - | { message: string; duration: number; activeProviderCount: number; announcementCount: number } - | undefined + { message: string; duration: number; activeProviderCount: number } | undefined >(undefined); const lines = () => quota()?.message.split("\n") ?? []; const expandable = () => lines().length > 2; @@ -415,7 +534,6 @@ const plugin = Plugin.define({ const api = context as unknown as TuiContext; let disposeEvents: (() => void) | undefined; let activeSessionID: string | undefined; - let announcementsNotified = false; const questionToolCalls = new Set(); const disposeApp = api.ui.slot({ append: "app", @@ -435,18 +553,6 @@ const plugin = Plugin.define({ message: quota.message, duration: quota.duration, }); - if (!announcementsNotified && quota.announcementCount > 0) { - const message = formatMaintainerAnnouncementHomeCountLine(quota.announcementCount); - if (message) { - api.ui.toast.show({ - variant: "info", - title: "OpenCode Quota", - message: sanitizeDisplayText(message), - duration: quota.duration, - }); - announcementsNotified = true; - } - } }) .catch(reportFailure); }; diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 49873a5e..d5e2ded4 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -19,7 +19,7 @@ const TEST_ACCOUNTING = { ownership: "maintained", authority: "provider_reported", } as const; -const ANNOUNCEMENT_TOAST_MESSAGE = +const ANNOUNCEMENT_HOME_MESSAGE = "Notice: Maintainer announcement available. Run /quota_announcements."; const TEST_ANNOUNCEMENT = vi.hoisted(() => ({ @@ -44,6 +44,7 @@ const mocks = vi.hoisted(() => ({ const announcementMocks = vi.hoisted(() => ({ getMaintainerAnnouncementsSummary: vi.fn(), + formatMaintainerAnnouncementHomeCountLine: vi.fn(), })); vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); @@ -59,12 +60,10 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => ); vi.mock("../src/lib/maintainer-announcements.js", () => ({ BUNDLED_MAINTAINER_ANNOUNCEMENTS: [TEST_ANNOUNCEMENT], - formatMaintainerAnnouncementHomeCountLine: (activeCount: number) => { - if (activeCount <= 0) return ""; - if (activeCount === 1) return ANNOUNCEMENT_TOAST_MESSAGE; - return `Notice: ${activeCount} maintainer announcements available. Run /quota_announcements.`; - }, + formatMaintainerAnnouncementHomeCountLine: + announcementMocks.formatMaintainerAnnouncementHomeCountLine, getMaintainerAnnouncementsSummary: announcementMocks.getMaintainerAnnouncementsSummary, + getMaintainerAnnouncementTargetProviderIds: () => ["copilot"], })); function makeAnnouncementSummary(overrides: Record = {}) { @@ -120,8 +119,13 @@ function configureQuestionQuotaToast( } async function startCli() { + vi.stubGlobal("React", { + createElement: (type: unknown, props: Record | null) => + typeof type === "function" ? type(props ?? {}) : { type, props }, + }); const { default: plugin } = await import("../src/tui-v2.js"); const listeners = new Map }) => void>(); + const renderers = new Map unknown>(); const toast = vi.fn(); const context = { location: { directory: process.cwd() }, @@ -136,6 +140,7 @@ async function startCli() { keymap: { layer: vi.fn() }, ui: { slot: vi.fn((claim: { append: string; render: () => unknown }) => { + renderers.set(claim.append, claim.render); if (claim.append === "app") claim.render(); return vi.fn(); }), @@ -143,7 +148,8 @@ async function startCli() { }, }; const dispose = plugin.setup(context as never); - return { listeners, toast, dispose }; + const renderHome = () => renderers.get("home.footer.status")?.(); + return { listeners, toast, renderHome, dispose }; } async function buildAnnouncementsDialogOutput(params: { @@ -189,11 +195,21 @@ describe("maintainer announcement plugin integration", () => { }, resetPluginState: true, }); - announcementMocks.getMaintainerAnnouncementsSummary.mockReturnValue(makeAnnouncementSummary()); + announcementMocks.getMaintainerAnnouncementsSummary + .mockReset() + .mockReturnValue(makeAnnouncementSummary()); + announcementMocks.formatMaintainerAnnouncementHomeCountLine + .mockReset() + .mockImplementation((activeCount: number) => { + if (activeCount <= 0) return ""; + if (activeCount === 1) return ANNOUNCEMENT_HOME_MESSAGE; + return `Notice: ${activeCount} maintainer announcements available. Run /quota_announcements.`; + }); await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); afterEach(async () => { + vi.unstubAllGlobals(); await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); @@ -283,62 +299,51 @@ describe("maintainer announcement plugin integration", () => { ); }); - it("shows one count-only announcement toast after the first visible V2 CLI quota toast", async () => { + it("shows the count-only announcement on Home instead of a toast", async () => { configureQuestionQuotaToast(); const cli = await startCli(); - cli.listeners.get("session.tool.input.started")?.({ - data: { id: "call-1", name: "question", sessionID: "session-question" }, - }); - cli.listeners.get("session.tool.success")?.({ - data: { id: "call-1", sessionID: "session-question" }, - }); - await vi.waitFor(() => expect(cli.toast).toHaveBeenCalledTimes(2)); - expect(cli.toast.mock.calls[0]?.[0].message).toContain("Copilot"); - expect(cli.toast.mock.calls[1]?.[0].message).toBe(ANNOUNCEMENT_TOAST_MESSAGE); - expect(cli.toast.mock.calls[1]?.[0].message).not.toContain(TEST_ANNOUNCEMENT.message); - expect(cli.toast.mock.calls[1]?.[0].message).not.toContain(TEST_ANNOUNCEMENT.id); + cli.renderHome(); + await vi.waitFor(() => + expect(announcementMocks.formatMaintainerAnnouncementHomeCountLine).toHaveReturnedWith( + ANNOUNCEMENT_HOME_MESSAGE, + ), + ); expect(announcementMocks.getMaintainerAnnouncementsSummary).toHaveBeenCalledWith( expect.objectContaining({ enabledProviders: ["copilot"] }), ); + cli.listeners.get("session.tool.input.started")?.({ - data: { id: "call-2", name: "question", sessionID: "session-question" }, + data: { id: "call-1", name: "question", sessionID: "session-question" }, }); cli.listeners.get("session.tool.success")?.({ - data: { id: "call-2", sessionID: "session-question" }, + data: { id: "call-1", sessionID: "session-question" }, }); - await vi.waitFor(() => expect(cli.toast).toHaveBeenCalledTimes(3)); - expect( - cli.toast.mock.calls.filter(([notice]) => notice.message === ANNOUNCEMENT_TOAST_MESSAGE), - ).toHaveLength(1); + await vi.waitFor(() => expect(cli.toast).toHaveBeenCalledOnce()); + await flushMaintainerFallbackWork(); + expect(cli.toast).toHaveBeenCalledOnce(); + expect(cli.toast.mock.calls[0]?.[0].message).toContain("Copilot"); + expect(cli.toast.mock.calls[0]?.[0].message).not.toContain("Notice:"); cli.dispose?.(); }); - it("does not announce before or without a visible V2 CLI quota toast", async () => { - mocks.loadConfig.mockResolvedValue( - makeQuotaToastTestConfig({ + it("does not show the Home announcement when maintainerAnnouncements.home is off", async () => { + configureQuestionQuotaToast({ + maintainerAnnouncements: { enabled: true, home: false }, + tuiCompactStatus: { enabled: true, - enableToast: true, - showOnIdle: false, - showOnQuestion: false, - maintainerAnnouncements: { - enabled: true, - home: true, - }, - }), - ); - - const cli = await startCli(); - cli.listeners.get("session.execution.succeeded")?.({ data: { sessionID: "session-idle" } }); - cli.listeners.get("session.tool.input.started")?.({ - data: { id: "call-1", name: "question", sessionID: "session-question" }, - }); - cli.listeners.get("session.tool.failed")?.({ - data: { id: "call-1", sessionID: "session-question" }, + homeBottom: true, + sessionPrompt: false, + maxWidth: 96, + }, }); - await flushMaintainerFallbackWork(); + const [copilot] = mocks.getProviders(); + const cli = await startCli(); + cli.renderHome(); + // The Home compact line fetches quota after the announcement check would have run. + await vi.waitFor(() => expect(copilot.fetch).toHaveBeenCalled()); expect(announcementMocks.getMaintainerAnnouncementsSummary).not.toHaveBeenCalled(); - expect(cli.toast).not.toHaveBeenCalled(); + expect(announcementMocks.formatMaintainerAnnouncementHomeCountLine).not.toHaveBeenCalled(); cli.dispose?.(); }); }); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index e1724d9c..8f661cf2 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -869,8 +869,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { v2.dispose(); }); - it("renders CN general percentage quota and excludes video on all four surfaces", async () => { - currentConfig = configForSingleProvider("minimax-china-coding-plan"); + async function setupMiniMaxChinaSurfaces() { mocks.loadConfig.mockImplementation(async () => currentConfig); mocks.resolveMiniMaxChinaAuthCached.mockResolvedValue({ state: "configured", @@ -894,8 +893,12 @@ describe("v4 Phase 5 cross-surface release evidence", () => { client.config.providers.mockResolvedValue({ data: { providers: [{ id: "minimax-china-coding-plan" }] }, }); + return setupV2Surfaces(client, ["minimax-china-coding-plan"]); + } - const v2 = await setupV2Surfaces(client, ["minimax-china-coding-plan"]); + it("renders CN general percentage quota and excludes video on all four surfaces", async () => { + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + const v2 = await setupMiniMaxChinaSurfaces(); await v2.quota.run(); const serverOutput = v2.alert.mock.calls[0][0].message; expect(serverOutput).toContain("MiniMax Token Plan"); @@ -935,4 +938,58 @@ describe("v4 Phase 5 cross-surface release evidence", () => { v2.dispose(); }); + + it("honors tuiCompactStatus.formatStyle on the V2 compact lines", async () => { + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + currentConfig.tuiCompactStatus.formatStyle = "singleWindow"; + const v2 = await setupMiniMaxChinaSurfaces(); + + v2.emit("session.execution.succeeded", "minimax-china-session"); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); + const toastOutput = getV2ToastMessage(v2.toast); + expect(toastOutput).toContain("33%"); + expect(toastOutput).toContain("46%"); + + const sessionPromptCompact = await v2.renderSessionPrompt(); + expect(sessionPromptCompact).toContain("33%"); + expect(sessionPromptCompact).not.toContain("46%"); + const homeCompact = await v2.renderHomeBottom(); + expect(homeCompact).toContain("33%"); + expect(homeCompact).not.toContain("46%"); + + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + currentConfig.formatStyle = "singleWindow"; + currentConfig.tuiCompactStatus.formatStyle = "allWindows"; + const allWindowsCompact = await v2.renderHomeBottom(); + expect(allWindowsCompact).toContain("33%"); + expect(allWindowsCompact).toContain("46%"); + + v2.dispose(); + }); + + it("adds toast debug context when debug is enabled", async () => { + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + currentConfig.debug = true; + const v2 = await setupMiniMaxChinaSurfaces(); + + v2.emit("session.execution.succeeded", "minimax-china-session"); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); + const toastOutput = getV2ToastMessage(v2.toast); + expect(toastOutput).toContain("33%"); + expect(toastOutput).toMatch( + /\n\n\[debug\] src=\S+ providers=minimax-china-coding-plan avail=minimax-china-coding-plan:ok$/u, + ); + + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + currentConfig.enabledProviders = []; + currentConfig.debug = true; + v2.emit("session.execution.succeeded", "minimax-china-session"); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(2)); + const emptyProvidersOutput = getV2ToastMessage(v2.toast, 1); + expect(emptyProvidersOutput).toContain("Quota Toast Debug (opencode-quota)"); + expect(emptyProvidersOutput).toContain("trigger=idle reason=enabledProviders empty"); + expect(emptyProvidersOutput).toContain("providers=(none)"); + + v2.dispose(); + }); }); From fd16b9af60ff247de54b3fecb596a1873994b18f Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:30:55 +0200 Subject: [PATCH 38/97] fix(tui): restore reset notifications and quota export on OpenCode 2 Restore quota-reset-notifications.ts and its test from v4 (b8e47873c). Show the reset toast right after the idle/compacted/question quota toast, as the v4 server toast runtime did, and write the export file after each Home footer refresh, as the v4 Home bottom resource did. --- docs/readme/configuration.md | 9 +- docs/readme/external-integration.md | 2 +- src/lib/quota-reset-notifications.ts | 412 ++++++++++++++++++ src/tui-v2.tsx | 68 ++- tests/lib.quota-reset-notifications.test.ts | 453 ++++++++++++++++++++ tests/v4-phase5-cross-surface.test.ts | 91 +++- 6 files changed, 1027 insertions(+), 8 deletions(-) create mode 100644 src/lib/quota-reset-notifications.ts create mode 100644 tests/lib.quota-reset-notifications.test.ts diff --git a/docs/readme/configuration.md b/docs/readme/configuration.md index 761be197..1fcc154b 100644 --- a/docs/readme/configuration.md +++ b/docs/readme/configuration.md @@ -121,9 +121,10 @@ The state file stores pseudonymous SHA-256 identity keys plus remaining percenta observation, and acknowledgment timestamps. Literal account source identifiers, display labels, and credentials are not written to it. -Reset notifications use the server popup-toast surface. They do not appear in slash-command or CLI -output, Sidebar, Compact status, status collection, telemetry, or JSON exports, and require a host -that supports `tui.showToast`. Supported window names are `fiveHour`, `hourly`, `daily`, `weekly`, +Reset notifications appear as a second popup toast right after the quota toast, so they need +`enableToast` and a toast trigger (`showOnIdle`, `showOnCompact`, or `showOnQuestion`). They do not +appear in slash-command or CLI output, Sidebar, Compact status, status collection, telemetry, or +JSON exports. Supported window names are `fiveHour`, `hourly`, `daily`, `weekly`, `monthly`, and `yearly`. ### Include subagent session tokens @@ -469,7 +470,7 @@ Xiaomi MiMo has no `quota-toast.json` credential or endpoint setting. Use `MIMO_ | Option | Default | Meaning | | ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- | -| `export.enabled` | `false` | Write a JSON export file after each TUI background refresh. | +| `export.enabled` | `false` | Write a JSON export file each time the TUI Home screen quota footer refreshes. | | `export.path` | `""` | Export file path. Empty string uses the XDG default: `$XDG_CACHE_HOME/opencode/quota-export.json`. Supports `~/` expansion. | ### Telemetry settings diff --git a/docs/readme/external-integration.md b/docs/readme/external-integration.md index 91a57fc5..53cab60f 100644 --- a/docs/readme/external-integration.md +++ b/docs/readme/external-integration.md @@ -75,7 +75,7 @@ The file is normally written here: If you set `XDG_CACHE_HOME`, the file is written to `$XDG_CACHE_HOME/opencode/quota-export.json` instead. -The TUI refreshes the file about once a minute. A write error is logged, but it does not break the TUI. +The TUI rewrites the file each time the Home screen quota footer refreshes: when Home opens and after each model step while it is open. A write error is logged, but it does not break the TUI. ### tmux example diff --git a/src/lib/quota-reset-notifications.ts b/src/lib/quota-reset-notifications.ts new file mode 100644 index 00000000..c6eacf40 --- /dev/null +++ b/src/lib/quota-reset-notifications.ts @@ -0,0 +1,412 @@ +import { createHash, randomUUID } from "node:crypto"; +import { writeFileSync } from "node:fs"; +import { mkdir, open, readFile, rm, rmdir, stat, unlink } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +import { writeJsonAtomic } from "./atomic-json.js"; +import { isPercentEntry, type QuotaProviderResult, type QuotaToastEntry } from "./entries.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; +import { getQuotaProviderDisplayLabel } from "./provider-metadata.js"; +import { classifyQuotaWindowText, type QuotaWindowKind } from "./quota-entry-display.js"; +import type { QuotaResetWindow } from "./types.js"; + +const STATE_VERSION = 2; +const MAX_TRANSITION_AGE_MS = 24 * 60 * 60 * 1000; +const MAX_OBSERVATION_AGE_MS = 30 * 24 * 60 * 60 * 1000; +const LOCK_RETRY_DELAY_MS = 20; +const LOCK_TIMEOUT_MS = 2_000; +const STALE_LOCK_AGE_MS = 30_000; +const LOCK_OWNER_FILE = "owner.json"; +const LOCK_RECLAIM_FILE = "reclaim"; + +const WINDOW_KINDS: Readonly> = { + fiveHour: "five_hour", + hourly: "hour", + daily: "day", + weekly: "week", + monthly: "month", + yearly: "year", +}; + +type ResetObservation = { + resetAtMs: number; + percentRemaining: number; + observedAtMs: number; + notifiedResetAtMs?: number; +}; + +type ResetNotificationState = { + version: typeof STATE_VERSION; + observations: Record; +}; + +type ResetCandidate = { + current: ResetObservation; + notice: QuotaResetNotice; +}; + +type StateLockOwner = { + token: string; + pid: number; + createdAtMs: number; +}; + +export type QuotaResetProviderResult = { + providerId: string; + result: QuotaProviderResult; +}; + +export type QuotaResetNotice = { + providerId: string; + label: string; + window: QuotaResetWindow; + percentRemaining: number; +}; + +function getStatePath(): string { + return join( + getOpencodeRuntimeDirs().stateDir, + "opencode-quota", + "quota-reset-notifications.json", + ); +} + +function getWindow(entry: QuotaToastEntry): QuotaWindowKind | null { + if (entry.semantic) { + return entry.semantic.metric.kind === "window" ? entry.semantic.metric.window : null; + } + return classifyQuotaWindowText(entry.label ?? "") ?? classifyQuotaWindowText(entry.name); +} + +function getConfiguredWindow( + kind: QuotaWindowKind | null, + configured: readonly QuotaResetWindow[], +): QuotaResetWindow | null { + if (!kind) return null; + return configured.find((window) => WINDOW_KINDS[window] === kind) ?? null; +} + +function getIdentity(providerId: string, entry: QuotaToastEntry, window: QuotaResetWindow): string { + const raw = [ + providerId, + entry.accounting.sourceId ?? "single-source", + window, + entry.accounting.resultType, + ].join("\u001f"); + return createHash("sha256").update(raw).digest("hex"); +} + +function isObservation(value: unknown): value is ResetObservation { + if (!value || typeof value !== "object") return false; + const item = value as Partial; + return ( + Number.isFinite(item.resetAtMs) && + Number.isFinite(item.percentRemaining) && + Number.isFinite(item.observedAtMs) + ); +} + +async function readState(path: string): Promise { + try { + const parsed = JSON.parse(await readFile(path, "utf8")) as Partial; + if (parsed.version !== STATE_VERSION || !parsed.observations) throw new Error("invalid state"); + return { + version: STATE_VERSION, + observations: Object.fromEntries( + Object.entries(parsed.observations).filter((entry) => isObservation(entry[1])), + ), + }; + } catch { + return { version: STATE_VERSION, observations: {} }; + } +} + +function isFileSystemError(error: unknown, code: string): boolean { + return (error as NodeJS.ErrnoException | undefined)?.code === code; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function parseLockOwner(value: string): StateLockOwner | null { + try { + const owner = JSON.parse(value) as Partial; + if ( + typeof owner.token !== "string" || + !Number.isSafeInteger(owner.pid) || + (owner.pid ?? 0) <= 0 || + !Number.isFinite(owner.createdAtMs) + ) { + return null; + } + return owner as StateLockOwner; + } catch { + return null; + } +} + +async function readLockOwner(lockPath: string): Promise { + try { + return parseLockOwner(await readFile(join(lockPath, LOCK_OWNER_FILE), "utf8")); + } catch (error) { + if (isFileSystemError(error, "ENOENT")) return null; + throw error; + } +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return !isFileSystemError(error, "ESRCH"); + } +} + +async function getReclaimStatus( + lockPath: string, +): Promise<{ reclaimable: boolean; allowMissingOwner: boolean }> { + const owner = await readLockOwner(lockPath); + if (owner) return { reclaimable: !isProcessAlive(owner.pid), allowMissingOwner: false }; + + try { + const lock = await stat(lockPath); + const stale = Date.now() - lock.mtimeMs > STALE_LOCK_AGE_MS; + return { reclaimable: stale, allowMissingOwner: stale }; + } catch (error) { + if (isFileSystemError(error, "ENOENT")) { + return { reclaimable: true, allowMissingOwner: true }; + } + throw error; + } +} + +async function tryReclaimStateLock(lockPath: string, allowMissingOwner: boolean): Promise { + const reclaimPath = join(lockPath, LOCK_RECLAIM_FILE); + try { + const handle = await open(reclaimPath, "wx", 0o600); + await handle.close(); + } catch (error) { + if (isFileSystemError(error, "ENOENT")) return true; + if (!isFileSystemError(error, "EEXIST")) throw error; + + try { + const reclaim = await stat(reclaimPath); + if (Date.now() - reclaim.mtimeMs <= STALE_LOCK_AGE_MS) return false; + } catch (statError) { + if (isFileSystemError(statError, "ENOENT")) return false; + throw statError; + } + + const owner = await readLockOwner(lockPath); + if (owner && isProcessAlive(owner.pid)) { + await unlink(reclaimPath).catch(() => undefined); + return false; + } + if (!owner && !allowMissingOwner) return false; + await rm(lockPath, { recursive: true, force: true }); + return true; + } + + const owner = await readLockOwner(lockPath); + const reclaimable = owner ? !isProcessAlive(owner.pid) : allowMissingOwner; + if (reclaimable) { + await rm(lockPath, { recursive: true, force: true }); + return true; + } + + await unlink(reclaimPath).catch((error: unknown) => { + if (!isFileSystemError(error, "ENOENT")) throw error; + }); + return false; +} + +async function acquireStateLock(statePath: string): Promise<() => Promise> { + await mkdir(dirname(statePath), { recursive: true, mode: 0o700 }); + const lockPath = `${statePath}.lock`; + const token = randomUUID(); + const deadline = Date.now() + LOCK_TIMEOUT_MS; + + while (true) { + try { + await mkdir(lockPath, { mode: 0o700 }); + const owner = { token, pid: process.pid, createdAtMs: Date.now() } satisfies StateLockOwner; + try { + writeFileSync(join(lockPath, LOCK_OWNER_FILE), JSON.stringify(owner), { + flag: "wx", + mode: 0o600, + }); + } catch (error) { + await rmdir(lockPath).catch(() => undefined); + throw error; + } + + return async () => { + const currentOwner = await readLockOwner(lockPath); + if (currentOwner?.token !== token) return; + await rm(lockPath, { recursive: true, force: true }); + }; + } catch (error) { + if (!isFileSystemError(error, "EEXIST")) throw error; + } + + const status = await getReclaimStatus(lockPath); + if (status.reclaimable && (await tryReclaimStateLock(lockPath, status.allowMissingOwner))) { + continue; + } + + if (Date.now() >= deadline) { + throw new Error(`Timed out acquiring quota reset notification state lock: ${lockPath}`); + } + await delay(LOCK_RETRY_DELAY_MS); + } +} + +function didReset(previous: ResetObservation, current: ResetObservation, nowMs: number): boolean { + const transitionAge = nowMs - previous.resetAtMs; + return ( + previous.observedAtMs <= previous.resetAtMs && + transitionAge >= 0 && + transitionAge <= MAX_TRANSITION_AGE_MS && + current.resetAtMs > previous.resetAtMs && + current.resetAtMs > nowMs && + current.percentRemaining > previous.percentRemaining && + previous.percentRemaining < 100 && + previous.notifiedResetAtMs !== previous.resetAtMs + ); +} + +function updateObservation( + previous: ResetObservation | undefined, + current: ResetObservation, + nowMs: number, +): { observation: ResetObservation; notify: boolean } { + if (!previous) return { observation: current, notify: false }; + if (current.resetAtMs < previous.resetAtMs || current.observedAtMs < previous.observedAtMs) { + return { observation: previous, notify: false }; + } + + if (current.resetAtMs === previous.resetAtMs) { + if (current.resetAtMs <= nowMs && previous.observedAtMs <= previous.resetAtMs) { + return { observation: previous, notify: false }; + } + return { + observation: { ...current, notifiedResetAtMs: previous.notifiedResetAtMs }, + notify: false, + }; + } + + if (nowMs < previous.resetAtMs) return { observation: previous, notify: false }; + + const notify = didReset(previous, current, nowMs); + return { + observation: notify ? { ...current, notifiedResetAtMs: previous.resetAtMs } : current, + notify, + }; +} + +function collectCandidates(params: { + providers: QuotaResetProviderResult[]; + windows: readonly QuotaResetWindow[]; + nowMs: number; +}): Map { + const candidates = new Map(); + + for (const provider of params.providers) { + for (const entry of provider.result.entries) { + if (!isPercentEntry(entry)) continue; + if (entry.accounting.resultType !== "quota" && entry.accounting.resultType !== "rate_limit") { + continue; + } + if (!entry.resetTimeIso) continue; + + const window = getConfiguredWindow(getWindow(entry), params.windows); + if (!window) continue; + const resetAtMs = Date.parse(entry.resetTimeIso); + if (!Number.isFinite(resetAtMs)) continue; + + const key = getIdentity(provider.providerId, entry, window); + const candidate: ResetCandidate = { + current: { + resetAtMs, + percentRemaining: entry.percentRemaining, + observedAtMs: params.nowMs, + }, + notice: { + providerId: provider.providerId, + label: entry.group?.trim() || getQuotaProviderDisplayLabel(provider.providerId), + window, + percentRemaining: entry.percentRemaining, + }, + }; + const matching = candidates.get(key); + if (matching) matching.push(candidate); + else candidates.set(key, [candidate]); + } + } + + return candidates; +} + +export async function observeQuotaResetNotifications(params: { + providers: QuotaResetProviderResult[]; + windows: readonly QuotaResetWindow[]; + nowMs?: number; + statePath?: string; +}): Promise { + const nowMs = params.nowMs ?? Date.now(); + const statePath = params.statePath ?? getStatePath(); + const candidates = collectCandidates({ + providers: params.providers, + windows: params.windows, + nowMs, + }); + const releaseLock = await acquireStateLock(statePath); + + try { + const state = await readState(statePath); + const observations = Object.fromEntries( + Object.entries(state.observations).filter( + ([, observation]) => nowMs - observation.observedAtMs <= MAX_OBSERVATION_AGE_MS, + ), + ); + const notices: QuotaResetNotice[] = []; + + for (const [key, matching] of candidates) { + if (matching.length !== 1) { + delete observations[key]; + continue; + } + const candidate = matching[0]; + if (!candidate) continue; + const update = updateObservation(observations[key], candidate.current, nowMs); + observations[key] = update.observation; + if (update.notify) notices.push(candidate.notice); + } + + await writeJsonAtomic( + statePath, + { version: STATE_VERSION, observations } satisfies ResetNotificationState, + { trailingNewline: true, directoryMode: 0o700, fileMode: 0o600 }, + ); + return notices; + } finally { + await releaseLock(); + } +} + +export function formatQuotaResetNotification(notices: readonly QuotaResetNotice[]): string | null { + if (notices.length === 0) return null; + const labels = [...new Set(notices.map((notice) => notice.label))]; + const shown = labels.slice(0, 3); + const overflow = labels.length - shown.length; + const providers = `${shown.join(", ")}${overflow > 0 ? ` and ${overflow} more` : ""}`; + + const [notice] = notices; + if (notice && notices.length === 1) { + const window = notice.window === "weekly" ? "Weekly" : "Quota"; + return `${window} quota reset: ${providers} is available again (${Math.round(notice.percentRemaining)}% remaining).`; + } + return `Quota reset: ${providers} are available again.`; +} diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 573e05fa..ab42f738 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -19,12 +19,22 @@ import { QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, } from "./lib/quota-dialog-commands.js"; +import { + buildQuotaExport, + createExportProviderContext, + resolveExportPath, + writeQuotaExport, +} from "./lib/quota-export.js"; import { resolveQuotaFormatStyle } from "./lib/quota-format-style.js"; import { type CollectQuotaRenderDataResult, collectConcreteEnabledProviderIds, collectQuotaRenderData, } from "./lib/quota-render-data.js"; +import { + formatQuotaResetNotification, + observeQuotaResetNotifications, +} from "./lib/quota-reset-notifications.js"; import { createQuotaProviderRuntimeContext, createQuotaRuntimeRequestContext, @@ -126,7 +136,10 @@ async function getQuotaMessage( context: TuiContext, sessionID: string, surface: "sidebar" | "idle" | "compacted" | "question", -): Promise<{ message: string; duration: number; activeProviderCount: number } | undefined> { +): Promise< + | { message: string; duration: number; activeProviderCount: number; resetNotification?: string } + | undefined +> { const runtime = await resolveQuotaRuntimeContext({ client: quotaClient(context), roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, @@ -184,6 +197,23 @@ async function getQuotaMessage( formatStyle, providers: runtime.providers, }); + let resetNotification: string | undefined; + if ( + surface !== "sidebar" && + config.resetNotifications.enabled && + result.providerResults.length > 0 + ) { + try { + const notices = await observeQuotaResetNotifications({ + providers: result.providerResults, + windows: config.resetNotifications.windows, + }); + resetNotification = formatQuotaResetNotification(notices) ?? undefined; + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + console.warn(`[opencode-quota] failed to observe quota reset transitions: ${reason}`); + } + } const data = result.data; const message = surface === "sidebar" @@ -202,6 +232,7 @@ async function getQuotaMessage( message: sanitizeDisplayText(message), duration: config.toastDurationMs, activeProviderCount: result.active.length, + resetNotification, } : undefined; } @@ -397,7 +428,15 @@ function QuotaFooter(props: { const [lines, setLines] = createSignal([]); const refresh = () => void getQuotaFooter(props.context, props.sessionID, props.surface) - .then(setLines) + .then((next) => { + setLines(next); + if (props.surface !== "home") return; + // Fire-and-forget: write the export file if enabled. A failed write + // must never affect rendering, so log a warning and continue. + void writeQuotaExportIfEnabled(props.context).catch((error) => { + console.warn(`[opencode-quota] quota export write failed: ${String(error)}`); + }); + }) .catch(reportFailure); refresh(); const stop = props.context.data.on("session.step.ended", (event) => { @@ -415,6 +454,23 @@ function QuotaFooter(props: { ); } +/** Writes the quota export file if `config.export.enabled` is true. Errors propagate. */ +async function writeQuotaExportIfEnabled(context: TuiContext): Promise { + const runtime = await resolveQuotaRuntimeContext({ + client: quotaClient(context), + roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, + }); + if (!runtime.config.enabled || !runtime.config.export.enabled) return; + + const exportData = await buildQuotaExport({ + providers: runtime.providers, + ctx: createExportProviderContext(runtime), + ttlMs: runtime.config.minIntervalMs, + fromCache: true, + }); + await writeQuotaExport(exportData, resolveExportPath(runtime.config.export.path)); +} + function reportFailure(error: unknown): void { const message = error instanceof Error ? error.message : String(error); console.warn(`[opencode-quota] failed to load quota: ${message}`); @@ -553,6 +609,14 @@ const plugin = Plugin.define({ message: quota.message, duration: quota.duration, }); + if (quota.resetNotification) { + api.ui.toast.show({ + variant: "success", + title: "Quota available", + message: sanitizeDisplayText(quota.resetNotification), + duration: quota.duration, + }); + } }) .catch(reportFailure); }; diff --git a/tests/lib.quota-reset-notifications.test.ts b/tests/lib.quota-reset-notifications.test.ts new file mode 100644 index 00000000..7421f13a --- /dev/null +++ b/tests/lib.quota-reset-notifications.test.ts @@ -0,0 +1,453 @@ +import { spawn } from "node:child_process"; +import { once } from "node:events"; +import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import type { QuotaResetProviderResult } from "../src/lib/quota-reset-notifications.js"; +import { + formatQuotaResetNotification, + observeQuotaResetNotifications, +} from "../src/lib/quota-reset-notifications.js"; + +const created: string[] = []; + +async function statePath(): Promise { + const root = await mkdtemp(join(tmpdir(), "opencode-quota-reset-")); + created.push(root); + return join(root, "state.json"); +} + +function provider(params: { + percentRemaining: number; + resetAtMs: number; + providerId?: string; + sourceId?: string; + name?: string; + group?: string; + label?: string; + resultType?: "quota" | "usage"; +}): QuotaResetProviderResult { + const providerId = params.providerId ?? "openai"; + const name = params.name ?? (providerId === "openai" ? "OpenAI" : "Anthropic"); + return { + providerId, + result: { + attempted: true, + errors: [], + entries: [ + { + name, + group: params.group ?? (params.sourceId ? `${name} (${params.sourceId})` : name), + label: params.label ?? "7d", + percentRemaining: params.percentRemaining, + resetTimeIso: new Date(params.resetAtMs).toISOString(), + accounting: { + resultType: params.resultType ?? "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + sourceId: params.sourceId, + }, + }, + ], + }, + }; +} + +afterEach(async () => { + await Promise.all(created.splice(0).map((path) => rm(path, { recursive: true, force: true }))); +}); + +describe("quota reset notifications", () => { + it("uses the first observation as a baseline and persists one acknowledgement", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const firstReset = start + 60 * 60 * 1000; + const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; + + expect( + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 12, resetAtMs: firstReset })], + windows: ["weekly"], + nowMs: start, + statePath: path, + }), + ).toEqual([]); + + const notices = await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 100, resetAtMs: nextReset })], + windows: ["weekly"], + nowMs: firstReset + 60_000, + statePath: path, + }); + expect(notices).toEqual([ + { + providerId: "openai", + label: "OpenAI", + window: "weekly", + percentRemaining: 100, + }, + ]); + expect(formatQuotaResetNotification(notices)).toBe( + "Weekly quota reset: OpenAI is available again (100% remaining).", + ); + + expect( + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 100, resetAtMs: nextReset })], + windows: ["weekly"], + nowMs: firstReset + 120_000, + statePath: path, + }), + ).toEqual([]); + }); + + it("reclaims a lock left by an exited process", async () => { + const path = await statePath(); + const lockPath = `${path}.lock`; + await mkdir(lockPath, { mode: 0o700 }); + const exitedOwner = spawn(process.execPath, ["-e", ""]); + const exitedPid = exitedOwner.pid; + if (!exitedPid) throw new Error("Failed to start lock-owner test process"); + await once(exitedOwner, "exit"); + await writeFile( + join(lockPath, "owner.json"), + JSON.stringify({ token: "exited-owner", pid: exitedPid, createdAtMs: Date.now() }), + ); + + const start = Date.UTC(2026, 0, 1, 12); + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 10, resetAtMs: start + 60 * 60 * 1000 })], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + expect(Object.keys(JSON.parse(await readFile(path, "utf8")).observations)).toHaveLength(1); + await expect(stat(lockPath)).rejects.toMatchObject({ code: "ENOENT" }); + }); + + it("serializes concurrent observers so the same reset is notified once", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const firstReset = start + 60 * 60 * 1000; + const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 10, resetAtMs: firstReset })], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + const results = await Promise.all( + [1, 2].map(() => + observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 100, resetAtMs: nextReset })], + windows: ["weekly"], + nowMs: firstReset + 60_000, + statePath: path, + }), + ), + ); + + expect(results.flat()).toHaveLength(1); + }); + + it("preserves disjoint observations written concurrently", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const reset = start + 60 * 60 * 1000; + + await Promise.all( + ["work@example.com", "personal@example.com"].map((sourceId) => + observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 5, resetAtMs: reset, sourceId })], + windows: ["weekly"], + nowMs: start, + statePath: path, + }), + ), + ); + + const state = JSON.parse(await readFile(path, "utf8")); + expect(Object.keys(state.observations)).toHaveLength(2); + }); + + it("preserves a pre-boundary baseline while a provider still reports the expired reset", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const firstReset = start + 60 * 60 * 1000; + const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 10, resetAtMs: firstReset })], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 10, resetAtMs: firstReset })], + windows: ["weekly"], + nowMs: firstReset + 60_000, + statePath: path, + }); + + expect( + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 100, resetAtMs: nextReset })], + windows: ["weekly"], + nowMs: firstReset + 120_000, + statePath: path, + }), + ).toHaveLength(1); + }); + + it("keeps identity stable when presentation labels change", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const firstReset = start + 60 * 60 * 1000; + const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; + await observeQuotaResetNotifications({ + providers: [ + provider({ + percentRemaining: 10, + resetAtMs: firstReset, + sourceId: "account-1", + name: "Old provider name", + group: "Old account label", + label: "7d", + }), + ], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + const notices = await observeQuotaResetNotifications({ + providers: [ + provider({ + percentRemaining: 100, + resetAtMs: nextReset, + sourceId: "account-1", + name: "New provider name", + group: "New account label", + label: "weekly", + }), + ], + windows: ["weekly"], + nowMs: firstReset + 60_000, + statePath: path, + }); + + expect(notices).toEqual([ + expect.objectContaining({ providerId: "openai", label: "New account label" }), + ]); + }); + + it("skips ambiguous rows instead of attributing one series to another", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const reset = start + 60 * 60 * 1000; + + await observeQuotaResetNotifications({ + providers: [ + provider({ percentRemaining: 5, resetAtMs: reset, name: "First" }), + provider({ percentRemaining: 15, resetAtMs: reset, name: "Second" }), + ], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + expect(JSON.parse(await readFile(path, "utf8")).observations).toEqual({}); + }); + + it("aggregates providers that reset together", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const firstReset = start + 60 * 60 * 1000; + const nextReset = firstReset + 7 * 24 * 60 * 60 * 1000; + const initial = [ + provider({ percentRemaining: 5, resetAtMs: firstReset, providerId: "openai" }), + provider({ percentRemaining: 15, resetAtMs: firstReset, providerId: "anthropic" }), + ]; + await observeQuotaResetNotifications({ + providers: initial, + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + const notices = await observeQuotaResetNotifications({ + providers: [ + provider({ percentRemaining: 100, resetAtMs: nextReset, providerId: "openai" }), + provider({ percentRemaining: 100, resetAtMs: nextReset, providerId: "anthropic" }), + ], + windows: ["weekly"], + nowMs: firstReset + 60_000, + statePath: path, + }); + + expect(notices).toHaveLength(2); + expect(formatQuotaResetNotification(notices)).toBe( + "Quota reset: OpenAI, Anthropic are available again.", + ); + }); + + it("does not notify before the previous reset is crossed or when quota did not improve", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const firstReset = start + 60 * 60 * 1000; + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 25, resetAtMs: firstReset })], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + expect( + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 100, resetAtMs: firstReset + 604_800_000 })], + windows: ["weekly"], + nowMs: start + 30 * 60 * 1000, + statePath: path, + }), + ).toEqual([]); + }); + + it("limits structured reset observations to typed-window quota and rate-limit percentages", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const reset = start + 60 * 60 * 1000; + const accounting = { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + } as const; + + await observeQuotaResetNotifications({ + providers: [ + { + providerId: "openai", + result: { + attempted: true, + errors: [], + entries: [ + { + kind: "quantity", + accounting: { ...accounting, resultType: "balance" }, + semantic: { + metric: { kind: "component", component: "current_balance" }, + prominence: "primary", + }, + name: "Balance", + quantity: { decimal: "10", unit: { kind: "currency", code: "USD" } }, + resetTimeIso: new Date(reset).toISOString(), + }, + { + kind: "boolean", + accounting: { ...accounting, resultType: "status" }, + semantic: { + metric: { kind: "component", component: "auto_reload" }, + prominence: "supplementary", + }, + name: "Auto-reload", + value: true, + resetTimeIso: new Date(reset).toISOString(), + }, + { + accounting: { ...accounting, resultType: "budget" }, + semantic: { metric: { kind: "window", window: "week" }, prominence: "primary" }, + name: "Budget", + percentRemaining: 10, + resetTimeIso: new Date(reset).toISOString(), + }, + { + accounting, + semantic: { metric: { kind: "aggregate" }, prominence: "primary" }, + name: "Aggregate quota", + label: "Weekly:", + percentRemaining: 10, + resetTimeIso: new Date(reset).toISOString(), + }, + { + accounting: { ...accounting, sourceId: "quota" }, + semantic: { metric: { kind: "window", window: "week" }, prominence: "primary" }, + name: "Typed quota", + label: "Monthly:", + percentRemaining: 10, + resetTimeIso: new Date(reset).toISOString(), + }, + { + accounting: { ...accounting, resultType: "rate_limit", sourceId: "rate" }, + semantic: { metric: { kind: "window", window: "week" }, prominence: "primary" }, + name: "Typed rate limit", + label: "Monthly:", + percentRemaining: 20, + resetTimeIso: new Date(reset).toISOString(), + }, + { + accounting: { ...accounting, sourceId: "monthly" }, + semantic: { metric: { kind: "window", window: "month" }, prominence: "primary" }, + name: "Typed monthly quota", + label: "Weekly:", + percentRemaining: 30, + resetTimeIso: new Date(reset).toISOString(), + }, + ], + }, + }, + ], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + expect(Object.keys(JSON.parse(await readFile(path, "utf8")).observations)).toHaveLength(2); + }); + + it("filters window and accounting types", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const reset = start + 60 * 60 * 1000; + + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 10, resetAtMs: reset, label: "Monthly" })], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + await observeQuotaResetNotifications({ + providers: [provider({ percentRemaining: 10, resetAtMs: reset, resultType: "usage" })], + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + expect(JSON.parse(await readFile(path, "utf8")).observations).toEqual({}); + }); + + it("keeps accounts independent without persisting source identifiers", async () => { + const path = await statePath(); + const start = Date.UTC(2026, 0, 1, 12); + const reset = start + 60 * 60 * 1000; + const sources = ["work@example.com", "personal@example.com"]; + + await observeQuotaResetNotifications({ + providers: sources.map((sourceId) => + provider({ percentRemaining: 5, resetAtMs: reset, sourceId }), + ), + windows: ["weekly"], + nowMs: start, + statePath: path, + }); + + const state = await readFile(path, "utf8"); + expect(Object.keys(JSON.parse(state).observations)).toHaveLength(2); + expect(state).not.toContain("work@example.com"); + expect(state).not.toContain("personal@example.com"); + }); +}); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 8f661cf2..c3960e6c 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -1,4 +1,5 @@ -import { rm } from "node:fs/promises"; +import { readFile, rm } from "node:fs/promises"; +import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { @@ -71,6 +72,12 @@ const otel = vi.hoisted(() => { }; }); +const resetNotifications = vi.hoisted(() => ({ observe: vi.fn() })); +vi.mock("../src/lib/quota-reset-notifications.js", async (importOriginal) => ({ + ...(await importOriginal()), + observeQuotaResetNotifications: resetNotifications.observe, +})); + vi.mock("@opentelemetry/api", () => ({ metrics: { getMeter: otel.getMeter }, })); @@ -353,6 +360,8 @@ describe("v4 Phase 5 cross-surface release evidence", () => { process.env.PHASE5_OPENROUTER_KEY = PHASE5_SECRET_CANARIES.openRouterKey; process.env.PHASE5_FAILING_KEY = PHASE5_SECRET_CANARIES.failingKey; + resetNotifications.observe.mockReset(); + resetNotifications.observe.mockResolvedValue([]); currentConfig = configFor("allWindows"); seedDefaultPluginBootstrapMocks(mocks, { configOverrides: currentConfig, @@ -992,4 +1001,84 @@ describe("v4 Phase 5 cross-surface release evidence", () => { v2.dispose(); }); + + it("shows a reset notification toast after the quota toast when a window resets", async () => { + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + currentConfig.resetNotifications = { enabled: true, windows: ["weekly"] }; + resetNotifications.observe.mockResolvedValueOnce([ + { + providerId: "minimax-china-coding-plan", + label: "MiniMax Token Plan (CN)", + window: "weekly", + percentRemaining: 46, + }, + ]); + const v2 = await setupMiniMaxChinaSurfaces(); + + v2.emit("session.execution.succeeded", "minimax-china-session"); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(2)); + expect(resetNotifications.observe).toHaveBeenCalledOnce(); + const observed = resetNotifications.observe.mock.calls[0][0]; + expect(observed.windows).toEqual(["weekly"]); + expect(observed.providers.map((item: { providerId: string }) => item.providerId)).toEqual([ + "minimax-china-coding-plan", + ]); + expect(v2.toast.mock.calls[0][0]).toMatchObject({ variant: "info", title: "OpenCode Quota" }); + expect(v2.toast.mock.calls[1][0]).toEqual({ + variant: "success", + title: "Quota available", + message: "Weekly quota reset: MiniMax Token Plan (CN) is available again (46% remaining).", + duration: currentConfig.toastDurationMs, + }); + + v2.emit("session.execution.succeeded", "minimax-china-session"); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(3)); + expect(resetNotifications.observe).toHaveBeenCalledTimes(2); + expect(v2.toast.mock.calls[2][0]).toMatchObject({ variant: "info" }); + + v2.dispose(); + }); + + it("does not observe resets when reset notifications are disabled", async () => { + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + const v2 = await setupMiniMaxChinaSurfaces(); + + v2.emit("session.execution.succeeded", "minimax-china-session"); + await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); + await v2.renderSidebar("minimax-china-session"); + expect(resetNotifications.observe).not.toHaveBeenCalled(); + + v2.dispose(); + }); + + it("writes the quota export file after the Home footer refreshes", async () => { + const exportPath = join(TEST_RUNTIME_ROOT, "export", "quota-export.json"); + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + currentConfig.export = { enabled: true, path: exportPath }; + const v2 = await setupMiniMaxChinaSurfaces(); + + const homeCompact = await v2.renderHomeBottom(); + expect(homeCompact).toContain("33%"); + const exported = await vi.waitFor(async () => JSON.parse(await readFile(exportPath, "utf8"))); + expect(exported.version).toBe(2); + expect(exported.fromCache).toBe(true); + expect(exported.providers["minimax-china-coding-plan"]?.status).toBe("ok"); + expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(1); + + v2.dispose(); + }); + + it("does not write the quota export file when export is disabled", async () => { + const exportPath = join(TEST_RUNTIME_ROOT, "export", "quota-export.json"); + currentConfig = configForSingleProvider("minimax-china-coding-plan"); + currentConfig.export = { enabled: false, path: exportPath }; + const v2 = await setupMiniMaxChinaSurfaces(); + + await v2.renderHomeBottom(); + await v2.renderSessionPrompt(); + await new Promise((resolve) => setTimeout(resolve, 50)); + await expect(readFile(exportPath, "utf8")).rejects.toThrow(); + + v2.dispose(); + }); }); From aa7e75cd71d5ca5e02d2ce1f96ea428950734fd4 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:33:36 +0200 Subject: [PATCH 39/97] fix(tui): refresh the Home footer every minute like v4 Restore v4's 60-second refresh (REFRESH_INTERVAL_MS) for the Home footer, the prompt footer, and the sidebar, which all refreshed on a 60s interval in v4. The timer starts when the slot mounts and is cleared on unmount, so the Home footer (and its quota export write) only refreshes while Home is open. Restore the "about once a minute" export wording. --- docs/readme/external-integration.md | 2 +- src/tui-v2.tsx | 13 ++- tests/tui-v2-refresh.test.ts | 144 ++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 tests/tui-v2-refresh.test.ts diff --git a/docs/readme/external-integration.md b/docs/readme/external-integration.md index 53cab60f..81c5e77c 100644 --- a/docs/readme/external-integration.md +++ b/docs/readme/external-integration.md @@ -75,7 +75,7 @@ The file is normally written here: If you set `XDG_CACHE_HOME`, the file is written to `$XDG_CACHE_HOME/opencode/quota-export.json` instead. -The TUI rewrites the file each time the Home screen quota footer refreshes: when Home opens and after each model step while it is open. A write error is logged, but it does not break the TUI. +The TUI refreshes the file about once a minute while the Home screen is open. A write error is logged, but it does not break the TUI. ### tmux example diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index ab42f738..0e682fbe 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -52,6 +52,7 @@ import { buildSidebarQuotaPanelLines } from "./lib/tui-sidebar-format.js"; import type { QuotaToastConfig } from "./lib/types.js"; const terminalForeground = RGBA.defaultForeground(); +const REFRESH_INTERVAL_MS = 60_000; type TuiEvent = { data?: Record }; type Toast = { @@ -439,10 +440,14 @@ function QuotaFooter(props: { }) .catch(reportFailure); refresh(); + const interval = setInterval(refresh, REFRESH_INTERVAL_MS); const stop = props.context.data.on("session.step.ended", (event) => { if (props.surface === "home" || getSessionID(event) === props.sessionID) refresh(); }); - onCleanup(stop); + onCleanup(() => { + clearInterval(interval); + stop(); + }); return ( @@ -549,10 +554,14 @@ function SidebarQuotaView(props: { .catch(reportFailure); }; refresh(); + const interval = setInterval(refresh, REFRESH_INTERVAL_MS); const unsubscribe = props.context.data.on("session.step.ended", (event) => { if (getSessionID(event) === props.sessionID) refresh(); }); - onCleanup(unsubscribe); + onCleanup(() => { + clearInterval(interval); + unsubscribe(); + }); return ( diff --git a/tests/tui-v2-refresh.test.ts b/tests/tui-v2-refresh.test.ts new file mode 100644 index 00000000..05d765ee --- /dev/null +++ b/tests/tui-v2-refresh.test.ts @@ -0,0 +1,144 @@ +import { createRoot } from "solid-js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { buildQuotaExport, collectQuotaRenderData, resolveQuotaRuntimeContext, writeQuotaExport } = + vi.hoisted(() => ({ + buildQuotaExport: vi.fn(), + collectQuotaRenderData: vi.fn(), + resolveQuotaRuntimeContext: vi.fn(), + writeQuotaExport: vi.fn(), + })); + +vi.mock("../src/lib/quota-render-data.js", async () => { + const actual = await vi.importActual( + "../src/lib/quota-render-data.js", + ); + return { ...actual, collectQuotaRenderData }; +}); + +vi.mock("../src/lib/quota-runtime-context.js", async () => { + const actual = await vi.importActual( + "../src/lib/quota-runtime-context.js", + ); + return { ...actual, resolveQuotaRuntimeContext }; +}); + +vi.mock("../src/lib/quota-export.js", async () => { + const actual = await vi.importActual( + "../src/lib/quota-export.js", + ); + return { ...actual, buildQuotaExport, createExportProviderContext: vi.fn(), writeQuotaExport }; +}); + +import plugin from "../src/tui-v2.tsx"; + +const REFRESH_INTERVAL_MS = 60_000; + +async function flushPromises(): Promise { + for (let i = 0; i < 20; i += 1) await Promise.resolve(); +} + +function setupFooterSlots(): Map unknown> { + const renderers = new Map unknown>(); + plugin.setup({ + data: { on: vi.fn(() => vi.fn()) }, + keymap: { layer: vi.fn() }, + ui: { + slot: vi.fn((claim) => { + renderers.set(claim.append, claim.render); + return vi.fn(); + }), + toast: { show: vi.fn() }, + dialog: { alert: vi.fn(), prompt: vi.fn(), set: vi.fn() }, + }, + } as any); + return renderers; +} + +function mountSlot(render: (() => unknown) | undefined): () => void { + return createRoot((dispose) => { + render?.(); + return dispose; + }); +} + +describe("V2 footer refresh timer", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.stubGlobal("React", { + createElement: (type: unknown, props: Record | null) => + typeof type === "function" ? type(props ?? {}) : { type, props }, + }); + resolveQuotaRuntimeContext.mockReset(); + collectQuotaRenderData.mockReset(); + buildQuotaExport.mockReset(); + writeQuotaExport.mockReset(); + resolveQuotaRuntimeContext.mockResolvedValue({ + client: {}, + config: { + enabled: true, + formatStyle: "singleWindow", + percentDisplayMode: "remaining", + minIntervalMs: 60_000, + maintainerAnnouncements: { enabled: false, home: false }, + tuiPromptBar: { enabled: false }, + tuiCompactStatus: { enabled: true, homeBottom: true, sessionPrompt: true, maxWidth: 80 }, + export: { enabled: true, path: "/tmp/opencode-quota-refresh-test.json" }, + }, + configMeta: {}, + providers: [], + resolveRuntimeProviderIds: vi.fn(), + session: {}, + }); + collectQuotaRenderData.mockResolvedValue({ + active: [], + data: { entries: [{ name: "Copilot", percentRemaining: 50 }], errors: [] }, + }); + buildQuotaExport.mockResolvedValue({ version: 2 }); + writeQuotaExport.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); + + it("refreshes the Home footer and writes the export every minute until unmount", async () => { + const renderers = setupFooterSlots(); + const dispose = mountSlot(renderers.get("home.footer.status")); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + expect(writeQuotaExport).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS - 1); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); + expect(writeQuotaExport).toHaveBeenCalledTimes(2); + + dispose(); + await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS * 3); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); + expect(writeQuotaExport).toHaveBeenCalledTimes(2); + }); + + it("refreshes the prompt footer every minute without writing the export", async () => { + const renderers = setupFooterSlots(); + const dispose = mountSlot(renderers.get("prompt.footer")); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); + + dispose(); + await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS * 3); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); + expect(writeQuotaExport).not.toHaveBeenCalled(); + }); +}); From 001f844ce1aa356fa1076aadeb4603fb5c9530ce Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:37:01 +0200 Subject: [PATCH 40/97] fix(cli): use OpenCode 2 login commands OpenCode 2 takes the integration as a positional argument (opencode auth login ); the -p / --provider flags are gone. --- docs/readme/providers.md | 6 +++--- docs/readme/troubleshooting.md | 6 +++--- src/lib/opencode-go-auth.ts | 2 +- src/lib/scoped-update.ts | 2 +- tests/google-gemini-cli-org-only-docs.test.ts | 4 ++-- tests/lib.opencode-go-auth.test.ts | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/readme/providers.md b/docs/readme/providers.md index 0508d29b..63c39a89 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -413,7 +413,7 @@ If that response includes Anthropic's model-scoped Fable weekly window, OpenCode Use companion plugin [`@playwo/opencode-cursor-oauth`](https://github.com/PoolPirate/opencode-cursor#readme). Add it before `@slkiser/opencode-quota` in `opencode.json`, then authenticate once: ```bash -opencode auth login --provider cursor +opencode auth login cursor ``` Cursor estimates the current local billing cycle from OpenCode history. With complete model coverage and a positive configured/preset allowance, it shows an **API budget** percentage with used, limit, and remaining USD facts. If any Cursor model is unknown, it shows only **Known API spend** plus a partial-data issue; it never presents that partial spend as total account spend or a percentage. Without an allowance it shows **API spend**. **Auto+Composer spend** is supplementary and appears in detailed output when space allows. @@ -448,7 +448,7 @@ Use companion plugin [`@anthonyhaussman/opencode-agy-auth`](https://github.com/a Google AGY reports the companion's grouped weekly and five-hour quota windows for each account. ```bash -opencode auth login --provider google-agy +opencode auth login google-agy ``` If you use manual provider selection, include `google-agy` in `enabledProviders`. @@ -482,7 +482,7 @@ Gemini CLI works only with Gemini Code Assist Standard or Enterprise (organizati Use companion plugin [`opencode-gemini-auth`](https://github.com/jenslys/opencode-gemini-auth#readme). Add it before `@slkiser/opencode-quota` in `opencode.json`, then authenticate Google once: ```bash -opencode auth login --provider google +opencode auth login google ``` If you use manual provider selection, include `google-gemini-cli` in `enabledProviders`. diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 1b8eb315..0c67c564 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -131,7 +131,7 @@ Run `/quota_status` and check the Cursor section. | Symptom | Fix | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------- | | Cursor not detected | Put `@playwo/opencode-cursor-oauth` before `@slkiser/opencode-quota` in `opencode.json`. | -| Cursor auth missing | Run `opencode auth login --provider cursor`. | +| Cursor auth missing | Run `opencode auth login cursor`. | | Quota appears but no remaining percentage | Set `cursorPlan` or `cursorIncludedApiUsd` in `opencode-quota/quota-toast.json`. | | Billing cycle looks wrong | Set `cursorBillingCycleStartDay` in `opencode-quota/quota-toast.json` to your local billing anchor day. | | Unknown Cursor pricing | Run `/pricing_refresh`; if still unknown, check `/quota_status` for unknown model ids. | @@ -200,7 +200,7 @@ Run `/quota_status` and check the `google_agy` section. | ----------------------------------- | --------------------------------------------------------------------------------------------- | | Companion missing | Put `@anthonyhaussman/opencode-agy-auth` before `@slkiser/opencode-quota` in `opencode.json`. | | Provider not enabled in manual mode | Include `google-agy` in `enabledProviders` in `opencode-quota/quota-toast.json`. | -| Auth missing | Run `opencode auth login --provider google-agy`. | +| Auth missing | Run `opencode auth login google-agy`. | | Project missing | Set `OPENCODE_AGY_PROJECT_ID` or `provider.google-agy.options.projectId`. | | Provider returns no rows | Check `live_probe`, `live_entry_*`, and `live_error_*` in `/quota_status`. | @@ -217,7 +217,7 @@ Run `/quota_status` and check the Gemini CLI live probe rows. | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Companion missing | Put `opencode-gemini-auth` before `@slkiser/opencode-quota` in `opencode.json`. | | Provider not enabled in manual mode | Include `google-gemini-cli` in `enabledProviders` in `opencode-quota/quota-toast.json`. | -| Auth missing | Run `opencode auth login --provider google`. | +| Auth missing | Run `opencode auth login google`. | | Project missing | Set `provider.google.options.projectId`, `OPENCODE_GEMINI_PROJECT_ID`, `GOOGLE_CLOUD_PROJECT`, or `GOOGLE_CLOUD_PROJECT_ID`. |
diff --git a/src/lib/opencode-go-auth.ts b/src/lib/opencode-go-auth.ts index 92d92b00..6000aa49 100644 --- a/src/lib/opencode-go-auth.ts +++ b/src/lib/opencode-go-auth.ts @@ -7,7 +7,7 @@ import { getCredentialDatabasePaths, readAuthFileCached } from "./opencode-auth. export const DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS = 5_000; // `opencode-go` is the provider id the OpenCode CLI writes via -// `opencode auth login -p opencode-go` (shown as "OpenCode Go api" in `opencode auth list`). +// `opencode auth login opencode-go` (shown as "OpenCode Go api" in `opencode auth list`). // `opencode` stays as a fallback alias for existing manual setups. const OPENCODE_GO_AUTH_KEYS = ["opencode-go", "opencode"] as const; const OPENCODE_GO_PROVIDER_KEYS = ["opencode-go", "opencode"] as const; diff --git a/src/lib/scoped-update.ts b/src/lib/scoped-update.ts index 8c00b899..36c0d759 100644 --- a/src/lib/scoped-update.ts +++ b/src/lib/scoped-update.ts @@ -422,7 +422,7 @@ function formatSafeAction(action: ScopedUpdateSafeAction): string { } const OBSOLETE_GO_GUIDANCE = - "OpenCode Go no longer uses this workspace/cookie source, and it cannot be converted into the official API key. Configure OPENCODE_API_KEY, trusted global provider.opencode-go.options.apiKey, fallback provider.opencode.options.apiKey, or run opencode auth login -p opencode-go. This updater will not read, copy, or delete credentials; remove the old variable/file manually after the supported key works."; + "OpenCode Go no longer uses this workspace/cookie source, and it cannot be converted into the official API key. Configure OPENCODE_API_KEY, trusted global provider.opencode-go.options.apiKey, fallback provider.opencode.options.apiKey, or run opencode auth login opencode-go. This updater will not read, copy, or delete credentials; remove the old variable/file manually after the supported key works."; function formatManualFinding(finding: ScopedUpdateManualFinding): string { switch (finding.kind) { diff --git a/tests/google-gemini-cli-org-only-docs.test.ts b/tests/google-gemini-cli-org-only-docs.test.ts index 8757ef6b..840caf43 100644 --- a/tests/google-gemini-cli-org-only-docs.test.ts +++ b/tests/google-gemini-cli-org-only-docs.test.ts @@ -59,7 +59,7 @@ describe("Gemini CLI organization-only documentation", () => { ); expect(providerSection).toContain("[Google AGY](#google-agy-quick-setup)"); expect(providerSection).toContain("opencode-gemini-auth"); - expect(providerSection).toContain("opencode auth login --provider google"); + expect(providerSection).toContain("opencode auth login google"); expect(providerSection).toContain("include `google-gemini-cli` in `enabledProviders`"); expect(migration).toContain(ORG_ONLY); @@ -83,7 +83,7 @@ describe("Gemini CLI organization-only documentation", () => { expect(troubleshootingSection).toContain(ORG_ONLY); expect(troubleshootingSection).toContain("opencode-gemini-auth"); expect(troubleshootingSection).toContain("Include `google-gemini-cli` in `enabledProviders`"); - expect(troubleshootingSection).toContain("opencode auth login --provider google"); + expect(troubleshootingSection).toContain("opencode auth login google"); for (const projectIdSource of [ "provider.google.options.projectId", "OPENCODE_GEMINI_PROJECT_ID", diff --git a/tests/lib.opencode-go-auth.test.ts b/tests/lib.opencode-go-auth.test.ts index c51732d0..b9baa07f 100644 --- a/tests/lib.opencode-go-auth.test.ts +++ b/tests/lib.opencode-go-auth.test.ts @@ -132,7 +132,7 @@ describe("OpenCode Go auth resolution", () => { } }); - it("accepts the opencode-go opencode.db key written by `opencode auth login -p opencode-go`", () => { + it("accepts the opencode-go opencode.db key written by `opencode auth login opencode-go`", () => { expect(resolveOpenCodeGoAuth({ "opencode-go": { type: "api", key: "cli-key" } })).toEqual({ state: "configured", apiKey: "cli-key", From 99b8e34bfe207699f1af671927c4e12e70d21239 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 08:39:07 +0200 Subject: [PATCH 41/97] build!: target @opencode/plugin 2.0.16 Pin @opencode/plugin 2.0.16 exactly in devDependencies and peerDependencies, and update the version gate and tests to match. --- package.json | 4 +- pnpm-lock.yaml | 303 +++++++++----------------- scripts/verify-typescript-version.mjs | 2 +- tests/package-manifest.test.ts | 6 +- tests/release-gates.test.ts | 2 +- tests/v4-migration-docs.test.ts | 2 +- 6 files changed, 111 insertions(+), 208 deletions(-) diff --git a/package.json b/package.json index 6938d647..44fbb479 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@babel/core": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@biomejs/biome": "2.3.11", - "@opencode/plugin": "2.0.15", + "@opencode/plugin": "2.0.16", "@opentelemetry/sdk-metrics": "2.10.0", "@types/node": "^22.0.0", "babel-preset-solid": "^1.9.12", @@ -101,7 +101,7 @@ "xdg-basedir": "^5.1.0" }, "peerDependencies": { - "@opencode/plugin": "2.0.15" + "@opencode/plugin": "2.0.16" }, "engines": { "node": ">=22.13.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd6175c3..da844f44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,8 +43,8 @@ importers: specifier: 2.3.11 version: 2.3.11 '@opencode/plugin': - specifier: 2.0.15 - version: 2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))(redis@6.2.1(@opentelemetry/api@1.9.1))(solid-js@1.9.12) + specifier: 2.0.16 + version: 2.0.16(@opentelemetry/semantic-conventions@1.43.0)(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))(redis@6.2.1(@opentelemetry/api@1.9.1))(solid-js@1.9.12) '@opentelemetry/sdk-metrics': specifier: 2.10.0 version: 2.10.0(@opentelemetry/api@1.9.1) @@ -687,11 +687,11 @@ packages: resolution: {integrity: sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==} engines: {node: ^20.17.0 || >=22.9.0} - '@opencode/ai@2.0.15': - resolution: {integrity: sha512-dY3HG6bp+UMJOi+m1nfzvQaTPpCSmMNDswAOLQq/MJghGrD54WrcEC4IhAQ1hJY7b/hhQYOzPAMeJsvF4aRYYQ==} + '@opencode/ai@2.0.16': + resolution: {integrity: sha512-QNOut+LUDNqMm2gqzr5xiOkj2xxD3JQX/DG3Z358aBJMgYZkDHY+ui20J7Jl7XdudQb4Jot2hIQ7t6q17BpOFw==} - '@opencode/client@2.0.15': - resolution: {integrity: sha512-Lu3Pb6Z/lJ797VryQrsdqEgVBXjtYZWOxLNZKyBUDgXhO/GpzOpXi1H1Uo4g+qWrH8LwUISDBdxHHOWJy5HcqQ==} + '@opencode/client@2.0.16': + resolution: {integrity: sha512-lEZM5NY8cKVwnlIJVHiERIaIMJfWfKTlQvKKkIz7NcNEwUXlrTXOA79kS5n17HtjYer1u1XiQIIggYtEYJXHRw==} peerDependencies: effect: 4.0.0-rc.112 solid-js: '>=1.9.0' @@ -701,10 +701,10 @@ packages: solid-js: optional: true - '@opencode/plugin@2.0.15': - resolution: {integrity: sha512-TsSXd2g/433De7ViY+5d/tlLtkZCSmYKo8ScnhtcmPVoM/U5ptQDFXEXx5psTlAfkKcISJh+5vMSOe4u3uCqUQ==} + '@opencode/plugin@2.0.16': + resolution: {integrity: sha512-Ir+7fJNUqo81dfzLAcQ8cZVLREbUxx/jbbiPQFR6wCjKWdJXLtMWJIAnYnYxBLf4Im7GcQQfdnlsG5xT9NYNrQ==} peerDependencies: - '@opencode/theme': 2.0.15 + '@opencode/theme': 2.0.16 '@opentui/core': '>=0.5.10' '@opentui/solid': '>=0.5.10' solid-js: '>=1.9.0' @@ -718,17 +718,17 @@ packages: solid-js: optional: true - '@opencode/protocol@2.0.15': - resolution: {integrity: sha512-fFi3VYdgNQzT4TswfKGul7DzseOyhA4b/hE/elUe3xxm5iki4tP/pjT8CU3rRWX8sK/75ICr36NAUhSayFtJuw==} + '@opencode/protocol@2.0.16': + resolution: {integrity: sha512-TQQOXLngSH4D9Z/B6DkyioFsHyApaczjU9EdD5BJ0UtwihKs3iT5mLQlicctKzcWomNoCqIM45XEjBv+WrSynA==} - '@opencode/schema@2.0.15': - resolution: {integrity: sha512-D0JDwF4uTNmFA2MyiSnZ5DlT/t2PF1EEwVbRPulB/Ppcss/z5Fcn6VybHrapki2bKDVQu1Mnn8zBW3G6KP5I6g==} + '@opencode/schema@2.0.16': + resolution: {integrity: sha512-MXENqPxsEzv77AAQDZsyYM4kSqUSApGEKD3xQ6kDDDvm5f5SHMffcNQ+uJXbh7rFo7Fm7nzdvoDHeZwiXm2UvQ==} - '@opencode/util@2.0.15': - resolution: {integrity: sha512-s0Z+2rxb+2Tif+ZV1EvTiGQ0tRiemIZq3WOOjBw2XPBSBt2Sfv9rWXJhTzSpkOZucfTyymuIVXslXPGFF7aaQA==} + '@opencode/util@2.0.16': + resolution: {integrity: sha512-VfY65xppJgSQSXT3x5hH3jM6e5Z08pYEaRA3sKv1K7I8jIFNma8IGZXSoYWaSXPnDt1svxd1sZ9b7tA1RRhOGw==} - '@opentelemetry/api-logs@0.214.0': - resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + '@opentelemetry/api-logs@0.219.0': + resolution: {integrity: sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.0': @@ -739,8 +739,8 @@ packages: resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@2.6.1': - resolution: {integrity: sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ==} + '@opentelemetry/context-async-hooks@2.8.0': + resolution: {integrity: sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -751,26 +751,26 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.6.1': - resolution: {integrity: sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==} + '@opentelemetry/core@2.8.0': + resolution: {integrity: sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-trace-otlp-http@0.214.0': - resolution: {integrity: sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw==} + '@opentelemetry/exporter-trace-otlp-http@0.219.0': + resolution: {integrity: sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.214.0': - resolution: {integrity: sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg==} + '@opentelemetry/otlp-exporter-base@0.219.0': + resolution: {integrity: sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.214.0': - resolution: {integrity: sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w==} + '@opentelemetry/otlp-transformer@0.219.0': + resolution: {integrity: sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -781,14 +781,14 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/resources@2.6.1': - resolution: {integrity: sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==} + '@opentelemetry/resources@2.8.0': + resolution: {integrity: sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.214.0': - resolution: {integrity: sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==} + '@opentelemetry/sdk-logs@0.219.0': + resolution: {integrity: sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' @@ -799,20 +799,20 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.6.1': - resolution: {integrity: sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==} + '@opentelemetry/sdk-metrics@2.8.0': + resolution: {integrity: sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.6.1': - resolution: {integrity: sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==} + '@opentelemetry/sdk-trace-base@2.8.0': + resolution: {integrity: sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.6.1': - resolution: {integrity: sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw==} + '@opentelemetry/sdk-trace-node@2.8.0': + resolution: {integrity: sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -877,33 +877,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.2': - resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} - '@redis/bloom@6.2.1': resolution: {integrity: sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag==} engines: {node: '>= 20.0.0'} @@ -1842,9 +1815,6 @@ packages: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2091,10 +2061,6 @@ packages: promise-call-limit@3.0.2: resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} - protobufjs@7.6.6: - resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} - engines: {node: '>=12.0.0'} - pure-rand@8.4.0: resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} @@ -2904,18 +2870,18 @@ snapshots: fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 - '@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.214.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112)': + '@effect/opentelemetry@4.0.0-rc.112(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112)': dependencies: '@opentelemetry/semantic-conventions': 1.43.0 effect: 4.0.0-rc.112 optionalDependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.214.0 - '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.214.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.219.0 + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.219.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.8.0(@opentelemetry/api@1.9.0) '@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-rc.112)': dependencies: @@ -3192,10 +3158,10 @@ snapshots: node-gyp: 12.4.0 proc-log: 6.1.0 - '@opencode/ai@2.0.15': + '@opencode/ai@2.0.16': dependencies: '@aws-sdk/credential-providers': 3.1057.0 - '@opencode/schema': 2.0.15 + '@opencode/schema': 2.0.16 '@smithy/eventstream-codec': 4.2.14 '@smithy/util-utf8': 4.2.2 aws4fetch: 1.0.20 @@ -3204,22 +3170,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@opencode/client@2.0.15(effect@4.0.0-rc.112)(solid-js@1.9.12)': + '@opencode/client@2.0.16(effect@4.0.0-rc.112)(solid-js@1.9.12)': dependencies: - '@opencode/protocol': 2.0.15 - '@opencode/schema': 2.0.15 + '@opencode/protocol': 2.0.16 + '@opencode/schema': 2.0.16 optionalDependencies: effect: 4.0.0-rc.112 solid-js: 1.9.12 - '@opencode/plugin@2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))(redis@6.2.1(@opentelemetry/api@1.9.1))(solid-js@1.9.12)': + '@opencode/plugin@2.0.16(@opentelemetry/semantic-conventions@1.43.0)(@opentui/core@0.5.10(typescript@7.0.2)(web-tree-sitter@0.25.10))(@opentui/solid@0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10))(redis@6.2.1(@opentelemetry/api@1.9.1))(solid-js@1.9.12)': dependencies: '@ai-sdk/provider': 3.0.8 - '@opencode/ai': 2.0.15 - '@opencode/client': 2.0.15(effect@4.0.0-rc.112)(solid-js@1.9.12) - '@opencode/protocol': 2.0.15 - '@opencode/schema': 2.0.15 - '@opencode/util': 2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(redis@6.2.1(@opentelemetry/api@1.9.1)) + '@opencode/ai': 2.0.16 + '@opencode/client': 2.0.16(effect@4.0.0-rc.112)(solid-js@1.9.12) + '@opencode/protocol': 2.0.16 + '@opencode/schema': 2.0.16 + '@opencode/util': 2.0.16(@opentelemetry/semantic-conventions@1.43.0)(redis@6.2.1(@opentelemetry/api@1.9.1)) '@standard-schema/spec': 1.1.0 effect: 4.0.0-rc.112 zod: 4.1.8 @@ -3228,10 +3194,6 @@ snapshots: '@opentui/solid': 0.5.10(solid-js@1.9.12)(typescript@7.0.2)(web-tree-sitter@0.25.10) solid-js: 1.9.12 transitivePeerDependencies: - - '@opentelemetry/api-logs' - - '@opentelemetry/resources' - - '@opentelemetry/sdk-logs' - - '@opentelemetry/sdk-metrics' - '@opentelemetry/sdk-trace-web' - '@opentelemetry/semantic-conventions' - bufferutil @@ -3239,28 +3201,32 @@ snapshots: - supports-color - utf-8-validate - '@opencode/protocol@2.0.15': + '@opencode/protocol@2.0.16': dependencies: - '@opencode/schema': 2.0.15 + '@opencode/schema': 2.0.16 effect: 4.0.0-rc.112 - '@opencode/schema@2.0.15': + '@opencode/schema@2.0.16': dependencies: '@standard-schema/spec': 1.1.0 effect: 4.0.0-rc.112 - '@opencode/util@2.0.15(@opentelemetry/api-logs@0.214.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(redis@6.2.1(@opentelemetry/api@1.9.1))': + '@opencode/util@2.0.16(@opentelemetry/semantic-conventions@1.43.0)(redis@6.2.1(@opentelemetry/api@1.9.1))': dependencies: - '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.214.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) + '@effect/opentelemetry': 4.0.0-rc.112(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-rc.112) '@effect/platform-node': 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1(@opentelemetry/api@1.9.1)) '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112) '@npmcli/arborist': 9.4.0 '@npmcli/config': 10.8.1 '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.214.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.219.0 + '@opentelemetry/context-async-hooks': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.219.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.219.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.8.0(@opentelemetry/api@1.9.0) cross-spawn: 7.0.6 effect: 4.0.0-rc.112 glob: 13.0.5 @@ -3269,10 +3235,6 @@ snapshots: npm-package-arg: 13.0.2 pacote: 21.5.1 transitivePeerDependencies: - - '@opentelemetry/api-logs' - - '@opentelemetry/resources' - - '@opentelemetry/sdk-logs' - - '@opentelemetry/sdk-metrics' - '@opentelemetry/sdk-trace-web' - '@opentelemetry/semantic-conventions' - bufferutil @@ -3280,7 +3242,7 @@ snapshots: - supports-color - utf-8-validate - '@opentelemetry/api-logs@0.214.0': + '@opentelemetry/api-logs@0.219.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -3288,7 +3250,7 @@ snapshots: '@opentelemetry/api@1.9.1': {} - '@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/context-async-hooks@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -3297,42 +3259,35 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/semantic-conventions': 1.43.0 - optional: true - - '@opentelemetry/exporter-trace-otlp-http@0.214.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.219.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.214.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.214.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.219.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.219.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base@0.214.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.219.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.214.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.219.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.214.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.219.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.214.0 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.214.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) - protobufjs: 7.6.6 + '@opentelemetry/api-logs': 0.219.0 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.219.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': dependencies: @@ -3340,61 +3295,45 @@ snapshots: '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.43.0 - - '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.43.0 - optional: true - '@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.214.0 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.219.0 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.214.0 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.43.0 - optional: true - '@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics@2.6.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions@1.43.0': {} @@ -3460,26 +3399,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.1': {} - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.2': {} - '@redis/bloom@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': dependencies: '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) @@ -4289,8 +4208,6 @@ snapshots: p-locate: 3.0.0 path-exists: 3.0.0 - long@5.3.2: {} - lru-cache@10.4.3: {} lru-cache@11.5.3: {} @@ -4569,20 +4486,6 @@ snapshots: promise-call-limit@3.0.2: {} - protobufjs@7.6.6: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.1 - '@protobufjs/fetch': 1.1.1 - '@protobufjs/float': 1.0.2 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.2 - '@types/node': 22.19.6 - long: 5.3.2 - pure-rand@8.4.0: {} read-cmd-shim@6.0.0: {} diff --git a/scripts/verify-typescript-version.mjs b/scripts/verify-typescript-version.mjs index 079300f3..aa2630d2 100644 --- a/scripts/verify-typescript-version.mjs +++ b/scripts/verify-typescript-version.mjs @@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url"; import { parse } from "yaml"; const EXPECTED_TYPESCRIPT_VERSION = "7.0.2"; -const EXPECTED_PLUGIN_VERSION = "2.0.15"; +const EXPECTED_PLUGIN_VERSION = "2.0.16"; const PLUGIN_PACKAGE = "@opencode/plugin"; const EXPECTED_OPENTUI_SPECIFIER = "^0.5.10"; const EXPECTED_OPENTUI_VERSION = "0.5.10"; diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index 6de6e806..0cef463d 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -153,8 +153,8 @@ describe("package manifest compatibility", () => { }); it("reflects the local OpenCode V2 plugin dependency override", () => { - expect(pkg.peerDependencies?.["@opencode/plugin"]).toBe("2.0.15"); - expect(pkg.devDependencies?.["@opencode/plugin"]).toBe("2.0.15"); + expect(pkg.peerDependencies?.["@opencode/plugin"]).toBe("2.0.16"); + expect(pkg.devDependencies?.["@opencode/plugin"]).toBe("2.0.16"); expect(pkg.dependencies?.["@opentui/core"]).toBe("^0.5.10"); expect(pkg.dependencies?.["@opentui/solid"]).toBe("^0.5.10"); expect(readme).toContain("Node.js `>= 22.13` is required"); @@ -165,7 +165,7 @@ describe("package manifest compatibility", () => { it("keeps the TypeScript 7 toolchain explicit without suppressing the known peer mismatch", () => { expect(tsconfig.compilerOptions?.types).toEqual(["node"]); expect(typescriptValidator).toContain('const EXPECTED_TYPESCRIPT_VERSION = "7.0.2";'); - expect(typescriptValidator).toContain('const EXPECTED_PLUGIN_VERSION = "2.0.15";'); + expect(typescriptValidator).toContain('const EXPECTED_PLUGIN_VERSION = "2.0.16";'); expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_SPECIFIER = "^0.5.10";'); expect(typescriptValidator).toContain('const EXPECTED_OPENTUI_VERSION = "0.5.10";'); expect(typescriptValidator).toContain('const BUN_FFI_STRUCTS_VERSION = "0.3.1";'); diff --git a/tests/release-gates.test.ts b/tests/release-gates.test.ts index e73d50c0..941b2456 100644 --- a/tests/release-gates.test.ts +++ b/tests/release-gates.test.ts @@ -121,7 +121,7 @@ describe("v4 release gates", () => { const typescript = run(typescriptScript); expect(typescript.status).toBe(0); expect(typescript.stdout).toContain( - "TypeScript 7.0.2 and @opencode/plugin 2.0.15 lock entries verified", + "TypeScript 7.0.2 and @opencode/plugin 2.0.16 lock entries verified", ); const historyRepo = path.join(tempDir, "clean-history"); diff --git a/tests/v4-migration-docs.test.ts b/tests/v4-migration-docs.test.ts index 86c3bd8d..d18f2243 100644 --- a/tests/v4-migration-docs.test.ts +++ b/tests/v4-migration-docs.test.ts @@ -24,7 +24,7 @@ const configuration = await readFile( describe("v4 migration documentation contract", () => { it("keeps requirements and navigation aligned", () => { expect(packageJson.engines?.node).toBe(">=22.13.0"); - expect(packageJson.peerDependencies?.["@opencode/plugin"]).toBe("2.0.15"); + expect(packageJson.peerDependencies?.["@opencode/plugin"]).toBe("2.0.16"); expect(packageJson.engines).not.toHaveProperty("opencode"); expect(migration).toContain("[← Back to README](../../README.md)"); From 049896d4647dfd69a56e0063de0a2e0c93175d36 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 08:40:48 +0200 Subject: [PATCH 42/97] chore(release): set version to 5.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44fbb479..c9e54672 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slkiser/opencode-quota", - "version": "4.10.3", + "version": "5.0.0", "packageManager": "pnpm@11.0.0", "description": "OpenCode quota & tokens usage with zero context window pollution. Supports GitHub Copilot, OpenAI (Plus/Pro), Chutes AI, Synthetic, Google AGY, Z.ai coding plan and more.", "type": "module", From f72fd1cc8a76097ff36b4b370e53db0b0678b62c Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:22:16 +0200 Subject: [PATCH 43/97] fix(config): resolve OpenCode config, cache and state dirs like OpenCode 2 On macOS the plugin also read the real ~/.config/opencode (quota-toast.jsonc, opencode.jsonc, mimo.json, ...) even when XDG_CONFIG_HOME pointed elsewhere, and those values won. The cause was the per-platform config/cache/state candidate lists (~/.config, ~/Library/Application Support, ~/Library/Caches, APPDATA/LOCALAPPDATA) that Step 4c left in place after fixing the data dir. OpenCode 2 has exactly one dir per kind on every platform (oc2 packages/util/src/global-roots.ts): $XDG_CONFIG_HOME|~/.config, $XDG_CACHE_HOME|~/.cache, $XDG_STATE_HOME|~/.local/state, plus /opencode. OPENCODE_CONFIG_DIR replaces the global config dir (oc2 packages/cli/src/index.ts, packages/server/src/routes.ts), and project config is still discovered from the project (packages/core/src/config/discovery.ts). - getOpencodeRuntimeDirs() now implements exactly those rules; getOpencodeRuntimeDirCandidates() and the xdg-basedir dependency are removed. A relative OPENCODE_CONFIG_DIR resolves against the cwd, as in OpenCode 2. - Config layering, provider-key lookup, Copilot/MiMo/Zen config files, Cursor detection, Google companion lookup, init (global scope), and update migration/audit/cache cleanup use the single config or cache dir. - OPENCODE_CONFIG_DIR is no longer used as the workspace config root (getEffectiveConfigRoot removed): it is the global dir, and the project's opencode.json / opencode-quota sidecar is read from the worktree root. The plugin's own cache/state files keep their write location (xdg-basedir already resolved ~/.cache and ~/.local/state on every platform); only the extra read-only fallback folders are gone. Caches found only under those fallbacks are rebuilt. --- docs/readme/configuration.md | 4 +- docs/readme/manual-install.md | 4 +- package.json | 3 +- pnpm-lock.yaml | 9 -- src/lib/api-key-resolver.ts | 14 +-- src/lib/cli-show.ts | 5 +- src/lib/config-file-utils.ts | 27 +---- src/lib/config.ts | 16 +-- src/lib/copilot.ts | 21 +--- src/lib/cursor-detection.ts | 6 +- src/lib/google-companion-credentials.ts | 50 ++++---- src/lib/init-installer.ts | 8 +- src/lib/mimo-config.ts | 5 +- src/lib/opencode-config-providers.ts | 30 +++-- src/lib/opencode-runtime-paths.ts | 107 ++--------------- src/lib/opencode-zen-config.ts | 5 +- src/lib/scoped-update-migration.ts | 49 +++----- src/lib/scoped-update.ts | 26 ++--- src/plugin.ts | 4 +- tests/helpers/config-loader-test-harness.ts | 42 +++---- tests/helpers/plugin-test-harness.ts | 29 +---- tests/helpers/trusted-config-test-harness.ts | 6 - tests/lib.api-key-resolver.test.ts | 6 - tests/lib.cli-show.test.ts | 29 ++--- tests/lib.cli-status.test.ts | 24 ++-- tests/lib.config-file-utils.test.ts | 45 +------- tests/lib.config.integration.test.ts | 108 +++++++++--------- tests/lib.config.security.test.ts | 10 +- tests/lib.config.test.ts | 32 ++++-- tests/lib.copilot.test.ts | 10 +- tests/lib.cursor-detection.test.ts | 10 +- tests/lib.google-companion.test.ts | 42 ++++--- tests/lib.mimo-config.test.ts | 30 +++-- tests/lib.mimo.security.test.ts | 10 +- tests/lib.opencode-config-providers.test.ts | 18 +-- tests/lib.opencode-go-auth.test.ts | 3 +- tests/lib.opencode-runtime-paths.test.ts | 67 +++++------ tests/lib.opencode-zen-config.test.ts | 49 +++----- ...lib.quota-export.production-policy.test.ts | 6 - tests/lib.quota-providers-auth.test.ts | 6 - tests/lib.quota-providers.integration.test.ts | 10 +- tests/lib.quota-providers.security.test.ts | 10 +- tests/lib.quota-runtime-context.test.ts | 45 +++----- tests/lib.quota-status.test.ts | 3 - tests/lib.scoped-update-migration.test.ts | 89 +++------------ tests/plugin.maintainer-announcements.test.ts | 2 +- tests/provider-surfaces.opencode-go.test.ts | 2 +- tests/providers.quota-providers.test.ts | 6 - tests/quota-state.test.ts | 6 - tests/v4-phase5-cross-surface.test.ts | 2 +- 50 files changed, 385 insertions(+), 765 deletions(-) diff --git a/docs/readme/configuration.md b/docs/readme/configuration.md index 1fcc154b..8df6d3c4 100644 --- a/docs/readme/configuration.md +++ b/docs/readme/configuration.md @@ -9,8 +9,8 @@ Most people only need the examples on this page. The full option list is at the OpenCode Quota normally keeps its settings in one separate file: - Project install: `/opencode-quota/quota-toast.jsonc` -- Global install: usually `~/.config/opencode/opencode-quota/quota-toast.jsonc` -- Custom config directory: `$OPENCODE_CONFIG_DIR/opencode-quota/quota-toast.jsonc` +- Global install: `~/.config/opencode/opencode-quota/quota-toast.jsonc` on every OS (`$XDG_CONFIG_HOME/opencode/...` when `XDG_CONFIG_HOME` is set) +- Custom config directory: `$OPENCODE_CONFIG_DIR/opencode-quota/quota-toast.jsonc` (replaces the global folder, like OpenCode 2) Strict `.json` files also work. Run `/quota_status` if you are unsure which file is active. diff --git a/docs/readme/manual-install.md b/docs/readme/manual-install.md index f16066f7..eab66b44 100644 --- a/docs/readme/manual-install.md +++ b/docs/readme/manual-install.md @@ -12,9 +12,9 @@ Use this guide only if you want to edit OpenCode files yourself. ## Choose where to install -- **Global:** works in every project. Files usually live in `~/.config/opencode`. +- **Global:** works in every project. Files live in `~/.config/opencode` on every OS (`$XDG_CONFIG_HOME/opencode` when `XDG_CONFIG_HOME` is set). - **Project:** works only in the current repo or worktree. -- **Custom:** if `OPENCODE_CONFIG_DIR` is set, use that directory. +- **Custom:** if `OPENCODE_CONFIG_DIR` is set, OpenCode uses that directory instead of the global one. Use `.jsonc` files if you want comments. Use `.json` files if another tool requires strict JSON. diff --git a/package.json b/package.json index c9e54672..052a9072 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,7 @@ "@opentui/solid": "^0.5.10", "comment-json": "^5.0.0", "jsonc-parser": "^3.3.1", - "solid-js": "^1.9.12", - "xdg-basedir": "^5.1.0" + "solid-js": "^1.9.12" }, "peerDependencies": { "@opencode/plugin": "2.0.16" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da844f44..39a3df98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,9 +29,6 @@ importers: solid-js: specifier: ^1.9.12 version: 1.9.12 - xdg-basedir: - specifier: ^5.1.0 - version: 5.1.0 devDependencies: '@babel/core': specifier: ^7.28.0 @@ -2389,10 +2386,6 @@ packages: utf-8-validate: optional: true - xdg-basedir@5.1.0: - resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} - engines: {node: '>=12'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4811,8 +4804,6 @@ snapshots: ws@8.21.3: {} - xdg-basedir@5.1.0: {} - yallist@3.1.1: {} yallist@4.0.0: {} diff --git a/src/lib/api-key-resolver.ts b/src/lib/api-key-resolver.ts index 99ac535c..ac1889a9 100644 --- a/src/lib/api-key-resolver.ts +++ b/src/lib/api-key-resolver.ts @@ -12,7 +12,7 @@ import { buildOpenCodeConfigCandidates, readOpenCodeConfigCandidate, } from "./opencode-config-read.js"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; /** A candidate config file path with its format */ export interface ConfigCandidate { @@ -20,9 +20,9 @@ export interface ConfigCandidate { isJsonc: boolean; } -function buildOpencodeConfigCandidates(configDirs: readonly string[]): ConfigCandidate[] { +function buildOpencodeConfigCandidates(directories: readonly string[]): ConfigCandidate[] { return buildOpenCodeConfigCandidates({ - directories: configDirs, + directories, formatOrder: ["jsonc", "json"], }).map((candidate) => ({ path: candidate.path, @@ -38,9 +38,9 @@ function buildOpencodeConfigCandidates(configDirs: readonly string[]): ConfigCan */ export function getOpencodeConfigCandidatePaths(): ConfigCandidate[] { const cwd = process.cwd(); - const { configDirs } = getOpencodeRuntimeDirCandidates(); + const { configDir } = getOpencodeRuntimeDirs(); - return [...buildOpencodeConfigCandidates([cwd]), ...buildOpencodeConfigCandidates(configDirs)]; + return [...buildOpencodeConfigCandidates([cwd]), ...buildOpencodeConfigCandidates([configDir])]; } /** @@ -50,8 +50,8 @@ export function getOpencodeConfigCandidatePaths(): ConfigCandidate[] { * current workspace may be untrusted. */ export function getGlobalOpencodeConfigCandidatePaths(): ConfigCandidate[] { - const { configDirs } = getOpencodeRuntimeDirCandidates(); - return buildOpencodeConfigCandidates(configDirs); + const { configDir } = getOpencodeRuntimeDirs(); + return buildOpencodeConfigCandidates([configDir]); } /** diff --git a/src/lib/cli-show.ts b/src/lib/cli-show.ts index 55f59ab6..e74f9ffd 100644 --- a/src/lib/cli-show.ts +++ b/src/lib/cli-show.ts @@ -1,6 +1,6 @@ import { resolve } from "path"; import { hasAnthropicCredentialsConfigured } from "./anthropic.js"; -import { findGitWorktreeRoot, getEffectiveConfigRoot } from "./config-file-utils.js"; +import { findGitWorktreeRoot } from "./config-file-utils.js"; import { sanitizeQuotaRenderData } from "./display-sanitize.js"; import { formatQuotaRows } from "./format.js"; import { formatQuotaModeHeading } from "./format-utils.js"; @@ -146,10 +146,9 @@ export function resolveCliRoots(cwd: string): { } { const fallbackDirectory = resolve(cwd); const worktreeRoot = findGitWorktreeRoot(fallbackDirectory) ?? fallbackDirectory; - const configRoot = getEffectiveConfigRoot(worktreeRoot); return { workspaceRoot: worktreeRoot, - configRoot, + configRoot: worktreeRoot, fallbackDirectory, }; } diff --git a/src/lib/config-file-utils.ts b/src/lib/config-file-utils.ts index e2d5d56d..44e645e6 100644 --- a/src/lib/config-file-utils.ts +++ b/src/lib/config-file-utils.ts @@ -1,5 +1,5 @@ import { existsSync } from "fs"; -import { dirname, isAbsolute, join, resolve } from "path"; +import { dirname, join } from "path"; export type ConfigFileKind = "opencode"; export type ConfigFileFormat = "json" | "jsonc"; @@ -44,29 +44,6 @@ function pickFirstNonEmptyString(items: Array): strin return null; } -/** - * Returns the effective config root directory. - * - * Priority: - * 1. `OPENCODE_CONFIG_DIR` environment variable (if set and non-empty) - * 2. The provided fallback directory - * - * This matches OpenCode's own behavior: when `OPENCODE_CONFIG_DIR` is set, - * config files are resolved relative to it rather than the current working directory. - */ -export function getEffectiveConfigRoot(fallback: string): string { - const envDir = process.env.OPENCODE_CONFIG_DIR?.trim(); - if (!envDir) { - return fallback; - } - - if (isAbsolute(envDir)) { - return envDir; - } - - return resolve(fallback, envDir); -} - export function resolveRuntimeContextRoots(params: RuntimeContextRootHints): RuntimeContextRoots { const workspaceRoot = pickFirstNonEmptyString([ @@ -78,7 +55,7 @@ export function resolveRuntimeContextRoots(params: RuntimeContextRootHints): Run const explicitConfigRoot = pickFirstNonEmptyString([params.configRoot]); const computedConfigRoot = pickFirstNonEmptyString([workspaceRoot, params.activeDirectory]) ?? workspaceRoot; - const configRoot = explicitConfigRoot ?? getEffectiveConfigRoot(computedConfigRoot); + const configRoot = explicitConfigRoot ?? computedConfigRoot; return { workspaceRoot, configRoot }; } diff --git a/src/lib/config.ts b/src/lib/config.ts index 77084e14..85bc2c2d 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -9,13 +9,12 @@ import { existsSync } from "fs"; import { join } from "path"; -import { getEffectiveConfigRoot } from "./config-file-utils.js"; import { isResetTimeDecimals } from "./format-utils.js"; import { buildOpenCodeConfigCandidates, readOpenCodeConfigCandidate, } from "./opencode-config-read.js"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { getQuotaProviderShape, normalizeQuotaProviderId } from "./provider-metadata.js"; import { isQuotaFormatStyle, resolveQuotaFormatStyle } from "./quota-format-style.js"; import { cloneQuotaProviders, validateQuotaProviders } from "./quota-providers.js"; @@ -1192,13 +1191,11 @@ function buildConfigLayerCandidatesForRoot( } export function buildConfigLayerCandidates( - configDirs: string[], + globalConfigDir: string, configRootDir: string, ): ConfigLayerCandidate[] { const workspaceCandidates = buildConfigLayerCandidatesForRoot(configRootDir, "workspace"); - const globalCandidates = configDirs.flatMap((dir) => - buildConfigLayerCandidatesForRoot(dir, "global"), - ); + const globalCandidates = buildConfigLayerCandidatesForRoot(globalConfigDir, "global"); const globalPaths = new Set(globalCandidates.map((candidate) => candidate.path)); return [ @@ -1253,9 +1250,8 @@ export async function loadConfig( networkSettingSources: Record; configIssues: LoadConfigIssue[]; }> { - const configRootDir = - options?.configRootDir ?? getEffectiveConfigRoot(options?.cwd ?? process.cwd()); - const { configDirs } = getOpencodeRuntimeDirCandidates(); + const configRootDir = options?.configRootDir ?? options?.cwd ?? process.cwd(); + const { configDir } = getOpencodeRuntimeDirs(); const config = cloneDefaultConfig(); const usedPaths: string[] = []; const globalConfigPaths: string[] = []; @@ -1264,7 +1260,7 @@ export async function loadConfig( const configIssues: LoadConfigIssue[] = []; const authoritativeSidecarRoots = new Set(); - for (const candidate of buildConfigLayerCandidates(configDirs, configRootDir)) { + for (const candidate of buildConfigLayerCandidates(configDir, configRootDir)) { const rootKey = `${candidate.scope}:${candidate.rootDir}`; if (candidate.kind === "legacy" && authoritativeSidecarRoots.has(rootKey)) { continue; diff --git a/src/lib/copilot.ts b/src/lib/copilot.ts index f9631f09..b77db249 100644 --- a/src/lib/copilot.ts +++ b/src/lib/copilot.ts @@ -11,7 +11,7 @@ import { join } from "path"; import { sanitizeDisplaySnippet, sanitizeDisplayText } from "./display-sanitize.js"; import { fetchWithTimeout } from "./http.js"; import { readAuthFile } from "./opencode-auth.js"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { deriveResolvedAuthIdentity, type ResolvedAuthIdentity } from "./resolved-auth-identity.js"; import type { AuthData, @@ -210,20 +210,6 @@ const LEGACY_PREMIUM_REQUEST_TOTALS: Partial> = { "pro+": 1500, }; -function dedupeStrings(values: Array): string[] { - const out: string[] = []; - const seen = new Set(); - - for (const value of values) { - const trimmed = value?.trim(); - if (!trimmed || seen.has(trimmed)) continue; - seen.add(trimmed); - out.push(trimmed); - } - - return out; -} - function validateEnterpriseHost(value: unknown): { host?: string; error?: string } { if (value === undefined || value === null) return {}; if (typeof value !== "string" || !value.trim()) { @@ -431,10 +417,7 @@ function validatePatTargetCompatibility( } export function getCopilotPatConfigCandidatePaths(): string[] { - const { configDirs } = getOpencodeRuntimeDirCandidates(); - return dedupeStrings( - configDirs.map((configDir) => join(configDir, COPILOT_QUOTA_CONFIG_FILENAME)), - ); + return [join(getOpencodeRuntimeDirs().configDir, COPILOT_QUOTA_CONFIG_FILENAME)]; } function validateQuotaConfig(raw: unknown): { config: CopilotQuotaConfig | null; error?: string } { diff --git a/src/lib/cursor-detection.ts b/src/lib/cursor-detection.ts index a483818e..677c29f5 100644 --- a/src/lib/cursor-detection.ts +++ b/src/lib/cursor-detection.ts @@ -5,7 +5,7 @@ import { join } from "path"; import { CURSOR_LEGACY_PROVIDER_ID } from "./cursor-pricing.js"; import { parseJsonOrJsonc } from "./jsonc.js"; import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { getQuotaProviderRuntimeIds } from "./provider-metadata.js"; import type { CursorOAuthAuthData } from "./types.js"; @@ -177,9 +177,9 @@ function parseOpenCodeConfig( } export async function inspectCursorOpenCodeIntegration(): Promise { - const { configDirs } = getOpencodeRuntimeDirCandidates(); + const { configDir } = getOpencodeRuntimeDirs(); const checkedPaths = dedupe( - [...configDirs, process.cwd()].flatMap((dir) => [ + [configDir, process.cwd()].flatMap((dir) => [ join(dir, "opencode.json"), join(dir, "opencode.jsonc"), ]), diff --git a/src/lib/google-companion-credentials.ts b/src/lib/google-companion-credentials.ts index 9ee0d291..ee26f639 100644 --- a/src/lib/google-companion-credentials.ts +++ b/src/lib/google-companion-credentials.ts @@ -4,7 +4,7 @@ import { dirname, join } from "node:path"; import { pathToFileURL } from "node:url"; import { readFile } from "fs/promises"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; const require = createRequire(import.meta.url); @@ -124,7 +124,7 @@ function resolveSpecifier( try { return require.resolve(specifier, { - paths: getOpencodeRuntimeDirCandidates().cacheDirs, + paths: [getOpencodeRuntimeDirs().cacheDir], }); } catch (runtimeError) { markPackageFound(runtimeError, context); @@ -134,36 +134,32 @@ function resolveSpecifier( } function getRuntimePackageRoots(descriptor: GoogleCompanionDescriptor): string[] { - const cacheDirs = getOpencodeRuntimeDirCandidates().cacheDirs; - const packageRoots = cacheDirs.map((cacheDir) => - join(cacheDir, "node_modules", descriptor.packageName), - ); + const { cacheDir } = getOpencodeRuntimeDirs(); + const packageRoots = [join(cacheDir, "node_modules", descriptor.packageName)]; - for (const cacheDir of cacheDirs) { - try { - const packagesDir = join(cacheDir, "packages"); - if (descriptor.packageScan === "scoped") { - const [scope, name] = descriptor.packageName.split("/"); - const scopeDir = join(packagesDir, scope!); - for (const entry of readdirSync(scopeDir, { withFileTypes: true })) { - if (entry.isDirectory() && entry.name.startsWith(name!)) { - const packagePath = join(scopeDir, entry.name); - packageRoots.push(packagePath); - packageRoots.push(join(packagePath, "node_modules", descriptor.packageName)); - } + try { + const packagesDir = join(cacheDir, "packages"); + if (descriptor.packageScan === "scoped") { + const [scope, name] = descriptor.packageName.split("/"); + const scopeDir = join(packagesDir, scope!); + for (const entry of readdirSync(scopeDir, { withFileTypes: true })) { + if (entry.isDirectory() && entry.name.startsWith(name!)) { + const packagePath = join(scopeDir, entry.name); + packageRoots.push(packagePath); + packageRoots.push(join(packagePath, "node_modules", descriptor.packageName)); } - } else { - for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { - if (entry.isDirectory() && entry.name.startsWith(descriptor.packageName)) { - const packagePath = join(packagesDir, entry.name); - packageRoots.push(packagePath); - packageRoots.push(join(packagePath, "node_modules", descriptor.packageName)); - } + } + } else { + for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { + if (entry.isDirectory() && entry.name.startsWith(descriptor.packageName)) { + const packagePath = join(packagesDir, entry.name); + packageRoots.push(packagePath); + packageRoots.push(join(packagePath, "node_modules", descriptor.packageName)); } } - } catch { - // A missing runtime packages directory is not an installed companion. } + } catch { + // A missing runtime packages directory is not an installed companion. } return packageRoots; diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index a93db320..957dbb25 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -24,7 +24,7 @@ import { planConfigDocumentEdit, validateConfigDocumentEdit, } from "./opencode-config-editor.js"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { getQuotaProviderDisplayLabel, normalizeQuotaProviderId, @@ -973,11 +973,7 @@ export function resolveInitInstallerBaseDir(params: { homeDir?: string; }): string { if (params.scope === "global") { - const candidates = getOpencodeRuntimeDirCandidates({ - env: params.env, - homeDir: params.homeDir, - }); - return candidates.configDirs[0]!; + return getOpencodeRuntimeDirs({ env: params.env, homeDir: params.homeDir }).configDir; } const cwd = params.cwd ?? process.cwd(); diff --git a/src/lib/mimo-config.ts b/src/lib/mimo-config.ts index d3706639..0aa4eb51 100644 --- a/src/lib/mimo-config.ts +++ b/src/lib/mimo-config.ts @@ -1,7 +1,7 @@ import { readFile } from "fs/promises"; import { join } from "path"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; export interface MimoConfig { cookie: string; @@ -35,8 +35,7 @@ const RETAINED_COOKIE_NAME_SET = new Set(RETAINED_COOKIE_NAMES); const REQUIRED_COOKIE_NAMES = ["api-platform_serviceToken", "userId"] as const; function getConfigCandidatePaths(): string[] { - const { configDirs } = getOpencodeRuntimeDirCandidates(); - return configDirs.map((dir) => join(dir, "opencode-quota", "mimo.json")); + return [join(getOpencodeRuntimeDirs().configDir, "opencode-quota", "mimo.json")]; } export function normalizeMimoCookieHeader(raw: string): string | null { diff --git a/src/lib/opencode-config-providers.ts b/src/lib/opencode-config-providers.ts index 6409703c..a0c391a5 100644 --- a/src/lib/opencode-config-providers.ts +++ b/src/lib/opencode-config-providers.ts @@ -20,7 +20,7 @@ import { readOpenCodeConfigCandidate, selectFirstExistingOpenCodeConfigCandidate, } from "./opencode-config-read.js"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { getQuotaProviderRuntimeIds, getQuotaProviderShape, @@ -50,18 +50,17 @@ function isRecord(value: unknown): value is Record { } function getCandidates(configRootDir: string): OpenCodeConfigCandidate[] { - return dedupeNonEmptyStrings([ - ...getOpencodeRuntimeDirCandidates().configDirs, - configRootDir, - ]).flatMap((directory) => { - const selected = selectFirstExistingOpenCodeConfigCandidate( - buildOpenCodeConfigCandidates({ - directories: [directory], - formatOrder: ["jsonc", "json"], - }), - ); - return selected ? [selected] : []; - }); + return dedupeNonEmptyStrings([getOpencodeRuntimeDirs().configDir, configRootDir]).flatMap( + (directory) => { + const selected = selectFirstExistingOpenCodeConfigCandidate( + buildOpenCodeConfigCandidates({ + directories: [directory], + formatOrder: ["jsonc", "json"], + }), + ); + return selected ? [selected] : []; + }, + ); } async function readConfig( @@ -169,9 +168,8 @@ export async function reconcileDetectedProvidersInGlobalConfig( return Boolean(shape && shape.id !== "quota-providers"); }), ); - const { configDirs } = getOpencodeRuntimeDirCandidates(); - const globalConfigDir = configDirs[0]; - if (!globalConfigDir || detectedProviderIds.length === 0) { + const globalConfigDir = getOpencodeRuntimeDirs().configDir; + if (detectedProviderIds.length === 0) { return { path: null, format: null, addedProviderIds: [], changed: false }; } diff --git a/src/lib/opencode-runtime-paths.ts b/src/lib/opencode-runtime-paths.ts index 86abecae..21300e2c 100644 --- a/src/lib/opencode-runtime-paths.ts +++ b/src/lib/opencode-runtime-paths.ts @@ -1,6 +1,5 @@ import { homedir } from "os"; -import { dirname, isAbsolute, join, resolve } from "path"; -import { xdgCache, xdgConfig, xdgData, xdgState } from "xdg-basedir"; +import { join, resolve } from "path"; export interface OpencodeRuntimeDirs { dataDir: string; @@ -9,26 +8,12 @@ export interface OpencodeRuntimeDirs { stateDir: string; } -// No data-dir candidates: OpenCode 2 has exactly one data dir (`dataDir`), and -// its database path comes from `getOpenCodeDbPath()`. -export interface OpencodeRuntimeDirCandidates { - configDirs: string[]; - cacheDirs: string[]; - stateDirs: string[]; -} - -function dedupe(list: string[]): string[] { - const out: string[] = []; - const seen = new Set(); - for (const item of list) { - if (!item) continue; - if (seen.has(item)) continue; - seen.add(item); - out.push(item); - } - return out; -} - +/** + * OpenCode 2's global dirs (oc2 packages/util/src/global-roots.ts): one dir per + * kind on every platform, `$XDG__HOME/opencode` or the XDG default under + * the home dir. `OPENCODE_CONFIG_DIR` replaces the config dir + * (oc2 packages/cli/src/index.ts `Global.layerWith({ config })`). + */ export function getOpencodeRuntimeDirs(params?: { env?: NodeJS.ProcessEnv; homeDir?: string; @@ -36,84 +21,16 @@ export function getOpencodeRuntimeDirs(params?: { const env = params?.env ?? process.env; const home = params?.homeDir ?? homedir(); - // Match OpenCode behavior: Global.Path. = join(xdg, "opencode") - // xdg-basedir may return undefined if env is missing; provide deterministic fallbacks. - const dataBase = env.XDG_DATA_HOME?.trim() || xdgData || join(home, ".local", "share"); - const configBase = env.XDG_CONFIG_HOME?.trim() || xdgConfig || join(home, ".config"); - const defaultConfigDir = join(configBase, "opencode"); + const dataBase = env.XDG_DATA_HOME?.trim() || join(home, ".local", "share"); + const configBase = env.XDG_CONFIG_HOME?.trim() || join(home, ".config"); + const cacheBase = env.XDG_CACHE_HOME?.trim() || join(home, ".cache"); + const stateBase = env.XDG_STATE_HOME?.trim() || join(home, ".local", "state"); const configuredConfigDir = env.OPENCODE_CONFIG_DIR?.trim(); - const configDir = configuredConfigDir - ? isAbsolute(configuredConfigDir) - ? configuredConfigDir - : resolve(defaultConfigDir, configuredConfigDir) - : defaultConfigDir; - const cacheBase = env.XDG_CACHE_HOME?.trim() || xdgCache || join(home, ".cache"); - const stateBase = env.XDG_STATE_HOME?.trim() || xdgState || join(home, ".local", "state"); return { dataDir: join(dataBase, "opencode"), - configDir, + configDir: configuredConfigDir ? resolve(configuredConfigDir) : join(configBase, "opencode"), cacheDir: join(cacheBase, "opencode"), stateDir: join(stateBase, "opencode"), }; } - -export function getOpencodeRuntimeDirCandidates(params?: { - platform?: NodeJS.Platform; - env?: NodeJS.ProcessEnv; - homeDir?: string; - primary?: OpencodeRuntimeDirs; -}): OpencodeRuntimeDirCandidates { - const platform = params?.platform ?? process.platform; - const env = params?.env ?? process.env; - const home = params?.homeDir ?? homedir(); - - const primary = params?.primary ?? getOpencodeRuntimeDirs({ env, homeDir: home }); - - const winAppData = env.APPDATA?.trim(); - const winLocalAppData = env.LOCALAPPDATA?.trim(); - - const windowsRoamingFallback = join(home, "AppData", "Roaming"); - const windowsLocalFallback = join(home, "AppData", "Local"); - - const configDirs: string[] = [primary.configDir]; - const cacheDirs: string[] = [primary.cacheDir]; - const stateDirs: string[] = [primary.stateDir]; - - if (platform === "win32") { - // OpenCode uses xdg-basedir; on some Windows setups that can resolve to - // non-AppData locations (e.g. C:\\Users\\name.local\\share). However, many - // users and older installs place OpenCode data/config under APPDATA/LOCALAPPDATA. - const appDataBase = winAppData || windowsRoamingFallback; - const localAppDataBase = winLocalAppData || windowsLocalFallback; - - // Config: include both roaming and local as alternates. - configDirs.push(join(appDataBase, "opencode")); - configDirs.push(join(localAppDataBase, "opencode")); - - // Cache/state: local is more likely. - cacheDirs.push(join(localAppDataBase, "opencode")); - stateDirs.push(join(localAppDataBase, "opencode")); - } else if (platform === "darwin") { - // Preserve compatibility with legacy Linux-style installs on macOS. - configDirs.push(join(home, ".config", "opencode")); - cacheDirs.push(join(home, ".cache", "opencode")); - stateDirs.push(join(home, ".local", "state", "opencode")); - - // macOS canonical dirs (OpenCode xdg-basedir should already map here, - // but keep explicit candidates to aid diagnostics and migrations). - configDirs.push(join(home, "Library", "Application Support", "opencode")); - cacheDirs.push(join(home, "Library", "Caches", "opencode")); - } else { - // Linux / other: add common legacy fallbacks. - configDirs.push(join(home, ".config", "opencode")); - cacheDirs.push(join(home, ".cache", "opencode")); - stateDirs.push(join(home, ".local", "state", "opencode")); - } - - return { - configDirs: dedupe(configDirs), - cacheDirs: dedupe(cacheDirs), - stateDirs: dedupe(stateDirs), - }; -} diff --git a/src/lib/opencode-zen-config.ts b/src/lib/opencode-zen-config.ts index c8214a61..4f98d6b8 100644 --- a/src/lib/opencode-zen-config.ts +++ b/src/lib/opencode-zen-config.ts @@ -1,7 +1,7 @@ import { readFile } from "fs/promises"; import { join } from "path"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; export interface OpenCodeZenConfig { workspaceId: string; @@ -31,8 +31,7 @@ const LEGACY_AUTH_COOKIE_ERROR = "authCookie no longer works after the OpenCode Console redesign; paste the __Host-console_session cookie as consoleSessionCookie"; function getConfigCandidatePaths(): string[] { - const { configDirs } = getOpencodeRuntimeDirCandidates(); - return configDirs.map((dir) => join(dir, "opencode-quota", "opencode.json")); + return [join(getOpencodeRuntimeDirs().configDir, "opencode-quota", "opencode.json")]; } function getConfigFileError(error: unknown): string { diff --git a/src/lib/scoped-update-migration.ts b/src/lib/scoped-update-migration.ts index 7d8531be..3ea99bb2 100644 --- a/src/lib/scoped-update-migration.ts +++ b/src/lib/scoped-update-migration.ts @@ -276,18 +276,16 @@ export function inspectLegacyDisplayDocument(params: { } export function buildScopedUpdateMigrationCandidates(params: { - globalRoots: readonly string[]; + globalRoot: string; workspaceRoot: string; }): ScopedUpdateMigrationCandidate[] { - return buildConfigLayerCandidates([...params.globalRoots], params.workspaceRoot).map( - (candidate) => ({ - path: candidate.path, - rootDir: candidate.rootDir, - scope: candidate.scope, - format: candidate.path.endsWith(".jsonc") ? "jsonc" : "json", - container: candidate.kind === "plugin" ? "quota-root" : "experimental.quotaToast", - }), - ); + return buildConfigLayerCandidates(params.globalRoot, params.workspaceRoot).map((candidate) => ({ + path: candidate.path, + rootDir: candidate.rootDir, + scope: candidate.scope, + format: candidate.path.endsWith(".jsonc") ? "jsonc" : "json", + container: candidate.kind === "plugin" ? "quota-root" : "experimental.quotaToast", + })); } function isMissing(error: unknown): boolean { @@ -346,7 +344,7 @@ export async function resolveScopedUpdateMigrationBoundary(params: { } export async function discoverExistingScopedUpdateMigrationCandidates(params: { - globalRoots: readonly string[]; + globalRoot: string; workspaceRoot: string; selectedPackagePaths?: readonly string[]; }): Promise<{ @@ -430,8 +428,7 @@ async function knownPathExists(path: string, action: string): Promise { export async function auditObsoleteUpdateSources(params: { env: NodeJS.ProcessEnv; - configDirs: string[]; - primaryConfigDir: string; + configDir: string; }): Promise { const findings: ScopedUpdateManualFinding[] = []; @@ -441,32 +438,24 @@ export async function auditObsoleteUpdateSources(params: { } } - const configDirs = [...new Set(params.configDirs)]; - const goPaths = configDirs.map((dir) => join(dir, OBSOLETE_GO_FILE)); - const zenPaths = configDirs.map((dir) => join(dir, SUPPORTED_ZEN_FILE)); - const [goPresence, zenPresence] = await Promise.all([ - Promise.all( - goPaths.map((path) => knownPathExists(path, "inspect obsolete OpenCode Go source")), - ), - Promise.all( - zenPaths.map((path) => knownPathExists(path, "inspect supported OpenCode Zen source")), - ), + const goPath = join(params.configDir, OBSOLETE_GO_FILE); + const zenPath = join(params.configDir, SUPPORTED_ZEN_FILE); + const [goPresent, zenPresent] = await Promise.all([ + knownPathExists(goPath, "inspect obsolete OpenCode Go source"), + knownPathExists(zenPath, "inspect supported OpenCode Zen source"), ]); - for (let index = 0; index < goPaths.length; index++) { - const path = goPaths[index]; - if (goPresence[index] && path) { - findings.push({ kind: "obsolete-go-file", path }); - } + if (goPresent) { + findings.push({ kind: "obsolete-go-file", path: goPath }); } - if (!zenPresence.some(Boolean)) { + if (!zenPresent) { const names = AMBIGUOUS_ZEN_ENV_NAMES.filter((name) => Object.hasOwn(params.env, name)); if (names.length > 0) { findings.push({ kind: "ambiguous-zen-env", names: [...names], - suggestedPath: join(params.primaryConfigDir, SUPPORTED_ZEN_FILE), + suggestedPath: zenPath, }); } } diff --git a/src/lib/scoped-update.ts b/src/lib/scoped-update.ts index 36c0d759..0df57731 100644 --- a/src/lib/scoped-update.ts +++ b/src/lib/scoped-update.ts @@ -15,10 +15,7 @@ import { } from "./config-write-target.js"; import { sanitizeSingleLineDisplayText } from "./display-sanitize.js"; import { editConfigDocumentPaths, parseConfigDocument } from "./opencode-config-editor.js"; -import { - getOpencodeRuntimeDirCandidates, - getOpencodeRuntimeDirs, -} from "./opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { auditObsoleteUpdateSources, discoverExistingScopedUpdateMigrationCandidates, @@ -197,16 +194,10 @@ export async function planScopedUpdate( const cwd = params.cwd ?? process.cwd(); const env = params.env ?? process.env; const projectRoot = findGitWorktreeRoot(cwd) ?? cwd; - const primaryRuntime = getOpencodeRuntimeDirs({ env, homeDir: params.homeDir }); - const runtime = getOpencodeRuntimeDirCandidates({ - platform: params.platform, - env, - homeDir: params.homeDir, - primary: primaryRuntime, - }); + const runtime = getOpencodeRuntimeDirs({ env, homeDir: params.homeDir }); const configPaths = await dedupeByRealPath([ ...selectedConfigPaths(projectRoot), - ...selectedConfigPaths(primaryRuntime.configDir), + ...selectedConfigPaths(runtime.configDir), ]); const workingDocuments = new Map(); @@ -241,7 +232,7 @@ export async function planScopedUpdate( } const migrationDiscovery = await discoverExistingScopedUpdateMigrationCandidates({ - globalRoots: runtime.configDirs, + globalRoot: runtime.configDir, workspaceRoot: projectRoot, selectedPackagePaths: configPaths, }); @@ -303,8 +294,7 @@ export async function planScopedUpdate( manualFindings.push( ...(await auditObsoleteUpdateSources({ env, - configDirs: runtime.configDirs, - primaryConfigDir: primaryRuntime.configDir, + configDir: runtime.configDir, })), ); @@ -345,10 +335,8 @@ export async function planScopedUpdate( const uniqueSpecs = [...new Set(foundSpecs)]; const cacheSpecs = [...new Set([...uniqueSpecs, QUOTA_LATEST_SPEC])]; - const cacheCandidates = runtime.cacheDirs.flatMap((cacheDir) => - cacheSpecs.map((spec) => - join(cacheDir, "packages", sanitizeOpenCodePackageSpec(spec, params.platform)), - ), + const cacheCandidates = cacheSpecs.map((spec) => + join(runtime.cacheDir, "packages", sanitizeOpenCodePackageSpec(spec, params.platform)), ); return { diff --git a/src/plugin.ts b/src/plugin.ts index efdf2f49..87ab0d5e 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,6 +1,6 @@ /** OpenCode V2 server plugin: deterministic quota diagnostics tool. */ import { Plugin } from "@opencode/plugin"; -import { findGitWorktreeRoot, getEffectiveConfigRoot } from "./lib/config-file-utils.js"; +import { findGitWorktreeRoot } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { reconcileDetectedProvidersInGlobalConfig } from "./lib/opencode-config-providers.js"; import { buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js"; @@ -12,7 +12,7 @@ export const QuotaToastPlugin = Plugin.define({ const workspaceRoot = findGitWorktreeRoot(directory) ?? directory; const roots = { workspaceRoot, - configRoot: getEffectiveConfigRoot(workspaceRoot), + configRoot: workspaceRoot, fallbackDirectory: directory, }; // The quota collector accepts a small V1-shaped configuration client. V2 diff --git a/tests/helpers/config-loader-test-harness.ts b/tests/helpers/config-loader-test-harness.ts index c83b9104..f27852f7 100644 --- a/tests/helpers/config-loader-test-harness.ts +++ b/tests/helpers/config-loader-test-harness.ts @@ -2,12 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; -type RuntimeDirCandidates = { - dataDirs: string[]; - configDirs: string[]; - cacheDirs: string[]; - stateDirs: string[]; -}; +import type { OpencodeRuntimeDirs } from "../../src/lib/opencode-runtime-paths.js"; type ConfigLoaderWorkspaceOptions = { nestedPath?: string[]; @@ -21,19 +16,19 @@ export type ConfigLoaderWorkspace = { xdgDataHome: string; xdgCacheHome: string; xdgStateHome: string; - appDataRoaming: string; - appDataLocal: string; opencodeConfigDir: string; - runtimeDirs: RuntimeDirCandidates; + runtimeDirs: OpencodeRuntimeDirs; cleanup: () => void; }; -export function createEmptyRuntimeDirCandidates(): RuntimeDirCandidates { +/** Runtime dirs under `root/unused-runtime`, which tests never create. */ +export function createUnusedRuntimeDirs(root: string): OpencodeRuntimeDirs { + const base = join(root, "unused-runtime"); return { - dataDirs: [], - configDirs: [], - cacheDirs: [], - stateDirs: [], + dataDir: join(base, "data"), + configDir: join(base, "config"), + cacheDir: join(base, "cache"), + stateDir: join(base, "state"), }; } @@ -50,8 +45,6 @@ export function createConfigLoaderWorkspace( const xdgDataHome = join(tempDir, "xdg-data"); const xdgCacheHome = join(tempDir, "xdg-cache"); const xdgStateHome = join(tempDir, "xdg-state"); - const appDataRoaming = join(tempDir, "appdata", "roaming"); - const appDataLocal = join(tempDir, "appdata", "local"); const opencodeConfigDir = join(xdgConfigHome, "opencode"); mkdirSync(nestedDir, { recursive: true }); @@ -65,14 +58,12 @@ export function createConfigLoaderWorkspace( xdgDataHome, xdgCacheHome, xdgStateHome, - appDataRoaming, - appDataLocal, opencodeConfigDir, runtimeDirs: { - dataDirs: [join(xdgDataHome, "opencode")], - configDirs: [opencodeConfigDir], - cacheDirs: [join(xdgCacheHome, "opencode")], - stateDirs: [join(xdgStateHome, "opencode")], + dataDir: join(xdgDataHome, "opencode"), + configDir: opencodeConfigDir, + cacheDir: join(xdgCacheHome, "opencode"), + stateDir: join(xdgStateHome, "opencode"), }, cleanup: () => rmSync(tempDir, { recursive: true, force: true }), }; @@ -80,7 +71,7 @@ export function createConfigLoaderWorkspace( export function createConfigLoaderEnv( workspace: ConfigLoaderWorkspace, - options: { home?: string; includePlatformAppData?: boolean } = {}, + options: { home?: string } = {}, ): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { XDG_CONFIG_HOME: workspace.xdgConfigHome, @@ -93,11 +84,6 @@ export function createConfigLoaderEnv( env.HOME = options.home; } - if (options.includePlatformAppData) { - env.APPDATA = workspace.appDataRoaming; - env.LOCALAPPDATA = workspace.appDataLocal; - } - return env; } diff --git a/tests/helpers/plugin-test-harness.ts b/tests/helpers/plugin-test-harness.ts index 667dffa6..b0b1e250 100644 --- a/tests/helpers/plugin-test-harness.ts +++ b/tests/helpers/plugin-test-harness.ts @@ -56,17 +56,6 @@ interface PluginRuntimeRoot { stateDir: string; } -interface PluginRuntimePathCandidates { - cacheDirs: string[]; - configDirs: string[]; - dataDirs: string[]; - stateDirs: string[]; -} - -interface PluginRuntimePathsMockOptions { - includeCandidates?: boolean; -} - export function createConfigModuleMock(loadConfig: MockFunction) { return { loadConfig, @@ -121,27 +110,11 @@ export function createPluginBootstrapRuntimeRoot(root: string): PluginRuntimeRoo }; } -export function createPluginRuntimePathCandidates(root: string): PluginRuntimePathCandidates { - return { - dataDirs: [`${root}/data`], - configDirs: [`${root}/config`], - cacheDirs: [`${root}/cache`], - stateDirs: [`${root}/state`], - }; -} - -export function createPluginRuntimePathsMockModule( - root: string, - options: PluginRuntimePathsMockOptions = {}, -) { +export function createPluginRuntimePathsMockModule(root: string) { const runtimeRoot = createPluginBootstrapRuntimeRoot(root); - const candidates = createPluginRuntimePathCandidates(root); return { getOpencodeRuntimeDirs: () => ({ ...runtimeRoot }), - ...(options.includeCandidates - ? { getOpencodeRuntimeDirCandidates: () => ({ ...candidates }) } - : {}), }; } diff --git a/tests/helpers/trusted-config-test-harness.ts b/tests/helpers/trusted-config-test-harness.ts index e2555f77..09c2078e 100644 --- a/tests/helpers/trusted-config-test-harness.ts +++ b/tests/helpers/trusted-config-test-harness.ts @@ -13,12 +13,6 @@ export function createRuntimePathsMockModule() { const stateDir = join(homedir(), ".local", "state", "opencode"); return { - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: [dataDir], - configDirs: [configDir], - cacheDirs: [cacheDir], - stateDirs: [stateDir], - }), getOpencodeRuntimeDirs: () => ({ dataDir, configDir, diff --git a/tests/lib.api-key-resolver.test.ts b/tests/lib.api-key-resolver.test.ts index 0dbeacca..2b5b6a7f 100644 --- a/tests/lib.api-key-resolver.test.ts +++ b/tests/lib.api-key-resolver.test.ts @@ -3,12 +3,6 @@ import { join } from "path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: [join(homedir(), ".local", "share", "opencode")], - configDirs: [join(homedir(), ".config", "opencode")], - cacheDirs: [join(homedir(), ".cache", "opencode")], - stateDirs: [join(homedir(), ".local", "state", "opencode")], - }), getOpencodeRuntimeDirs: () => ({ dataDir: join(homedir(), ".local", "share", "opencode"), configDir: join(homedir(), ".config", "opencode"), diff --git a/tests/lib.cli-show.test.ts b/tests/lib.cli-show.test.ts index cd8b5a95..b481642d 100644 --- a/tests/lib.cli-show.test.ts +++ b/tests/lib.cli-show.test.ts @@ -19,10 +19,10 @@ const { authMocks, mockProviders, runtimeDirs } = vi.hoisted(() => ({ mockProviders: [] as any[], runtimeDirs: { value: { - dataDirs: [] as string[], - configDirs: [] as string[], - cacheDirs: [] as string[], - stateDirs: [] as string[], + dataDir: "/tmp/opencode-quota-cli-show-data", + configDir: "/tmp/opencode-quota-cli-show-config", + cacheDir: "/tmp/opencode-quota-cli-show-cache", + stateDir: "/tmp/opencode-quota-cli-show-state", }, }, })); @@ -42,13 +42,7 @@ vi.mock("../src/providers/registry.js", () => ({ })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => runtimeDirs.value, - getOpencodeRuntimeDirs: () => ({ - dataDir: runtimeDirs.value.dataDirs[0] ?? "/tmp/opencode-quota-cli-show-data", - configDir: runtimeDirs.value.configDirs[0] ?? "/tmp/opencode-quota-cli-show-config", - cacheDir: runtimeDirs.value.cacheDirs[0] ?? "/tmp/opencode-quota-cli-show-cache", - stateDir: runtimeDirs.value.stateDirs[0] ?? "/tmp/opencode-quota-cli-show-state", - }), + getOpencodeRuntimeDirs: () => runtimeDirs.value, })); import { createCliQuotaClient, runCliShowCommand } from "../src/lib/cli-show.js"; @@ -87,10 +81,10 @@ describe("runCliShowCommand", () => { mkdirSync(globalConfigDir, { recursive: true }); mkdirSync(workspaceDir, { recursive: true }); runtimeDirs.value = { - dataDirs: [], - configDirs: [globalConfigDir], - cacheDirs: [join(tempDir, "cache")], - stateDirs: [], + dataDir: "/tmp/opencode-quota-cli-show-data", + configDir: globalConfigDir, + cacheDir: join(tempDir, "cache"), + stateDir: "/tmp/opencode-quota-cli-show-state", }; mockProviders.length = 0; __resetQuotaStateForTests(); @@ -444,7 +438,7 @@ describe("runCliShowCommand", () => { expect(stderr.output).toContain("Quota disabled in config"); }); - it("resolves relative OPENCODE_CONFIG_DIR from the worktree root", async () => { + it("reads worktree root config even when OPENCODE_CONFIG_DIR is set", async () => { const nestedDir = join(workspaceDir, "packages", "app"); const provider = { id: "synthetic", @@ -455,10 +449,9 @@ describe("runCliShowCommand", () => { mockProviders.push(provider); mkdirSync(nestedDir, { recursive: true }); mkdirSync(join(workspaceDir, ".git")); - mkdirSync(join(workspaceDir, ".opencode"), { recursive: true }); process.env.OPENCODE_CONFIG_DIR = ".opencode"; writeFileSync( - join(workspaceDir, ".opencode", "opencode.json"), + join(workspaceDir, "opencode.json"), JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), "utf8", ); diff --git a/tests/lib.cli-status.test.ts b/tests/lib.cli-status.test.ts index a314f22d..cd5e7f91 100644 --- a/tests/lib.cli-status.test.ts +++ b/tests/lib.cli-status.test.ts @@ -7,10 +7,10 @@ const { mockProviders, runtimeDirs, statusData } = vi.hoisted(() => ({ mockProviders: [] as any[], runtimeDirs: { value: { - dataDirs: [] as string[], - configDirs: [] as string[], - cacheDirs: [] as string[], - stateDirs: [] as string[], + dataDir: "/tmp/opencode-quota-cli-status-data", + configDir: "/tmp/opencode-quota-cli-status-config", + cacheDir: "/tmp/opencode-quota-cli-status-cache", + stateDir: "/tmp/opencode-quota-cli-status-state", }, }, statusData: { @@ -27,13 +27,7 @@ vi.mock("../src/providers/registry.js", () => ({ })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => runtimeDirs.value, - getOpencodeRuntimeDirs: () => ({ - dataDir: runtimeDirs.value.dataDirs[0] ?? "/tmp/opencode-quota-cli-status-data", - configDir: runtimeDirs.value.configDirs[0] ?? "/tmp/opencode-quota-cli-status-config", - cacheDir: runtimeDirs.value.cacheDirs[0] ?? "/tmp/opencode-quota-cli-status-cache", - stateDir: runtimeDirs.value.stateDirs[0] ?? "/tmp/opencode-quota-cli-status-state", - }), + getOpencodeRuntimeDirs: () => runtimeDirs.value, })); vi.mock("../src/lib/quota-dialog-commands.js", () => ({ @@ -132,10 +126,10 @@ describe("runCliStatusCommand", () => { mkdirSync(globalConfigDir, { recursive: true }); mkdirSync(workspaceDir, { recursive: true }); runtimeDirs.value = { - dataDirs: [], - configDirs: [globalConfigDir], - cacheDirs: [join(tempDir, "cache")], - stateDirs: [], + dataDir: "/tmp/opencode-quota-cli-status-data", + configDir: globalConfigDir, + cacheDir: join(tempDir, "cache"), + stateDir: "/tmp/opencode-quota-cli-status-state", }; mockProviders.length = 0; statusData.value = null; diff --git a/tests/lib.config-file-utils.test.ts b/tests/lib.config-file-utils.test.ts index ebf00158..a695ad27 100644 --- a/tests/lib.config-file-utils.test.ts +++ b/tests/lib.config-file-utils.test.ts @@ -1,48 +1,11 @@ import { resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { - getEffectiveConfigRoot, - resolveRuntimeContextRoots, -} from "../src/lib/config-file-utils.js"; +import { resolveRuntimeContextRoots } from "../src/lib/config-file-utils.js"; -const PROJECT_ROOT = resolve("home", "user", "project"); const WORKSPACE_ROOT = resolve("work", "repo"); const FALLBACK_DIRECTORY = resolve(WORKSPACE_ROOT, "packages", "app"); -describe("getEffectiveConfigRoot", () => { - const original = process.env.OPENCODE_CONFIG_DIR; - - afterEach(() => { - if (original === undefined) { - delete process.env.OPENCODE_CONFIG_DIR; - } else { - process.env.OPENCODE_CONFIG_DIR = original; - } - }); - - it("returns fallback when OPENCODE_CONFIG_DIR is not set", () => { - delete process.env.OPENCODE_CONFIG_DIR; - expect(getEffectiveConfigRoot(PROJECT_ROOT)).toBe(PROJECT_ROOT); - }); - - it("returns OPENCODE_CONFIG_DIR when set", () => { - const customConfig = resolve("custom", "config"); - process.env.OPENCODE_CONFIG_DIR = customConfig; - expect(getEffectiveConfigRoot(PROJECT_ROOT)).toBe(customConfig); - }); - - it("resolves relative OPENCODE_CONFIG_DIR from fallback", () => { - process.env.OPENCODE_CONFIG_DIR = ".opencode"; - expect(getEffectiveConfigRoot(PROJECT_ROOT)).toBe(resolve(PROJECT_ROOT, ".opencode")); - }); - - it("ignores whitespace-only OPENCODE_CONFIG_DIR", () => { - process.env.OPENCODE_CONFIG_DIR = " "; - expect(getEffectiveConfigRoot(PROJECT_ROOT)).toBe(PROJECT_ROOT); - }); -}); - describe("resolveRuntimeContextRoots", () => { const original = process.env.OPENCODE_CONFIG_DIR; @@ -54,7 +17,7 @@ describe("resolveRuntimeContextRoots", () => { } }); - it("uses OPENCODE_CONFIG_DIR only when explicit configRoot is absent", () => { + it("uses the workspace root as config root even when OPENCODE_CONFIG_DIR is set", () => { process.env.OPENCODE_CONFIG_DIR = ".opencode"; expect( resolveRuntimeContextRoots({ @@ -63,11 +26,11 @@ describe("resolveRuntimeContextRoots", () => { }), ).toEqual({ workspaceRoot: WORKSPACE_ROOT, - configRoot: resolve(WORKSPACE_ROOT, ".opencode"), + configRoot: WORKSPACE_ROOT, }); }); - it("uses explicit configRoot as-is without re-applying OPENCODE_CONFIG_DIR", () => { + it("uses explicit configRoot as-is", () => { process.env.OPENCODE_CONFIG_DIR = ".opencode"; const explicitConfigRoot = resolve(WORKSPACE_ROOT, ".explicit"); expect( diff --git a/tests/lib.config.integration.test.ts b/tests/lib.config.integration.test.ts index 4b62eeb6..581411ae 100644 --- a/tests/lib.config.integration.test.ts +++ b/tests/lib.config.integration.test.ts @@ -26,7 +26,7 @@ vi.mock("os", async (importOriginal) => { import { createLoadConfigMeta, loadConfig } from "../src/lib/config.js"; import { applyInitInstallerPlan, planInitInstaller } from "../src/lib/init-installer.js"; -import { getOpencodeRuntimeDirCandidates } from "../src/lib/opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "../src/lib/opencode-runtime-paths.js"; import { applyProviderAddPlan, planProviderAdd } from "../src/lib/provider-add.js"; import { applyScopedUpdatePlan, planScopedUpdate } from "../src/lib/scoped-update.js"; @@ -50,7 +50,7 @@ describe("loadConfig integration runtime-path resolution", () => { process.env = { ...originalEnv, - ...createConfigLoaderEnv(workspace, { home: tempDir, includePlatformAppData: true }), + ...createConfigLoaderEnv(workspace, { home: tempDir }), }; delete process.env.OPENCODE_CONFIG_DIR; process.chdir(nestedDir); @@ -66,19 +66,17 @@ describe("loadConfig integration runtime-path resolution", () => { it("uses real runtime dirs as defaults and explicit cwd config as workspace overrides", async () => { const env = { ...process.env, - ...createConfigLoaderEnv(workspace, { home: tempDir, includePlatformAppData: true }), + ...createConfigLoaderEnv(workspace, { home: tempDir }), } as NodeJS.ProcessEnv; - const { configDirs } = getOpencodeRuntimeDirCandidates({ env, homeDir: tempDir }); - for (const dir of configDirs) { - mkdirSync(dir, { recursive: true }); - writeQuotaToastConfig(dir, { - enabled: false, - enabledProviders: ["openai"], - accountingDetail: "summary", - showOnIdle: false, - pricingSnapshot: { source: "bundled", autoRefresh: 30 }, - }); - } + const { configDir } = getOpencodeRuntimeDirs({ env, homeDir: tempDir }); + mkdirSync(configDir, { recursive: true }); + writeQuotaToastConfig(configDir, { + enabled: false, + enabledProviders: ["openai"], + accountingDetail: "summary", + showOnIdle: false, + pricingSnapshot: { source: "bundled", autoRefresh: 30 }, + }); writeQuotaToastConfig(workspaceDir, { enabled: true, @@ -104,30 +102,42 @@ describe("loadConfig integration runtime-path resolution", () => { expect(cfg.onlyCurrentModel).toBe(true); expect(meta.source).toBe("files"); - expect( - meta.paths.some((path) => configDirs.some((dir) => path === quotaConfigSource(dir))), - ).toBe(true); + expect(meta.paths).toContain(quotaConfigSource(configDir)); expect(meta.paths).toContain(quotaConfigSource(workspaceDir)); expect(meta.paths).not.toContain(quotaConfigSource(nestedDir)); expect(meta.workspaceConfigPaths).toEqual([quotaConfigSource(workspaceDir)]); - expect( - meta.globalConfigPaths.some((path) => - configDirs.some((dir) => path === quotaConfigSource(dir)), - ), - ).toBe(true); + expect(meta.globalConfigPaths).toEqual([quotaConfigSource(configDir)]); expect(meta.settingSources.enabled).toBe(quotaConfigSource(workspaceDir)); expect(meta.settingSources.enabledProviders).toBe(quotaConfigSource(workspaceDir)); expect(meta.settingSources.accountingDetail).toBe(quotaConfigSource(workspaceDir)); - expect( - configDirs.some( - (dir) => meta.settingSources["pricingSnapshot.source"] === quotaConfigSource(dir), - ), - ).toBe(true); - expect( - configDirs.some( - (dir) => meta.settingSources["pricingSnapshot.autoRefresh"] === quotaConfigSource(dir), - ), - ).toBe(true); + expect(meta.settingSources["pricingSnapshot.source"]).toBe(quotaConfigSource(configDir)); + expect(meta.settingSources["pricingSnapshot.autoRefresh"]).toBe(quotaConfigSource(configDir)); + }); + + it("never reads ~/.config/opencode on macOS when XDG_CONFIG_HOME points elsewhere", async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")!; + Object.defineProperty(process, "platform", { value: "darwin" }); + try { + const realConfigDirs = [ + join(tempDir, ".config", "opencode"), + join(tempDir, "Library", "Application Support", "opencode"), + ]; + for (const dir of realConfigDirs) { + writeQuotaSidecarConfig(dir, { enabled: false, minIntervalMs: 12_345 }); + writeQuotaToastConfig(dir, { enabled: false, minIntervalMs: 12_345 }); + } + expect(workspace.xdgConfigHome).not.toBe(join(tempDir, ".config")); + + const meta = createLoadConfigMeta(); + const cfg = await loadConfig(undefined, meta, { configRootDir: workspaceDir }); + + expect(cfg.enabled).toBe(true); + expect(cfg.minIntervalMs).not.toBe(12_345); + expect(meta.paths).toEqual([]); + expect(getOpencodeRuntimeDirs().configDir).toBe(workspace.opencodeConfigDir); + } finally { + Object.defineProperty(process, "platform", originalPlatform); + } }); it("diagnoses the removed Zen display key in file and legacy sources without translation", async () => { @@ -169,7 +179,7 @@ describe("loadConfig integration runtime-path resolution", () => { }); const env = { ...process.env, - ...createConfigLoaderEnv(workspace, { home: tempDir, includePlatformAppData: true }), + ...createConfigLoaderEnv(workspace, { home: tempDir }), } as NodeJS.ProcessEnv; const plan = await planScopedUpdate({ cwd: workspaceDir, env, homeDir: tempDir }); @@ -195,7 +205,7 @@ describe("loadConfig integration runtime-path resolution", () => { const original = readFileSync(hostPath, "utf8"); const env = { ...process.env, - ...createConfigLoaderEnv(workspace, { home: tempDir, includePlatformAppData: true }), + ...createConfigLoaderEnv(workspace, { home: tempDir }), } as NodeJS.ProcessEnv; const plan = await planScopedUpdate({ cwd: workspaceDir, env, homeDir: tempDir }); @@ -240,7 +250,7 @@ describe("loadConfig integration runtime-path resolution", () => { it("loads a custom provider after init creates a manual-mode JSONC sidecar", async () => { const env = process.env as NodeJS.ProcessEnv; - const configDir = getOpencodeRuntimeDirCandidates({ env, homeDir: tempDir }).configDirs[0]!; + const { configDir } = getOpencodeRuntimeDirs({ env, homeDir: tempDir }); const initPlan = await planInitInstaller({ env, homeDir: tempDir, @@ -357,18 +367,15 @@ describe("loadConfig integration runtime-path resolution", () => { it("uses the provided configRootDir to pick the workspace override layer over shared global defaults", async () => { const env = { ...process.env, - ...createConfigLoaderEnv(workspace, { home: tempDir, includePlatformAppData: true }), + ...createConfigLoaderEnv(workspace, { home: tempDir }), } as NodeJS.ProcessEnv; - const { configDirs } = getOpencodeRuntimeDirCandidates({ env, homeDir: tempDir }); - - for (const dir of configDirs) { - mkdirSync(dir, { recursive: true }); - writeQuotaToastConfig(dir, { - enabled: false, - enabledProviders: ["openai"], - minIntervalMs: 30_000, - }); - } + const { configDir } = getOpencodeRuntimeDirs({ env, homeDir: tempDir }); + mkdirSync(configDir, { recursive: true }); + writeQuotaToastConfig(configDir, { + enabled: false, + enabledProviders: ["openai"], + minIntervalMs: 30_000, + }); writeQuotaToastConfig(workspaceDir, { enabled: true, @@ -408,13 +415,8 @@ describe("loadConfig integration runtime-path resolution", () => { expect(workspaceMeta.workspaceConfigPaths).toEqual([quotaConfigSource(workspaceDir)]); expect(nestedMeta.workspaceConfigPaths).toEqual([quotaConfigSource(nestedDir)]); - expect( - configDirs.some( - (dir) => - workspaceMeta.globalConfigPaths.includes(quotaConfigSource(dir)) && - nestedMeta.globalConfigPaths.includes(quotaConfigSource(dir)), - ), - ).toBe(true); + expect(workspaceMeta.globalConfigPaths).toEqual([quotaConfigSource(configDir)]); + expect(nestedMeta.globalConfigPaths).toEqual([quotaConfigSource(configDir)]); expect(workspaceMeta.settingSources.enabled).toBe(quotaConfigSource(workspaceDir)); expect(nestedMeta.settingSources.enabled).toBe(quotaConfigSource(nestedDir)); expect(workspaceMeta.settingSources.minIntervalMs).toBe(quotaConfigSource(workspaceDir)); diff --git a/tests/lib.config.security.test.ts b/tests/lib.config.security.test.ts index 652b0c08..27368b21 100644 --- a/tests/lib.config.security.test.ts +++ b/tests/lib.config.security.test.ts @@ -10,15 +10,15 @@ import { const runtimeDirs = vi.hoisted(() => ({ value: { - dataDirs: [] as string[], - configDirs: [] as string[], - cacheDirs: [] as string[], - stateDirs: [] as string[], + dataDir: "", + configDir: "", + cacheDir: "", + stateDir: "", }, })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => runtimeDirs.value, + getOpencodeRuntimeDirs: () => runtimeDirs.value, })); import { createLoadConfigMeta, loadConfig } from "../src/lib/config.js"; diff --git a/tests/lib.config.test.ts b/tests/lib.config.test.ts index bc174dd4..4fe9fbe7 100644 --- a/tests/lib.config.test.ts +++ b/tests/lib.config.test.ts @@ -4,21 +4,21 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { type ConfigLoaderWorkspace, createConfigLoaderWorkspace, - createEmptyRuntimeDirCandidates, + createUnusedRuntimeDirs, quotaSidecarConfigSource, } from "./helpers/config-loader-test-harness.js"; const runtimeDirs = vi.hoisted(() => ({ value: { - dataDirs: [] as string[], - configDirs: [] as string[], - cacheDirs: [] as string[], - stateDirs: [] as string[], + dataDir: "", + configDir: "", + cacheDir: "", + stateDir: "", }, })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => runtimeDirs.value, + getOpencodeRuntimeDirs: () => runtimeDirs.value, })); import { createLoadConfigMeta, loadConfig } from "../src/lib/config.js"; @@ -34,7 +34,7 @@ describe("loadConfig", () => { delete process.env.OPENCODE_CONFIG_DIR; workspace = createConfigLoaderWorkspace("opencode-quota-config-sdk-"); isolatedCwd = workspace.workspaceDir; - runtimeDirs.value = createEmptyRuntimeDirCandidates(); + runtimeDirs.value = createUnusedRuntimeDirs(workspace.tempDir); }); afterEach(() => { @@ -536,7 +536,9 @@ describe("loadConfig", () => { expect(explicit.meta.networkSettingSources).toEqual({}); }); - it("resolves relative OPENCODE_CONFIG_DIR against cwd for file loading", async () => { + it("keeps the workspace config root when OPENCODE_CONFIG_DIR is set, like OpenCode 2", async () => { + // OpenCode 2 uses OPENCODE_CONFIG_DIR as the global config dir (resolved by + // opencode-runtime-paths, mocked here); project config still comes from the workspace. process.env.OPENCODE_CONFIG_DIR = ".opencode"; mkdirSync(join(isolatedCwd, ".opencode"), { recursive: true }); writeFileSync( @@ -544,14 +546,20 @@ describe("loadConfig", () => { JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), "utf8", ); + writeFileSync( + join(isolatedCwd, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { minIntervalMs: 12_345 } } }), + "utf8", + ); const meta = createLoadConfigMeta(); const config = await loadConfig(undefined, meta, { cwd: isolatedCwd }); - expect(config.enabled).toBe(false); - expect(meta.paths).toContain( - `${join(isolatedCwd, ".opencode", "opencode.json")} (experimental.quotaToast)`, - ); + expect(config.enabled).toBe(true); + expect(config.minIntervalMs).toBe(12_345); + expect(meta.workspaceConfigPaths).toEqual([ + `${join(isolatedCwd, "opencode.json")} (experimental.quotaToast)`, + ]); }); it("ignores invalid OpenCode Go windows without recording a setting source", async () => { diff --git a/tests/lib.copilot.test.ts b/tests/lib.copilot.test.ts index e21ea049..6988e2c8 100644 --- a/tests/lib.copilot.test.ts +++ b/tests/lib.copilot.test.ts @@ -40,11 +40,11 @@ vi.mock("fs", async (importOriginal) => { }); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: [testPaths.dataDir], - configDirs: [testPaths.configDir], - cacheDirs: [testPaths.cacheDir], - stateDirs: [testPaths.stateDir], + getOpencodeRuntimeDirs: () => ({ + dataDir: testPaths.dataDir, + configDir: testPaths.configDir, + cacheDir: testPaths.cacheDir, + stateDir: testPaths.stateDir, }), })); diff --git a/tests/lib.cursor-detection.test.ts b/tests/lib.cursor-detection.test.ts index 28fbf188..40600fa9 100644 --- a/tests/lib.cursor-detection.test.ts +++ b/tests/lib.cursor-detection.test.ts @@ -50,11 +50,11 @@ vi.mock("../src/lib/opencode-auth.js", () => ({ })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: [testPaths.data], - configDirs: [testPaths.config], - cacheDirs: [testPaths.cache], - stateDirs: [testPaths.state], + getOpencodeRuntimeDirs: () => ({ + dataDir: testPaths.data, + configDir: testPaths.config, + cacheDir: testPaths.cache, + stateDir: testPaths.state, }), })); diff --git a/tests/lib.google-companion.test.ts b/tests/lib.google-companion.test.ts index 3edb7240..902054f2 100644 --- a/tests/lib.google-companion.test.ts +++ b/tests/lib.google-companion.test.ts @@ -5,14 +5,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const moduleMocks = vi.hoisted(() => ({ resolveImpl: vi.fn<(specifier: string, options?: { paths?: string[] }) => string>(), - runtimeDirs: { value: { cacheDirs: [] as string[] } }, + runtimeDirs: { value: { cacheDir: "" } }, })); vi.mock("node:module", () => ({ createRequire: () => ({ resolve: moduleMocks.resolveImpl }), })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => moduleMocks.runtimeDirs.value, + getOpencodeRuntimeDirs: () => moduleMocks.runtimeDirs.value, })); type Presence = { @@ -135,11 +135,11 @@ function writeCredentials( async function loadWith( companion: CompanionCase, resolveImpl: (specifier: string, options?: { paths?: string[] }) => string, - cacheDirs: string[] = [], + cacheDir: string = join(tmpdir(), "opencode-quota-google-companion-missing-cache"), ): Promise { vi.resetModules(); moduleMocks.resolveImpl.mockReset(); - moduleMocks.runtimeDirs.value = { cacheDirs }; + moduleMocks.runtimeDirs.value = { cacheDir }; moduleMocks.resolveImpl.mockImplementation(resolveImpl); return companion.load(); } @@ -291,7 +291,7 @@ describe("google companion credential resolution", () => { if (specifier === companion.sourceSpecifier) return sourcePath; throw moduleNotFound(); }, - [cacheDir], + cacheDir, ); await expectConfigured(loaded, sourcePath, "-source"); } @@ -305,9 +305,13 @@ describe("google companion credential resolution", () => { const fallbackPath = join(packageRoot, ...companion.runtimeCandidates[1]!); mkdirSync(unreadablePath, { recursive: true }); writeCredentials(fallbackPath, companion, "var"); - const loaded = await loadWith(companion, () => { - throw moduleNotFound(); - }, [cacheDir]); + const loaded = await loadWith( + companion, + () => { + throw moduleNotFound(); + }, + cacheDir, + ); await expectConfigured(loaded, fallbackPath); } }); @@ -340,9 +344,13 @@ describe("google companion credential resolution", () => { : packageRoot; const candidatePath = join(credentialRoot, "dist", "index.js"); writeCredentials(candidatePath, companion, "var"); - const loaded = await loadWith(companion, () => { - throw moduleNotFound(); - }, [cacheDir]); + const loaded = await loadWith( + companion, + () => { + throw moduleNotFound(); + }, + cacheDir, + ); await expectConfigured(loaded, candidatePath); } }); @@ -352,9 +360,13 @@ describe("google companion credential resolution", () => { const cacheDir = join(tempDir, "gemini-runtime-cache"); const rootBundle = join(cacheDir, "node_modules", companion.packageName, "dist", "index.js"); writeCredentials(rootBundle, companion, "var", "-root"); - let loaded = await loadWith(companion, () => { - throw moduleNotFound(); - }, [cacheDir]); + let loaded = await loadWith( + companion, + () => { + throw moduleNotFound(); + }, + cacheDir, + ); await expectConfigured(loaded, rootBundle, "-root"); const resolvedRoot = join(tempDir, "gemini-resolved-root", "dist", "index.js"); @@ -369,7 +381,7 @@ describe("google companion credential resolution", () => { if (specifier.startsWith(`${companion.packageName}/`)) throw packagePathNotExported(); throw moduleNotFound(); }, - [resolvedCacheDir], + resolvedCacheDir, ); await expectConfigured(loaded, resolvedRoot, "-resolved"); expect(moduleMocks.resolveImpl).toHaveBeenCalledWith(companion.packageName, { diff --git a/tests/lib.mimo-config.test.ts b/tests/lib.mimo-config.test.ts index 9b931503..a21891a9 100644 --- a/tests/lib.mimo-config.test.ts +++ b/tests/lib.mimo-config.test.ts @@ -5,11 +5,11 @@ import { join } from "path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const runtimePathMocks = vi.hoisted(() => ({ - getOpencodeRuntimeDirCandidates: vi.fn(), + getOpencodeRuntimeDirs: vi.fn(), })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: runtimePathMocks.getOpencodeRuntimeDirCandidates, + getOpencodeRuntimeDirs: runtimePathMocks.getOpencodeRuntimeDirs, })); const originalEnv = process.env; @@ -36,7 +36,9 @@ describe("MiMo config resolution", () => { vi.clearAllMocks(); process.env = { ...originalEnv }; delete process.env.MIMO_USAGE_COOKIE; - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ configDirs: [] }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ + configDir: join(tmpdir(), "mimo-config-missing"), + }); }); afterEach(async () => { @@ -81,7 +83,7 @@ describe("MiMo config resolution", () => { it("prefers the environment and never reads a lower-priority valid file", async () => { const [primary] = await createConfigDirs(); await writeFile(configPath(primary), JSON.stringify({ cookie: requiredCookie })); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ configDirs: [primary] }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); process.env.MIMO_USAGE_COOKIE = "Cookie: userId=env-user; ignored=value; api-platform_serviceToken=env-service"; @@ -99,7 +101,7 @@ describe("MiMo config resolution", () => { it("treats a defined invalid environment value as blocking", async () => { const [primary] = await createConfigDirs(); await writeFile(configPath(primary), JSON.stringify({ cookie: requiredCookie })); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ configDirs: [primary] }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); process.env.MIMO_USAGE_COOKIE = " "; const { resolveMimoConfig } = await import("../src/lib/mimo-config.js"); @@ -120,7 +122,7 @@ describe("MiMo config resolution", () => { "api-platform_ph=ph; userId=file-user; api-platform_serviceToken=file-service; other=no", }), ); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ configDirs: [primary] }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveMimoConfig } = await import("../src/lib/mimo-config.js"); @@ -137,9 +139,7 @@ describe("MiMo config resolution", () => { const [primary, fallback] = await createConfigDirs(); await writeFile(configPath(primary), "{"); await writeFile(configPath(fallback), JSON.stringify({ cookie: requiredCookie })); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary, fallback], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveMimoConfig } = await import("../src/lib/mimo-config.js"); @@ -167,7 +167,7 @@ describe("MiMo config resolution", () => { ])("rejects %s without fallback", async (_label, body, error) => { const [primary] = await createConfigDirs(); await writeFile(configPath(primary), body); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ configDirs: [primary] }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveMimoConfig } = await import("../src/lib/mimo-config.js"); @@ -182,7 +182,7 @@ describe("MiMo config resolution", () => { const [primary] = await createConfigDirs(); const path = configPath(primary); await writeFile(path, JSON.stringify({ cookie: requiredCookie })); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ configDirs: [primary] }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveMimoConfigCached } = await import("../src/lib/mimo-config.js"); const first = await resolveMimoConfigCached({ maxAgeMs: 5_000 }); @@ -197,10 +197,8 @@ describe("MiMo config resolution", () => { }); it("reports trusted checked paths without exposing cookie names or values", async () => { - const [primary, fallback] = await createConfigDirs(); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary, fallback], - }); + const [primary] = await createConfigDirs(); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); process.env.MIMO_USAGE_COOKIE = requiredCookie; const { getMimoConfigDiagnostics } = await import("../src/lib/mimo-config.js"); @@ -211,7 +209,7 @@ describe("MiMo config resolution", () => { state: "configured", source: "env:MIMO_USAGE_COOKIE", error: null, - checkedPaths: [configPath(primary), configPath(fallback)], + checkedPaths: [configPath(primary)], }); expect(serialized).not.toContain("service-secret"); expect(serialized).not.toContain("user-secret"); diff --git a/tests/lib.mimo.security.test.ts b/tests/lib.mimo.security.test.ts index d0e22ae0..280d9524 100644 --- a/tests/lib.mimo.security.test.ts +++ b/tests/lib.mimo.security.test.ts @@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ readFile: vi.fn(), - getOpencodeRuntimeDirCandidates: vi.fn(), + getOpencodeRuntimeDirs: vi.fn(), })); vi.mock("fs/promises", () => ({ @@ -12,12 +12,11 @@ vi.mock("fs/promises", () => ({ })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: mocks.getOpencodeRuntimeDirCandidates, + getOpencodeRuntimeDirs: mocks.getOpencodeRuntimeDirs, })); const originalEnv = process.env; const primaryConfigDir = join(tmpdir(), "trusted", "primary"); -const fallbackConfigDir = join(tmpdir(), "trusted", "fallback"); describe("MiMo credential security boundary", () => { beforeEach(() => { @@ -25,9 +24,7 @@ describe("MiMo credential security boundary", () => { vi.clearAllMocks(); process.env = { ...originalEnv }; delete process.env.MIMO_USAGE_COOKIE; - mocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primaryConfigDir, fallbackConfigDir], - }); + mocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primaryConfigDir }); mocks.readFile.mockImplementation(async () => { const error = new Error("missing") as NodeJS.ErrnoException; error.code = "ENOENT"; @@ -45,7 +42,6 @@ describe("MiMo credential security boundary", () => { await expect(resolveMimoConfig()).resolves.toEqual({ state: "none" }); expect(mocks.readFile.mock.calls.map((call) => call[0])).toEqual([ join(primaryConfigDir, "opencode-quota", "mimo.json"), - join(fallbackConfigDir, "opencode-quota", "mimo.json"), ]); const checked = JSON.stringify(mocks.readFile.mock.calls); diff --git a/tests/lib.opencode-config-providers.test.ts b/tests/lib.opencode-config-providers.test.ts index 9daf84d2..3bc13b72 100644 --- a/tests/lib.opencode-config-providers.test.ts +++ b/tests/lib.opencode-config-providers.test.ts @@ -5,15 +5,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const runtimeDirs = vi.hoisted(() => ({ value: { - dataDirs: [] as string[], - configDirs: [] as string[], - cacheDirs: [] as string[], - stateDirs: [] as string[], + dataDir: "", + configDir: "", + cacheDir: "", + stateDir: "", }, })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => runtimeDirs.value, + getOpencodeRuntimeDirs: () => runtimeDirs.value, })); import { extractProviderIdsFromParsedConfig } from "../src/lib/config-file-utils.js"; @@ -35,10 +35,10 @@ describe("opencode config provider discovery", () => { mkdirSync(globalConfigDir, { recursive: true }); mkdirSync(workspaceDir, { recursive: true }); runtimeDirs.value = { - dataDirs: [], - configDirs: [globalConfigDir], - cacheDirs: [], - stateDirs: [], + dataDir: join(tempDir, "data"), + configDir: globalConfigDir, + cacheDir: join(tempDir, "cache"), + stateDir: join(tempDir, "state"), }; }); diff --git a/tests/lib.opencode-go-auth.test.ts b/tests/lib.opencode-go-auth.test.ts index b9baa07f..77613eca 100644 --- a/tests/lib.opencode-go-auth.test.ts +++ b/tests/lib.opencode-go-auth.test.ts @@ -242,8 +242,7 @@ describe("OpenCode Go auth resolution", () => { const readsBeforeAudit = authMocks.readFile.mock.calls.length; const findings = await auditObsoleteUpdateSources({ env: process.env, - configDirs: [configDir], - primaryConfigDir: configDir, + configDir, }); expect(authMocks.readFile).toHaveBeenCalledTimes(readsBeforeAudit); expect(process.env.OPENCODE_GO_WORKSPACE_ID).toBe("obsolete-go-workspace-canary"); diff --git a/tests/lib.opencode-runtime-paths.test.ts b/tests/lib.opencode-runtime-paths.test.ts index 7b84ad40..8fc99894 100644 --- a/tests/lib.opencode-runtime-paths.test.ts +++ b/tests/lib.opencode-runtime-paths.test.ts @@ -1,13 +1,10 @@ import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; -import { - getOpencodeRuntimeDirCandidates, - getOpencodeRuntimeDirs, -} from "../src/lib/opencode-runtime-paths.js"; +import { getOpencodeRuntimeDirs } from "../src/lib/opencode-runtime-paths.js"; describe("opencode-runtime-paths", () => { - it("builds deterministic dirs from XDG env fallbacks", () => { + it("builds deterministic dirs from XDG env", () => { const env: NodeJS.ProcessEnv = { XDG_DATA_HOME: "/x/data", XDG_CONFIG_HOME: "/x/config", @@ -24,7 +21,27 @@ describe("opencode-runtime-paths", () => { }); }); - it("uses OPENCODE_CONFIG_DIR as the primary global config directory", () => { + it("falls back to the XDG defaults under the home dir, like OpenCode 2 on every platform", () => { + const dirs = getOpencodeRuntimeDirs({ env: {}, homeDir: "/home/test" }); + expect(dirs).toEqual({ + dataDir: join("/home/test", ".local", "share", "opencode"), + configDir: join("/home/test", ".config", "opencode"), + cacheDir: join("/home/test", ".cache", "opencode"), + stateDir: join("/home/test", ".local", "state", "opencode"), + }); + }); + + it("ignores Windows APPDATA/LOCALAPPDATA", () => { + const dirs = getOpencodeRuntimeDirs({ + env: { APPDATA: "C:/Users/u/AppData/Roaming", LOCALAPPDATA: "C:/Users/u/AppData/Local" }, + homeDir: "/home/test", + }); + expect(dirs.configDir).toBe(join("/home/test", ".config", "opencode")); + expect(dirs.cacheDir).toBe(join("/home/test", ".cache", "opencode")); + expect(dirs.stateDir).toBe(join("/home/test", ".local", "state", "opencode")); + }); + + it("uses OPENCODE_CONFIG_DIR as the only global config directory", () => { const absoluteConfigDir = resolve("custom", "opencode"); const absolute = getOpencodeRuntimeDirs({ env: { @@ -42,42 +59,12 @@ describe("opencode-runtime-paths", () => { }, homeDir: "/home/test", }); - expect(relative.configDir).toBe(resolve(join("/x/config", "opencode"), "work-profile")); + expect(relative.configDir).toBe(resolve(process.cwd(), "work-profile")); - const candidates = getOpencodeRuntimeDirCandidates({ - platform: "linux", - env: { - XDG_CONFIG_HOME: "/x/config", - OPENCODE_CONFIG_DIR: absoluteConfigDir, - }, + const blank = getOpencodeRuntimeDirs({ + env: { XDG_CONFIG_HOME: "/x/config", OPENCODE_CONFIG_DIR: " " }, homeDir: "/home/test", }); - expect(candidates.configDirs[0]).toBe(absoluteConfigDir); - }); - - it("includes Windows APPDATA/LOCALAPPDATA fallbacks after primary", () => { - const env: NodeJS.ProcessEnv = { - XDG_DATA_HOME: "C:/Users/u.local/share", - XDG_CONFIG_HOME: "C:/Users/u.local/config", - XDG_CACHE_HOME: "C:/Users/u.local/cache", - XDG_STATE_HOME: "C:/Users/u.local/state", - APPDATA: "C:/Users/u/AppData/Roaming", - LOCALAPPDATA: "C:/Users/u/AppData/Local", - }; - - const primary = getOpencodeRuntimeDirs({ env, homeDir: "C:/Users/u" }); - const c = getOpencodeRuntimeDirCandidates({ - platform: "win32", - env, - homeDir: "C:/Users/u", - primary, - }); - - expect(c.configDirs[0]).toBe(primary.configDir); - expect(c.cacheDirs[0]).toBe(primary.cacheDir); - expect(c.stateDirs[0]).toBe(primary.stateDir); - - expect(c.configDirs).toContain(join(env.APPDATA!, "opencode")); - expect(c.configDirs).toContain(join(env.LOCALAPPDATA!, "opencode")); + expect(blank.configDir).toBe(join("/x/config", "opencode")); }); }); diff --git a/tests/lib.opencode-zen-config.test.ts b/tests/lib.opencode-zen-config.test.ts index 627b64c6..0c98adb5 100644 --- a/tests/lib.opencode-zen-config.test.ts +++ b/tests/lib.opencode-zen-config.test.ts @@ -5,11 +5,11 @@ import { join } from "path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const runtimePathMocks = vi.hoisted(() => ({ - getOpencodeRuntimeDirCandidates: vi.fn(), + getOpencodeRuntimeDirs: vi.fn(), })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: runtimePathMocks.getOpencodeRuntimeDirCandidates, + getOpencodeRuntimeDirs: runtimePathMocks.getOpencodeRuntimeDirs, })); const originalEnv = process.env; @@ -38,7 +38,9 @@ describe("opencode-zen config resolution", () => { delete process.env.OPENCODE_AUTH_COOKIE; delete process.env.OPENCODE_GO_WORKSPACE_ID; delete process.env.OPENCODE_GO_AUTH_COOKIE; - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ configDirs: [] }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ + configDir: join(tmpdir(), "opencode-zen-config-missing"), + }); }); afterEach(async () => { @@ -57,9 +59,7 @@ describe("opencode-zen config resolution", () => { path, JSON.stringify({ workspaceId: "wrk_file", consoleSessionCookie: "cookie-file" }), ); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); @@ -86,9 +86,7 @@ describe("opencode-zen config resolution", () => { path, JSON.stringify({ workspaceId: " wrk_file ", consoleSessionCookie: " cookie-file " }), ); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); @@ -103,9 +101,7 @@ describe("opencode-zen config resolution", () => { const [primary] = await createConfigDirs(); const path = configPath(primary); await writeFile(path, JSON.stringify({ workspaceId: 123, consoleSessionCookie: "cookie" })); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); @@ -120,9 +116,7 @@ describe("opencode-zen config resolution", () => { const [primary] = await createConfigDirs(); const path = configPath(primary); await writeFile(path, JSON.stringify({ workspaceId: "wrk_file", authCookie: "old-cookie" })); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { getOpenCodeZenConfigDiagnostics, resolveOpenCodeZenConfig } = await import( "../src/lib/opencode-zen-config.js" @@ -151,9 +145,7 @@ describe("opencode-zen config resolution", () => { consoleSessionCookie: "session-file", }), ); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); @@ -171,9 +163,7 @@ describe("opencode-zen config resolution", () => { configPath(fallback), JSON.stringify({ workspaceId: "wrk_ok", consoleSessionCookie: "cookie-ok" }), ); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary, fallback], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); @@ -188,9 +178,7 @@ describe("opencode-zen config resolution", () => { const [primary] = await createConfigDirs(); const path = configPath(primary); await writeFile(path, '{"consoleSessionCookie":super-secret}'); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); @@ -217,7 +205,7 @@ describe("opencode-zen config resolution", () => { JSON.stringify({ workspaceId: zenWorkspaceCanary, consoleSessionCookie: zenCookieCanary }), ); await writeFile(obsoleteGoPath, JSON.stringify({ authCookie: goFileCanary })); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ configDirs: [primary] }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { getOpenCodeZenConfigDiagnostics, resolveOpenCodeZenConfig } = await import( "../src/lib/opencode-zen-config.js" @@ -229,8 +217,7 @@ describe("opencode-zen config resolution", () => { const { auditObsoleteUpdateSources } = await import("../src/lib/scoped-update-migration.js"); const findings = await auditObsoleteUpdateSources({ env: process.env, - configDirs: [primary], - primaryConfigDir: primary, + configDir: primary, }); expect(await readFile(path)).toEqual(zenBytesBeforeAudit); expect(await readFile(obsoleteGoPath)).toEqual(goBytesBeforeAudit); @@ -278,9 +265,7 @@ describe("opencode-zen config resolution", () => { path, JSON.stringify({ workspaceId: "wrk_initial", consoleSessionCookie: "cookie-initial" }), ); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { resolveOpenCodeZenConfigCached } = await import("../src/lib/opencode-zen-config.js"); const first = await resolveOpenCodeZenConfigCached({ maxAgeMs: 5_000 }); @@ -299,9 +284,7 @@ describe("opencode-zen config resolution", () => { path, JSON.stringify({ workspaceId: "wrk_secret", consoleSessionCookie: "cookie-secret" }), ); - runtimePathMocks.getOpencodeRuntimeDirCandidates.mockReturnValue({ - configDirs: [primary], - }); + runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); const { getOpenCodeZenConfigDiagnostics } = await import("../src/lib/opencode-zen-config.js"); const diagnostics = await getOpenCodeZenConfigDiagnostics(); diff --git a/tests/lib.quota-export.production-policy.test.ts b/tests/lib.quota-export.production-policy.test.ts index 320a3dc4..292a44b6 100644 --- a/tests/lib.quota-export.production-policy.test.ts +++ b/tests/lib.quota-export.production-policy.test.ts @@ -13,12 +13,6 @@ const RESOLVED_AUTH_PROVIDER_IDS = [ const UNCACHED_CANONICAL_PROVIDER_IDS = ["cursor", "alibaba-token-plan"] as const; vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: [`${TEST_RUNTIME_ROOT}/data`], - configDirs: [`${TEST_RUNTIME_ROOT}/config`], - cacheDirs: [`${TEST_RUNTIME_ROOT}/cache`], - stateDirs: [`${TEST_RUNTIME_ROOT}/state`], - }), getOpencodeRuntimeDirs: () => ({ dataDir: `${TEST_RUNTIME_ROOT}/data`, configDir: `${TEST_RUNTIME_ROOT}/config`, diff --git a/tests/lib.quota-providers-auth.test.ts b/tests/lib.quota-providers-auth.test.ts index e52f84a9..c311242f 100644 --- a/tests/lib.quota-providers-auth.test.ts +++ b/tests/lib.quota-providers-auth.test.ts @@ -3,12 +3,6 @@ import { join } from "path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: [join(homedir(), ".local", "share", "opencode")], - configDirs: [join(homedir(), ".config", "opencode")], - cacheDirs: [join(homedir(), ".cache", "opencode")], - stateDirs: [join(homedir(), ".local", "state", "opencode")], - }), getOpencodeRuntimeDirs: () => ({ dataDir: join(homedir(), ".local", "share", "opencode"), configDir: join(homedir(), ".config", "opencode"), diff --git a/tests/lib.quota-providers.integration.test.ts b/tests/lib.quota-providers.integration.test.ts index b9ada417..014c6df5 100644 --- a/tests/lib.quota-providers.integration.test.ts +++ b/tests/lib.quota-providers.integration.test.ts @@ -15,15 +15,15 @@ import { const runtimeDirs = vi.hoisted(() => ({ value: { - dataDirs: [] as string[], - configDirs: [] as string[], - cacheDirs: [] as string[], - stateDirs: [] as string[], + dataDir: "", + configDir: "", + cacheDir: "", + stateDir: "", }, })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => runtimeDirs.value, + getOpencodeRuntimeDirs: () => runtimeDirs.value, })); import { createLoadConfigMeta, loadConfig } from "../src/lib/config.js"; diff --git a/tests/lib.quota-providers.security.test.ts b/tests/lib.quota-providers.security.test.ts index d5f1a3ac..43bf5974 100644 --- a/tests/lib.quota-providers.security.test.ts +++ b/tests/lib.quota-providers.security.test.ts @@ -11,15 +11,15 @@ import { const runtimeDirs = vi.hoisted(() => ({ value: { - dataDirs: [] as string[], - configDirs: [] as string[], - cacheDirs: [] as string[], - stateDirs: [] as string[], + dataDir: "", + configDir: "", + cacheDir: "", + stateDir: "", }, })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => runtimeDirs.value, + getOpencodeRuntimeDirs: () => runtimeDirs.value, })); import { createLoadConfigMeta, loadConfig } from "../src/lib/config.js"; diff --git a/tests/lib.quota-runtime-context.test.ts b/tests/lib.quota-runtime-context.test.ts index 607d9b0d..b8167439 100644 --- a/tests/lib.quota-runtime-context.test.ts +++ b/tests/lib.quota-runtime-context.test.ts @@ -147,29 +147,23 @@ describe("quota runtime context", () => { expect(runtime.configMeta.settingSources.enabled).toBe(worktreeConfigPath); }); - it("does not re-resolve OPENCODE_CONFIG_DIR when loadConfig receives resolved configRootDir", async () => { - process.env.OPENCODE_CONFIG_DIR = ".opencode"; - mkdirSync(join(worktreeDir, ".opencode", ".opencode"), { recursive: true }); + it("uses OPENCODE_CONFIG_DIR as the global config dir and keeps the worktree config root", async () => { + const customGlobalDir = join(tempDir, "custom-global"); + process.env.OPENCODE_CONFIG_DIR = customGlobalDir; + mkdirSync(customGlobalDir, { recursive: true }); writeFileSync( - join(worktreeDir, ".opencode", "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: false, - }, - }, - }), + join(customGlobalDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), "utf8", ); writeFileSync( - join(worktreeDir, ".opencode", ".opencode", "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabled: true, - }, - }, - }), + join(xdgConfigHome, "opencode", "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { minIntervalMs: 12_345 } } }), + "utf8", + ); + writeFileSync( + join(worktreeDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { formatStyle: "allWindows" } } }), "utf8", ); @@ -183,15 +177,12 @@ describe("quota runtime context", () => { providers: [], }); - expect(runtime.roots).toEqual({ - workspaceRoot: worktreeDir, - configRoot: join(worktreeDir, ".opencode"), - }); + expect(runtime.roots).toEqual({ workspaceRoot: worktreeDir, configRoot: worktreeDir }); expect(runtime.config.enabled).toBe(false); - expect(runtime.configMeta.paths).toContain(quotaConfigSource(join(worktreeDir, ".opencode"))); - expect(runtime.configMeta.paths).not.toContain( - quotaConfigSource(join(worktreeDir, ".opencode", ".opencode")), - ); + expect(runtime.config.formatStyle).toBe("allWindows"); + expect(runtime.config.minIntervalMs).not.toBe(12_345); + expect(runtime.configMeta.globalConfigPaths).toEqual([quotaConfigSource(customGlobalDir)]); + expect(runtime.configMeta.workspaceConfigPaths).toEqual([quotaConfigSource(worktreeDir)]); }); it("propagates provider config but keeps accounting detail out of provider and cache context", () => { diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index cb9b982b..81588b0c 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -67,9 +67,6 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ cacheDir: "/tmp/cache", stateDir: "/tmp/state", }), - getOpencodeRuntimeDirCandidates: () => ({ - configDirs: ["/tmp/config"], - }), })); vi.mock("../src/lib/synthetic.js", () => ({ diff --git a/tests/lib.scoped-update-migration.test.ts b/tests/lib.scoped-update-migration.test.ts index 2c084ee1..c09bbb9a 100644 --- a/tests/lib.scoped-update-migration.test.ts +++ b/tests/lib.scoped-update-migration.test.ts @@ -249,7 +249,7 @@ describe("legacy display inspection", () => { describe("migration candidate discovery", () => { it("reuses global-then-workspace candidate order and runtime relative paths", () => { const candidates = buildScopedUpdateMigrationCandidates({ - globalRoots: ["/global-a", "/global-b"], + globalRoot: "/global-a", workspaceRoot: "/workspace", }); @@ -285,30 +285,6 @@ describe("migration candidate discovery", () => { scope: "global", format: "jsonc", }, - { - path: join("/global-b", "opencode-quota/quota-toast.jsonc"), - container: "quota-root", - scope: "global", - format: "jsonc", - }, - { - path: join("/global-b", "opencode-quota/quota-toast.json"), - container: "quota-root", - scope: "global", - format: "json", - }, - { - path: join("/global-b", "opencode.json"), - container: "experimental.quotaToast", - scope: "global", - format: "json", - }, - { - path: join("/global-b", "opencode.jsonc"), - container: "experimental.quotaToast", - scope: "global", - format: "jsonc", - }, { path: join("/workspace", "opencode-quota/quota-toast.jsonc"), container: "quota-root", @@ -339,7 +315,7 @@ describe("migration candidate discovery", () => { it("does not duplicate a workspace root already present globally", () => { const root = tempDir(); const candidates = buildScopedUpdateMigrationCandidates({ - globalRoots: [root], + globalRoot: root, workspaceRoot: root, }); @@ -357,7 +333,7 @@ describe("migration candidate discovery", () => { write(workspaceFile, "{}"); const result = await discoverExistingScopedUpdateMigrationCandidates({ - globalRoots: [globalRoot], + globalRoot: globalRoot, workspaceRoot, }); @@ -378,7 +354,7 @@ describe("migration candidate discovery", () => { symlinkSync(outsideParent, join(globalRoot, "opencode-quota")); const result = await discoverExistingScopedUpdateMigrationCandidates({ - globalRoots: [globalRoot], + globalRoot: globalRoot, workspaceRoot: join(root, "workspace"), }); @@ -403,7 +379,7 @@ describe("migration candidate discovery", () => { symlinkSync(workspaceFile, symlinkPath); const result = await discoverExistingScopedUpdateMigrationCandidates({ - globalRoots: [globalRoot], + globalRoot: globalRoot, workspaceRoot, selectedPackagePaths: [symlinkPath], }); @@ -438,7 +414,7 @@ describe("migration candidate discovery", () => { symlinkSync(workspaceFile, symlinkPath); const result = await discoverExistingScopedUpdateMigrationCandidates({ - globalRoots: [globalRoot], + globalRoot: globalRoot, workspaceRoot, selectedPackagePaths: [workspaceFile], }); @@ -506,8 +482,7 @@ describe("obsolete source audit", () => { const findings = await auditObsoleteUpdateSources({ env, - configDirs: [root], - primaryConfigDir: root, + configDir: root, }); expect(findings.filter((finding) => finding.kind === "obsolete-go-env")).toEqual( @@ -516,19 +491,14 @@ describe("obsolete source audit", () => { expect(JSON.stringify(findings)).not.toContain("go-secret-canary"); }); - it("reports the exact obsolete Go path under every global config candidate", async () => { + it("reports the exact obsolete Go path under the global config dir", async () => { const root = tempDir(); - const configDirs = [join(root, "first"), join(root, "second")]; - const paths = configDirs.map((dir) => join(dir, OBSOLETE_GO_FILE)); - for (const path of paths) write(path, "legacy-file-secret-canary"); + const path = join(root, OBSOLETE_GO_FILE); + write(path, "legacy-file-secret-canary"); - const findings = await auditObsoleteUpdateSources({ - env: {}, - configDirs, - primaryConfigDir: configDirs[0] ?? root, - }); + const findings = await auditObsoleteUpdateSources({ env: {}, configDir: root }); - expect(findings).toEqual(paths.map((path) => ({ kind: "obsolete-go-file", path }))); + expect(findings).toEqual([{ kind: "obsolete-go-file", path }]); expect(JSON.stringify(findings)).not.toContain("legacy-file-secret-canary"); }); @@ -539,8 +509,7 @@ describe("obsolete source audit", () => { const findings = await auditObsoleteUpdateSources({ env: {}, - configDirs: [globalRoot], - primaryConfigDir: globalRoot, + configDir: globalRoot, }); expect(findings).toEqual([]); @@ -557,8 +526,7 @@ describe("obsolete source audit", () => { const findings = await auditObsoleteUpdateSources({ env, - configDirs: [root], - primaryConfigDir: root, + configDir: root, }); expect(findings).toEqual([ @@ -575,11 +543,10 @@ describe("obsolete source audit", () => { "malformed", "incomplete", "symlink", - ])("suppresses ambiguous Zen findings when any supported file path exists: %s", async (kind) => { + ])("suppresses ambiguous Zen findings when the supported file path exists: %s", async (kind) => { const root = tempDir(); - const first = join(root, "first"); - const second = join(root, "second"); - const supportedPath = join(second, SUPPORTED_ZEN_FILE); + const configDir = join(root, "config"); + const supportedPath = join(configDir, SUPPORTED_ZEN_FILE); if (kind === "symlink") { const target = join(root, "target.json"); write(target, "supported-file-secret-canary"); @@ -594,31 +561,11 @@ describe("obsolete source audit", () => { OPENCODE_WORKSPACE_ID: "zen-workspace-secret", OPENCODE_AUTH_COOKIE: "zen-cookie-secret", }, - configDirs: [first, second], - primaryConfigDir: first, + configDir, }); expect(findings).toEqual([]); }); - - it("deduplicates config paths while preserving first config-dir order", async () => { - const root = tempDir(); - const first = join(root, "first"); - const second = join(root, "second"); - write(join(first, OBSOLETE_GO_FILE), "one"); - write(join(second, OBSOLETE_GO_FILE), "two"); - - const findings = await auditObsoleteUpdateSources({ - env: {}, - configDirs: [first, first, second], - primaryConfigDir: first, - }); - - expect(findings).toEqual([ - { kind: "obsolete-go-file", path: join(first, OBSOLETE_GO_FILE) }, - { kind: "obsolete-go-file", path: join(second, OBSOLETE_GO_FILE) }, - ]); - }); }); describe("presentation sorting", () => { diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index d5e2ded4..3fee7fc5 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -56,7 +56,7 @@ vi.mock("../src/lib/alibaba-auth.js", () => createAlibabaAuthModuleMock(mocks.resolveAlibabaCodingPlanAuthCached), ); vi.mock("../src/lib/opencode-runtime-paths.js", () => - createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT, { includeCandidates: true }), + createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT), ); vi.mock("../src/lib/maintainer-announcements.js", () => ({ BUNDLED_MAINTAINER_ANNOUNCEMENTS: [TEST_ANNOUNCEMENT], diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 2f850018..083b24a8 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -43,7 +43,7 @@ vi.mock("../src/lib/modelsdev-pricing.js", async (importOriginal) => ({ ...createPricingModuleMock(mocks), })); vi.mock("../src/lib/opencode-runtime-paths.js", () => - createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT, { includeCandidates: true }), + createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT), ); vi.mock("../src/lib/opencode-go-auth.js", () => ({ DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS: 5_000, diff --git a/tests/providers.quota-providers.test.ts b/tests/providers.quota-providers.test.ts index 33bcd6b6..5b0b3ea7 100644 --- a/tests/providers.quota-providers.test.ts +++ b/tests/providers.quota-providers.test.ts @@ -18,12 +18,6 @@ const runtimeMocks = vi.hoisted(() => ({ })); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: [`${TEST_RUNTIME_ROOT}/data`], - configDirs: [`${TEST_RUNTIME_ROOT}/config`], - cacheDirs: [`${TEST_RUNTIME_ROOT}/cache`], - stateDirs: [`${TEST_RUNTIME_ROOT}/state`], - }), getOpencodeRuntimeDirs: () => ({ dataDir: `${TEST_RUNTIME_ROOT}/data`, configDir: `${TEST_RUNTIME_ROOT}/config`, diff --git a/tests/quota-state.test.ts b/tests/quota-state.test.ts index 20ee43ac..dd7a3f3a 100644 --- a/tests/quota-state.test.ts +++ b/tests/quota-state.test.ts @@ -16,12 +16,6 @@ const TEST_ACCOUNTING = { } as const; vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: () => ({ - dataDirs: [`${TEST_RUNTIME_ROOT}/data`], - configDirs: [`${TEST_RUNTIME_ROOT}/config`], - cacheDirs: [`${TEST_RUNTIME_ROOT}/cache`], - stateDirs: [`${TEST_RUNTIME_ROOT}/state`], - }), getOpencodeRuntimeDirs: () => ({ dataDir: `${TEST_RUNTIME_ROOT}/data`, configDir: `${TEST_RUNTIME_ROOT}/config`, diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index c3960e6c..6e4d93cb 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -113,7 +113,7 @@ vi.mock("../src/lib/anthropic.js", () => ({ queryAnthropicQuota: mocks.queryAnthropicQuota, })); vi.mock("../src/lib/opencode-runtime-paths.js", () => - createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT, { includeCandidates: true }), + createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT), ); const renderedSurfaces = vi.hoisted(() => ({ sidebar: [] as string[], compact: [] as string[] })); From 7bfd2b6dd113c9252dbc624949f409b8653f13b3 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:07:19 +0200 Subject: [PATCH 44/97] fix: address v5.0.0 review findings 1. Fixed: API-key config lookup reads OpenCode 2 native providers..settings.apiKey; a native entry replaces the legacy provider..options entry (OC2 normalize.ts mergeMaps); env priority, template allowlist and global-only trust kept; shared resolver, Alibaba and MiniMax use it; provider/plugin detectors also read providers. 2. Fixed: Home footer uses a Home runtime (onlyCurrentModel false, showSessionTokens false, no session) as in v4, matching createExportProviderContext cache keys. 3. Fixed: server and every TUI entry point share resolveOpenCodeLocationRoots (Git worktree root, else location dir); OC2 TUI exposes only location.directory, no worktree. 4. Fixed: update edits canonical specs in native plugins entries too (package leaf only, options kept); init, provider inference and Cursor detection recognize plugins; safeguards unchanged. 5. Fixed: footer and sidebar refreshes use v4's guard (disposed flag, load version, coalesced overlapping loads); no setLines or export write after unmount. 6. Fixed: only /tokens_between prompts; /quota_announcements and /pricing_refresh keep typed-argument forwarding and run without a prompt (their handlers reject any argument). 7. Fixed: engines.node is ^22.13.0 || >=23.4.0 (node:sqlite unflagged in 22.13.0 and 23.4.0); tests, README, CONTRIBUTING and packed smoke aligned. --- CONTRIBUTING.md | 4 +- README.md | 2 +- docs/readme/configuration.md | 2 + docs/readme/updating.md | 2 +- package.json | 2 +- scripts/smoke-packed-package.mjs | 2 +- src/lib/api-key-resolver.ts | 22 +++- src/lib/config-file-utils.ts | 32 ++++- src/lib/cursor-detection.ts | 11 +- src/lib/init-installer.ts | 27 ++-- src/lib/minimax-auth.ts | 10 +- src/lib/opencode-config-providers.ts | 14 ++ src/lib/scoped-update.ts | 23 +++- src/plugin.ts | 10 +- src/tui-v2.tsx | 123 +++++++++++++----- tests/lib.api-key-resolver.test.ts | 50 +++++++ tests/lib.cursor-detection.test.ts | 17 +++ tests/lib.init-installer.test.ts | 29 +++++ tests/lib.minimax-auth.test.ts | 17 +++ tests/lib.opencode-config-providers.test.ts | 23 ++++ tests/lib.scoped-update.test.ts | 21 +++ tests/package-manifest.test.ts | 6 +- tests/plugin.command-handled-boundary.test.ts | 3 +- tests/tui-v2-commands.test.ts | 45 ++++++- tests/tui-v2-refresh.test.ts | 98 +++++++++++++- tests/v4-migration-docs.test.ts | 2 +- 26 files changed, 512 insertions(+), 85 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3983451..a2813dd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ Thanks for contributing. This repo has strict local-only behavior and regression ## Development Setup -- The published package runtime supports Node.js `>=22.13.0` (matches `package.json` engines). +- The published package runtime supports Node.js `^22.13.0 || >=23.4.0` (matches `package.json` engines; `node:sqlite` needs no flag from those versions). - Repository development uses pnpm v11, which requires Node.js `>=22` for the pnpm CLI. - Enable the pinned package manager and install dependencies with: @@ -59,7 +59,7 @@ PR and `main` pushes trigger `.github/workflows/ci.yml` (`CI` workflow): - Job: `pnpm-quality` on Node `24.x` - Steps: frozen install, `pnpm verify`, then one exact npm artifact pack and upload - Job: `runtime-smoke` on Node `22.x` and `24.x` -- Runtime smoke installs that exact packed artifact as a consumer and verifies the default/server imports, TUI export payload, CLI help, and `engines.node >=22.13.0` +- Runtime smoke installs that exact packed artifact as a consumer and verifies the default/server imports, TUI export payload, CLI help, and `engines.node ^22.13.0 || >=23.4.0` Release workflow `.github/workflows/publish-npm.yml` first checks the release tag, SHA, and package version, then runs `pnpm verify` on Node 24. After that, it packs one exact artifact, smoke-tests that artifact on Node 22 and 24, verifies it again before provenance publishing, and backfills the release version. Run `pnpm run release:check` on Node 24 when the release environment is available; it adds the release-version assertion after the canonical gate. diff --git a/README.md b/README.md index 8f338561..a97bf0ac 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ npx @slkiser/opencode-quota init ``` > [!IMPORTANT] -> Requires OpenCode `2.0.16` or newer. Node.js `>= 22.13` is required for `npx @slkiser/opencode-quota ...`. +> Requires OpenCode `2.0.16` or newer. Node.js `22.13+` is required for `npx @slkiser/opencode-quota ...` (on Node 23, `23.4+`). Upgrading from v3? Read the [v4 migration guide](docs/readme/v4-migration.md). diff --git a/docs/readme/configuration.md b/docs/readme/configuration.md index 8df6d3c4..2dbccfbc 100644 --- a/docs/readme/configuration.md +++ b/docs/readme/configuration.md @@ -449,6 +449,8 @@ Existing `experimental.quotaToast` settings remain supported. | `cursorIncludedApiUsd` | unset | Override Cursor monthly included API budget in USD. | | `cursorBillingCycleStartDay` | unset | Local billing-cycle anchor day `1..28`; when unset, Cursor usage resets on the local calendar month. | +Every `provider..options.apiKey` below can also be written in OpenCode 2's native form, `providers..settings.apiKey`. Like OpenCode 2, a native `providers.` entry replaces the `provider.` entry with the same id. + Kimi has no `quota-toast.json` credential setting. Each regional plan resolves its own key: - **Kimi Code (Global):** `KIMI_GLOBAL_API_KEY` → trusted user/global `provider.kimi-code-plan-global.options.apiKey` → strict `kimi-code-plan-global` API-key entry in OpenCode `opencode.db`. diff --git a/docs/readme/updating.md b/docs/readme/updating.md index 9c1beadf..40b7e309 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -52,7 +52,7 @@ The `opencode.json` plugin entry loads both the server and the TUI; no `tui.json The preview can contain three sections: -- **Safe changes this command can make:** package-spec edits and recognized file-backed display-setting migration. +- **Safe changes this command can make:** package-spec edits (in `plugin` and OpenCode 2 `plugins` entries; plugin options are kept) and recognized file-backed display-setting migration. - **Manual actions — this command will not change these sources:** credential findings or config cases that require your review. - **Package-cache candidates:** directories considered for removal. A candidate is removed only after current config and the package manifest are verified. diff --git a/package.json b/package.json index 052a9072..5f0fff55 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,6 @@ "@opencode/plugin": "2.0.16" }, "engines": { - "node": ">=22.13.0" + "node": "^22.13.0 || >=23.4.0" } } diff --git a/scripts/smoke-packed-package.mjs b/scripts/smoke-packed-package.mjs index db81c92c..39b52884 100644 --- a/scripts/smoke-packed-package.mjs +++ b/scripts/smoke-packed-package.mjs @@ -80,7 +80,7 @@ try { const pkg = JSON.parse( await readFile("node_modules/@slkiser/opencode-quota/package.json", "utf8"), ); - assert.equal(pkg.engines?.node, ">=22.13.0"); + assert.equal(pkg.engines?.node, "^22.13.0 || >=23.4.0"); assert.equal(pkg.dependencies?.["@opentelemetry/api"], "^1.9.1"); for (const dependencyType of ["devDependencies", "optionalDependencies", "peerDependencies"]) { assert.equal(pkg[dependencyType]?.["@opentelemetry/api"], undefined); diff --git a/src/lib/api-key-resolver.ts b/src/lib/api-key-resolver.ts index ac1889a9..55b90538 100644 --- a/src/lib/api-key-resolver.ts +++ b/src/lib/api-key-resolver.ts @@ -106,6 +106,22 @@ export function getFirstAuthEntryRecord( return asRecord(getFirstAuthEntryValue(auth, authKeys)); } +/** + * Read one provider's settings the way OpenCode 2 normalizes its config. + * + * A native `providers.` entry replaces the legacy `provider.` entry, + * whose `options` OpenCode 2 migrates into `settings`. + */ +export function getProviderConfigSettings( + config: unknown, + providerKey: string, +): Record | null { + const root = asRecord(config); + const nativeProvider = asRecord(asRecord(root?.providers)?.[providerKey]); + if (nativeProvider) return asRecord(nativeProvider.settings); + return asRecord(asRecord(asRecord(root?.provider)?.[providerKey])?.options); +} + export function extractProviderOptionsApiKey( config: unknown, params: { @@ -113,12 +129,8 @@ export function extractProviderOptionsApiKey( allowedEnvVars?: readonly string[]; }, ): string | null { - const provider = asRecord(asRecord(config)?.provider); - if (!provider) return null; - for (const providerKey of params.providerKeys) { - const options = asRecord(asRecord(provider[providerKey])?.options); - const apiKey = options?.apiKey; + const apiKey = getProviderConfigSettings(config, providerKey)?.apiKey; if (typeof apiKey !== "string" || apiKey.trim().length === 0) continue; const trimmed = apiKey.trim(); diff --git a/src/lib/config-file-utils.ts b/src/lib/config-file-utils.ts index 44e645e6..009d2941 100644 --- a/src/lib/config-file-utils.ts +++ b/src/lib/config-file-utils.ts @@ -76,6 +76,17 @@ export function findGitWorktreeRoot(startDir: string): string | null { } } +/** + * Roots for an OpenCode location, shared by the server plugin and the TUI so both + * read the same project config: the enclosing Git worktree, else the location directory. + */ +export function resolveOpenCodeLocationRoots( + directory: string, +): RuntimeContextRoots & { fallbackDirectory: string } { + const workspaceRoot = findGitWorktreeRoot(directory) ?? directory; + return { workspaceRoot, configRoot: workspaceRoot, fallbackDirectory: directory }; +} + export function getConfigFileCandidatePaths(dir: string, kind: ConfigFileKind): string[] { return [join(dir, `${kind}.jsonc`), join(dir, `${kind}.json`)]; } @@ -130,13 +141,18 @@ export function resolveEditableConfigPath(params: { }; } +/** Reads the package spec from a legacy `plugin` entry or an OpenCode 2 native `plugins` entry. */ export function getPluginSpecFromEntry(entry: unknown): string | null { const spec = typeof entry === "string" ? entry : Array.isArray(entry) && typeof entry[0] === "string" ? entry[0] - : null; + : entry && + typeof entry === "object" && + typeof (entry as { package?: unknown }).package === "string" + ? (entry as { package: string }).package + : null; if (typeof spec !== "string") { return null; @@ -158,6 +174,10 @@ export function extractPluginSpecsFromParsedConfig(parsed: unknown): string[] { pluginEntries.push(...root.plugin); } + if (Array.isArray(root.plugins)) { + pluginEntries.push(...root.plugins); + } + if (root.tui && typeof root.tui === "object" && !Array.isArray(root.tui)) { const tuiRoot = root.tui as Record; if (Array.isArray(tuiRoot.plugin)) { @@ -177,12 +197,16 @@ export function extractProviderIdsFromParsedConfig(parsed: unknown): string[] { return []; } + // OpenCode 2 reads both the legacy `provider` map and the native `providers` map. const root = parsed as Record; - if (!root.provider || typeof root.provider !== "object" || Array.isArray(root.provider)) { - return []; + const providerIds: string[] = []; + for (const providerMap of [root.provider, root.providers]) { + if (providerMap && typeof providerMap === "object" && !Array.isArray(providerMap)) { + providerIds.push(...Object.keys(providerMap)); + } } - return dedupeNonEmptyStrings(Object.keys(root.provider)); + return dedupeNonEmptyStrings(providerIds); } export function isQuotaPluginSpec(spec: string): boolean { diff --git a/src/lib/cursor-detection.ts b/src/lib/cursor-detection.ts index 677c29f5..6083e4c6 100644 --- a/src/lib/cursor-detection.ts +++ b/src/lib/cursor-detection.ts @@ -2,6 +2,7 @@ import { existsSync } from "fs"; import { readFile } from "fs/promises"; import { homedir, platform } from "os"; import { join } from "path"; +import { getPluginSpecFromEntry } from "./config-file-utils.js"; import { CURSOR_LEGACY_PROVIDER_ID } from "./cursor-pricing.js"; import { parseJsonOrJsonc } from "./jsonc.js"; import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; @@ -162,6 +163,7 @@ function providerConfigIncludesCursor(value: unknown): boolean { return getQuotaProviderRuntimeIds("cursor").some((id) => Object.hasOwn(providerConfig, id)); } +/** OpenCode 2 reads both the legacy `plugin`/`provider` keys and the native `plugins`/`providers` keys. */ function parseOpenCodeConfig( raw: string, isJsonc: boolean, @@ -170,9 +172,14 @@ function parseOpenCodeConfig( provider: Record | null; } { const parsed = asRecord(parseJsonOrJsonc(raw, isJsonc)); + const legacyProvider = asRecord(parsed?.provider); + const nativeProviders = asRecord(parsed?.providers); return { - plugin: Array.isArray(parsed?.plugin) ? parsed.plugin : [], - provider: asRecord(parsed?.provider), + plugin: [ + ...(Array.isArray(parsed?.plugin) ? parsed.plugin : []), + ...(Array.isArray(parsed?.plugins) ? parsed.plugins.map(getPluginSpecFromEntry) : []), + ], + provider: legacyProvider || nativeProviders ? { ...legacyProvider, ...nativeProviders } : null, }; } diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index 957dbb25..760477f0 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -279,15 +279,17 @@ function ensureSchema(root: JsonObject, schemaUrl: string, edit: PlannedConfigEd pushSkippedIfChanged(edit, "$schema", root.$schema, schemaUrl); } +function isQuotaPluginEntry(entry: unknown): boolean { + const spec = getPluginSpecFromEntry(entry); + return typeof spec === "string" && isQuotaPluginSpec(spec); +} + function appendQuotaPluginIfMissing(params: { container: unknown[]; pathLabel: string; edit: PlannedConfigEdit; }): void { - const alreadyConfigured = params.container.some((entry) => { - const spec = getPluginSpecFromEntry(entry); - return typeof spec === "string" && isQuotaPluginSpec(spec); - }); + const alreadyConfigured = params.container.some(isQuotaPluginEntry); if (alreadyConfigured) { params.edit.skippedValues.push(`${params.pathLabel} already includes ${QUOTA_PLUGIN_SPEC}`); @@ -635,12 +637,17 @@ async function planOpencodeEdit(params: { ensureSchema(root, OPENCODE_SCHEMA_URL, edit); } - const plugin = ensureTopLevelPluginArray(root, edit); - appendQuotaPluginIfMissing({ - container: plugin, - pathLabel: "plugin", - edit, - }); + // OpenCode 2 also loads plugins from the native `plugins` array; never add a second entry. + if (Array.isArray(root.plugins) && root.plugins.some(isQuotaPluginEntry)) { + edit.skippedValues.push(`plugins already includes ${QUOTA_PLUGIN_SPEC}`); + } else { + const plugin = ensureTopLevelPluginArray(root, edit); + appendQuotaPluginIfMissing({ + container: plugin, + pathLabel: "plugin", + edit, + }); + } if (params.legacyQuotaToastToSync) { syncLegacyQuotaToast({ diff --git a/src/lib/minimax-auth.ts b/src/lib/minimax-auth.ts index 44418ebc..42ef92a1 100644 --- a/src/lib/minimax-auth.ts +++ b/src/lib/minimax-auth.ts @@ -10,6 +10,7 @@ import { getApiKeyCheckedPaths, getFirstAuthEntryValue, getGlobalOpencodeConfigCandidatePaths, + getProviderConfigSettings, readOpencodeConfig, } from "./api-key-resolver.js"; import { sanitizeDisplayText } from "./display-sanitize.js"; @@ -113,10 +114,6 @@ function sanitizeMiniMaxAuthValue(value: string): string { return (sanitized || "unknown").slice(0, 120); } -function asRecord(value: unknown): Record | null { - return value !== null && typeof value === "object" ? (value as Record) : null; -} - function getConfigOptionString(options: Record, key: string): string | null { const value = options[key]; return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; @@ -126,11 +123,8 @@ function extractMiniMaxConfigAuth( config: unknown, spec: MiniMaxAuthSpec, ): { state: "configured"; apiKey: string; endpoint: MiniMaxQuotaEndpointId } | null { - const provider = asRecord(asRecord(config)?.provider); - if (!provider) return null; - for (const providerKey of spec.providerKeys) { - const options = asRecord(asRecord(provider[providerKey])?.options); + const options = getProviderConfigSettings(config, providerKey); if (!options) continue; const apiKey = getConfigOptionString(options, "apiKey"); diff --git a/src/lib/opencode-config-providers.ts b/src/lib/opencode-config-providers.ts index a0c391a5..092cf878 100644 --- a/src/lib/opencode-config-providers.ts +++ b/src/lib/opencode-config-providers.ts @@ -100,6 +100,13 @@ function mergeOpenCodeConfig( }; } + if (isRecord(base.providers) || isRecord(next.providers)) { + merged.providers = { + ...(isRecord(base.providers) ? base.providers : {}), + ...(isRecord(next.providers) ? next.providers : {}), + }; + } + if (Array.isArray(base.plugin) || Array.isArray(next.plugin)) { merged.plugin = [ ...(Array.isArray(base.plugin) ? base.plugin : []), @@ -107,6 +114,13 @@ function mergeOpenCodeConfig( ]; } + if (Array.isArray(base.plugins) || Array.isArray(next.plugins)) { + merged.plugins = [ + ...(Array.isArray(base.plugins) ? base.plugins : []), + ...(Array.isArray(next.plugins) ? next.plugins : []), + ]; + } + return merged; } diff --git a/src/lib/scoped-update.ts b/src/lib/scoped-update.ts index 0df57731..5337fd38 100644 --- a/src/lib/scoped-update.ts +++ b/src/lib/scoped-update.ts @@ -117,11 +117,23 @@ async function dedupeByRealPath(paths: string[]): Promise { return output; } +/** OpenCode 2 native `plugins` entry: `{ "package": "...", "options": { ... } }`. */ +function isPackageEntry(entry: unknown): entry is { package: string } { + return ( + !!entry && + typeof entry === "object" && + !Array.isArray(entry) && + typeof (entry as { package?: unknown }).package === "string" + ); +} + +/** OpenCode 2 reads both the legacy `plugin` array and the native `plugins` array. */ function pluginArrays(config: unknown): Array<{ path: (string | number)[]; entries: unknown[] }> { if (!config || typeof config !== "object" || Array.isArray(config)) return []; const root = config as Record; const arrays: Array<{ path: (string | number)[]; entries: unknown[] }> = []; if (Array.isArray(root.plugin)) arrays.push({ path: ["plugin"], entries: root.plugin }); + if (Array.isArray(root.plugins)) arrays.push({ path: ["plugins"], entries: root.plugins }); return arrays; } @@ -152,12 +164,19 @@ function updateConfig( ? entry : Array.isArray(entry) && typeof entry[0] === "string" ? entry[0] - : null; + : isPackageEntry(entry) + ? entry.package + : null; if (spec === null || !isCanonicalQuotaUpdateSpec(spec)) continue; specs.push(spec); if (spec === QUOTA_LATEST_SPEC) continue; + // Replace only the package spec; keep the entry's plugin options. const targetPath = - typeof entry === "string" ? [...array.path, index] : [...array.path, index, 0]; + typeof entry === "string" + ? [...array.path, index] + : Array.isArray(entry) + ? [...array.path, index, 0] + : [...array.path, index, "package"]; edits.push({ path: targetPath, value: QUOTA_LATEST_SPEC }); replacements++; } diff --git a/src/plugin.ts b/src/plugin.ts index 87ab0d5e..132fe540 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,6 +1,6 @@ /** OpenCode V2 server plugin: deterministic quota diagnostics tool. */ import { Plugin } from "@opencode/plugin"; -import { findGitWorktreeRoot } from "./lib/config-file-utils.js"; +import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { reconcileDetectedProvidersInGlobalConfig } from "./lib/opencode-config-providers.js"; import { buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js"; @@ -8,13 +8,7 @@ import { buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js"; export const QuotaToastPlugin = Plugin.define({ id: "@slkiser/opencode-quota.server", async setup(ctx) { - const directory = ctx.location.directory; - const workspaceRoot = findGitWorktreeRoot(directory) ?? directory; - const roots = { - workspaceRoot, - configRoot: workspaceRoot, - fallbackDirectory: directory, - }; + const roots = resolveOpenCodeLocationRoots(ctx.location.directory); // The quota collector accepts a small V1-shaped configuration client. V2 // does not expose a mutable global config to plugins; quota settings are // read from the plugin's own config file by the collector. diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 0e682fbe..041c210c 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -5,6 +5,7 @@ import { RGBA } from "@opentui/core"; import type { JSX } from "@opentui/solid"; import { createSignal, onCleanup, Show } from "solid-js"; +import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { formatQuotaRows } from "./lib/format.js"; import { @@ -125,6 +126,11 @@ function quotaClient(context: TuiContext) { }; } +/** Same project roots as the server plugin: the location's Git worktree, else the location directory. */ +function quotaRoots(context: TuiContext) { + return resolveOpenCodeLocationRoots(context.location?.directory ?? process.cwd()); +} + async function getSessionModelMeta( context: TuiContext, sessionID: string, @@ -143,7 +149,7 @@ async function getQuotaMessage( > { const runtime = await resolveQuotaRuntimeContext({ client: quotaClient(context), - roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, + roots: quotaRoots(context), sessionID, resolveSessionMeta: (id) => getSessionModelMeta(context, id), includeSessionMeta: (config) => config.onlyCurrentModel, @@ -341,13 +347,23 @@ async function getQuotaFooter( sessionID: string | undefined, surface: "prompt" | "home", ): Promise { - const runtime = await resolveQuotaRuntimeContext({ + const resolvedRuntime = await resolveQuotaRuntimeContext({ client: quotaClient(context), - roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, + roots: quotaRoots(context), sessionID, resolveSessionMeta: (id) => getSessionModelMeta(context, id), includeSessionMeta: (config) => config.onlyCurrentModel && surface === "prompt", }); + // Home has no session: as in v4, it shows every enabled provider and no session + // tokens, and shares its cache keys with the export (createExportProviderContext). + const runtime: QuotaRuntimeContext = + surface === "home" + ? { + ...resolvedRuntime, + config: { ...resolvedRuntime.config, onlyCurrentModel: false, showSessionTokens: false }, + session: {}, + } + : resolvedRuntime; const config = runtime.config; if (!config.enabled) return []; if ( @@ -421,30 +437,75 @@ async function getQuotaFooter( return [announcement, compact].filter(Boolean); } +/** + * One view's refresh loop, as in v4 (tui-refresh-lifecycle.ts): a refresh that + * arrives while a load runs is coalesced into one follow-up load, and a result is + * dropped when the view was disposed or a newer load started. + */ +function createViewRefresh( + load: () => Promise, + apply: (value: T) => void, +): { refresh: () => void; dispose: () => void } { + let disposed = false; + let loadVersion = 0; + let inFlight = false; + let queued = false; + const refresh = () => { + if (disposed) return; + if (inFlight) { + queued = true; + return; + } + inFlight = true; + const currentVersion = ++loadVersion; + void load() + .then((value) => { + if (disposed || currentVersion !== loadVersion) return; + apply(value); + }) + .catch(reportFailure) + .finally(() => { + if (disposed) return; + inFlight = false; + if (queued) { + queued = false; + refresh(); + } + }); + }; + return { + refresh, + dispose: () => { + disposed = true; + }, + }; +} + function QuotaFooter(props: { context: TuiContext; sessionID?: string; surface: "prompt" | "home"; }): JSX.Element { const [lines, setLines] = createSignal([]); - const refresh = () => - void getQuotaFooter(props.context, props.sessionID, props.surface) - .then((next) => { - setLines(next); - if (props.surface !== "home") return; - // Fire-and-forget: write the export file if enabled. A failed write - // must never affect rendering, so log a warning and continue. - void writeQuotaExportIfEnabled(props.context).catch((error) => { - console.warn(`[opencode-quota] quota export write failed: ${String(error)}`); - }); - }) - .catch(reportFailure); - refresh(); - const interval = setInterval(refresh, REFRESH_INTERVAL_MS); + const view = createViewRefresh( + () => getQuotaFooter(props.context, props.sessionID, props.surface), + (next) => { + setLines(next); + if (props.surface !== "home") return; + // Fire-and-forget: write the export file if enabled. A failed write + // must never affect rendering, so log a warning and continue. + void writeQuotaExportIfEnabled(props.context).catch((error) => { + console.warn(`[opencode-quota] quota export write failed: ${String(error)}`); + }); + }, + ); + view.refresh(); + const interval = setInterval(view.refresh, REFRESH_INTERVAL_MS); const stop = props.context.data.on("session.step.ended", (event) => { - if (props.surface === "home" || getSessionID(event) === props.sessionID) refresh(); + if (props.surface === "home" || getSessionID(event) === props.sessionID) view.refresh(); }); onCleanup(() => { + view.dispose(); clearInterval(interval); stop(); }); @@ -463,7 +524,7 @@ function QuotaFooter(props: { async function writeQuotaExportIfEnabled(context: TuiContext): Promise { const runtime = await resolveQuotaRuntimeContext({ client: quotaClient(context), - roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, + roots: quotaRoots(context), }); if (!runtime.config.enabled || !runtime.config.export.enabled) return; @@ -489,10 +550,12 @@ async function runQuotaCommand( ): Promise { const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.id === command)!; let argumentsText = input?.trim() || undefined; - if (spec.acceptsArguments && argumentsText === undefined) { + // Only /tokens_between needs arguments. /quota_announcements and /pricing_refresh + // accept typed arguments only to reject them, so they run without a prompt. + if (command === "tokens_between" && argumentsText === undefined) { const value = await context.ui.dialog.prompt({ title: spec.title, - placeholder: command === "tokens_between" ? "YYYY-MM-DD YYYY-MM-DD" : "Optional arguments", + placeholder: "YYYY-MM-DD YYYY-MM-DD", }); if (value === undefined) return; argumentsText = value.trim() || undefined; @@ -503,7 +566,7 @@ async function runQuotaCommand( command, arguments: argumentsText, client: quotaClient(context), - roots: { fallbackDirectory: context.location?.directory ?? process.cwd() }, + roots: quotaRoots(context), sessionID, resolveSessionMeta: (id) => getSessionModelMeta(context, id), }); @@ -548,17 +611,17 @@ function SidebarQuotaView(props: { >(undefined); const lines = () => quota()?.message.split("\n") ?? []; const expandable = () => lines().length > 2; - const refresh = () => { - void getQuotaMessage(props.context, props.sessionID, "sidebar") - .then((result) => setQuota(result)) - .catch(reportFailure); - }; - refresh(); - const interval = setInterval(refresh, REFRESH_INTERVAL_MS); + const view = createViewRefresh( + () => getQuotaMessage(props.context, props.sessionID, "sidebar"), + setQuota, + ); + view.refresh(); + const interval = setInterval(view.refresh, REFRESH_INTERVAL_MS); const unsubscribe = props.context.data.on("session.step.ended", (event) => { - if (getSessionID(event) === props.sessionID) refresh(); + if (getSessionID(event) === props.sessionID) view.refresh(); }); onCleanup(() => { + view.dispose(); clearInterval(interval); unsubscribe(); }); diff --git a/tests/lib.api-key-resolver.test.ts b/tests/lib.api-key-resolver.test.ts index 2b5b6a7f..4a86c802 100644 --- a/tests/lib.api-key-resolver.test.ts +++ b/tests/lib.api-key-resolver.test.ts @@ -22,6 +22,7 @@ vi.mock("fs/promises", () => ({ import { createProviderApiKeyResolver, extractAuthApiKeyEntry, + extractProviderOptionsApiKey, getApiKeyCheckedPaths, resolveApiKey, resolveApiKeyFromEnvAndConfig, @@ -193,6 +194,55 @@ describe("api-key-resolver", () => { expect(invalidReadAuth).toHaveBeenCalledWith(0); }); + it("reads OpenCode 2 native provider settings before the legacy provider options", () => { + const params = { providerKeys: ["deepseek"], allowedEnvVars: ["DEEPSEEK_API_KEY"] }; + + expect( + extractProviderOptionsApiKey( + { providers: { deepseek: { settings: { apiKey: " native-key " } } } }, + params, + ), + ).toBe("native-key"); + expect( + extractProviderOptionsApiKey( + { + provider: { deepseek: { options: { apiKey: "legacy-key" } } }, + providers: { deepseek: { settings: { apiKey: "native-key" } } }, + }, + params, + ), + ).toBe("native-key"); + expect( + extractProviderOptionsApiKey( + { + provider: { deepseek: { options: { apiKey: "legacy-key" } } }, + providers: { deepseek: { name: "DeepSeek" } }, + }, + params, + ), + ).toBeNull(); + expect( + extractProviderOptionsApiKey( + { provider: { deepseek: { options: { apiKey: "legacy-key" } } } }, + params, + ), + ).toBe("legacy-key"); + + process.env.TEST_PROVIDER_KEY = "templated-key"; + expect( + extractProviderOptionsApiKey( + { providers: { deepseek: { settings: { apiKey: "{env:TEST_PROVIDER_KEY}" } } } }, + params, + ), + ).toBeNull(); + expect( + extractProviderOptionsApiKey( + { providers: { deepseek: { settings: { apiKey: "{env:TEST_PROVIDER_KEY}" } } } }, + { providerKeys: ["deepseek"], allowedEnvVars: ["TEST_PROVIDER_KEY"] }, + ), + ).toBe("templated-key"); + }); + it("extracts only strict api key auth entries", () => { expect( extractAuthApiKeyEntry( diff --git a/tests/lib.cursor-detection.test.ts b/tests/lib.cursor-detection.test.ts index 40600fa9..c63cc0ac 100644 --- a/tests/lib.cursor-detection.test.ts +++ b/tests/lib.cursor-detection.test.ts @@ -135,6 +135,23 @@ describe("cursor detection", () => { expect(result.matchedPaths).toEqual([testPaths.opencodeConfig]); }); + it("detects the Cursor companion and provider in OpenCode 2 native plugins and providers", async () => { + mockFiles.set( + testPaths.opencodeConfig, + JSON.stringify({ + plugins: [{ package: "@playwo/opencode-cursor-oauth", options: {} }], + providers: { cursor: { name: "Cursor" } }, + }), + ); + + const { inspectCursorOpenCodeIntegration } = await import("../src/lib/cursor-detection.js"); + const result = await inspectCursorOpenCodeIntegration(); + + expect(result.pluginEnabled).toBe(true); + expect(result.providerConfigured).toBe(true); + expect(result.matchedPaths).toEqual([testPaths.opencodeConfig]); + }); + it("keeps legacy Cursor plugin names as compatibility aliases", async () => { const aliases = [ "opencode-cursor-oauth", diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index ee96d7b8..713a68bd 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -362,6 +362,35 @@ describe("init installer planning and merge behavior", () => { ]); }); + it("does not add a legacy plugin entry when OpenCode 2 native plugins already load quota", async () => { + const projectDir = join(tempDir, "project"); + mkdirSync(projectDir, { recursive: true }); + + writeFileSync( + join(projectDir, "opencode.json"), + JSON.stringify({ + plugins: [{ package: "@slkiser/opencode-quota@latest", options: { keep: true } }], + }), + "utf8", + ); + + const plan = await planSelections({}, projectDir); + + const opencodeEdit = plan.edits.find((edit) => edit.kind === "opencode"); + expect(opencodeEdit?.addedPlugins).toEqual([]); + expect(opencodeEdit?.skippedValues).toContain( + "plugins already includes @slkiser/opencode-quota@latest", + ); + + await applyInitInstallerPlan(plan); + + const opencode = readJson(join(projectDir, "opencode.jsonc")); + expect(opencode.plugin).toBeUndefined(); + expect(opencode.plugins).toEqual([ + { package: "@slkiser/opencode-quota@latest", options: { keep: true } }, + ]); + }); + it("leaves an existing tui.json untouched and writes sidebar disabled when sidebar is not selected", async () => { const projectDir = join(tempDir, "project"); mkdirSync(projectDir, { recursive: true }); diff --git a/tests/lib.minimax-auth.test.ts b/tests/lib.minimax-auth.test.ts index a510b80f..56820e1a 100644 --- a/tests/lib.minimax-auth.test.ts +++ b/tests/lib.minimax-auth.test.ts @@ -192,6 +192,23 @@ describe("minimax auth resolution", () => { expect(mocks.readAuthFileCached).not.toHaveBeenCalled(); }); + it("reads OpenCode 2 native provider settings before legacy provider options", async () => { + mockTrustedConfigFile( + fsConfigMocks, + trustedPaths.json, + JSON.stringify({ + provider: { "minimax-coding-plan": { options: { apiKey: "legacy-key" } } }, + providers: { "minimax-coding-plan": { settings: { apiKey: "native-key" } } }, + }), + ); + + await expect(resolveMiniMaxAuthCached()).resolves.toEqual({ + state: "configured", + apiKey: "native-key", + endpoint: "international", + }); + }); + it("resolves China from trusted config China aliases", async () => { mockTrustedConfigFile( fsConfigMocks, diff --git a/tests/lib.opencode-config-providers.test.ts b/tests/lib.opencode-config-providers.test.ts index 3bc13b72..e0f2f2e3 100644 --- a/tests/lib.opencode-config-providers.test.ts +++ b/tests/lib.opencode-config-providers.test.ts @@ -120,6 +120,29 @@ describe("opencode config provider discovery", () => { ]); }); + it("reads OpenCode 2 native providers and plugins alongside the legacy keys", async () => { + writeFileSync( + join(globalConfigDir, "opencode.json"), + JSON.stringify({ providers: { deepseek: {} }, plugins: ["opencode-gemini-auth"] }), + "utf8", + ); + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ + provider: { openai: {} }, + plugins: [{ package: "@playwo/opencode-cursor-oauth", options: { enabled: true } }], + }), + "utf8", + ); + + await expect(loadConfiguredProviderIds({ configRootDir: workspaceDir })).resolves.toEqual([ + "openai", + "deepseek", + "google-gemini-cli", + "cursor", + ]); + }); + it("loads a merged OpenCode config view for standalone clients", async () => { writeFileSync( join(globalConfigDir, "opencode.json"), diff --git a/tests/lib.scoped-update.test.ts b/tests/lib.scoped-update.test.ts index 4c1423c4..fc15c978 100644 --- a/tests/lib.scoped-update.test.ts +++ b/tests/lib.scoped-update.test.ts @@ -135,6 +135,27 @@ describe("scoped update config planning", () => { expect(readFileSync(ignoredJson, "utf8")).toContain("@1.0.0"); }); + it("updates OpenCode 2 native plugins entries and keeps their options", async () => { + const f = fixture(); + const config = join(f.project, "opencode.json"); + write( + config, + `{"plugins":["@slkiser/opencode-quota@3.11.1",{"package":"@slkiser/opencode-quota@3.11.1","options":{"setting":true}},{"package":"@slkiser/opencode-quota@next"},"other"]}`, + ); + const params = { cwd: f.project, env: f.env, homeDir: join(f.root, "home") }; + const plan = await planScopedUpdate(params); + expect(plan.foundSpecs).toEqual(["@slkiser/opencode-quota@3.11.1"]); + await applyScopedUpdatePlan(plan); + expect(JSON.parse(readFileSync(config, "utf8"))).toEqual({ + plugins: [ + QUOTA_LATEST_SPEC, + { package: QUOTA_LATEST_SPEC, options: { setting: true } }, + { package: "@slkiser/opencode-quota@next" }, + "other", + ], + }); + }); + it("leaves tui.json files untouched", async () => { const f = fixture(); const projectTui = join(f.project, "tui.json"); diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index 0cef463d..621837e4 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -147,7 +147,7 @@ describe("package manifest compatibility", () => { expect(packageManagerMatch).not.toBeNull(); expect(Number(packageManagerMatch?.[1])).toBeGreaterThanOrEqual(11); - expect(pkg.engines?.node).toBe(">=22.13.0"); + expect(pkg.engines?.node).toBe("^22.13.0 || >=23.4.0"); expect(pkg.devDependencies?.typescript).toBe("7.0.2"); expect(pkg.devDependencies?.yaml).toBe("^2.8.3"); }); @@ -157,7 +157,9 @@ describe("package manifest compatibility", () => { expect(pkg.devDependencies?.["@opencode/plugin"]).toBe("2.0.16"); expect(pkg.dependencies?.["@opentui/core"]).toBe("^0.5.10"); expect(pkg.dependencies?.["@opentui/solid"]).toBe("^0.5.10"); - expect(readme).toContain("Node.js `>= 22.13` is required"); + expect(readme).toContain( + "Node.js `22.13+` is required for `npx @slkiser/opencode-quota ...` (on Node 23, `23.4+`)", + ); expect(readme).not.toContain("OpenCode `>= 1.4.3`"); expect(pkg.engines).not.toHaveProperty("opencode"); }); diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 880aba9b..7f06d7a2 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { resolveOpenCodeLocationRoots } from "../src/lib/config-file-utils.js"; import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-commands.js"; import tuiPlugin from "../src/tui-v2.js"; @@ -87,7 +88,7 @@ describe("V2 CLI command boundary", () => { expect.objectContaining({ command: "quota", sessionID: undefined, - roots: { fallbackDirectory: process.cwd() }, + roots: resolveOpenCodeLocationRoots(process.cwd()), }), ); expect(tui.alert).toHaveBeenCalledWith({ title: "Quota", message: "Quota ready" }); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index f254c8e9..b68436b5 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -1,3 +1,6 @@ +import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; const build = vi.hoisted(() => vi.fn()); @@ -15,9 +18,10 @@ type RegisteredCommand = { run: (input?: string) => Promise; }; -function startTui() { +function startTui(location?: { directory: string }) { let layer: { commands: RegisteredCommand[] } | undefined; const context = { + location, data: { on: vi.fn(() => vi.fn()), session: { @@ -128,6 +132,45 @@ describe("V2 quota TUI commands", () => { expect(build).not.toHaveBeenCalled(); }); + it("runs /quota_announcements and /pricing_refresh without a prompt and forwards typed arguments", async () => { + const { context, command } = startTui(); + + for (const id of ["quota_announcements", "pricing_refresh"]) { + await command(id).run(""); + await command(id).run(); + await command(id).run(" extra "); + } + + expect(context.ui.dialog.prompt).not.toHaveBeenCalled(); + expect(build.mock.calls.map(([params]) => [params.command, params.arguments])).toEqual([ + ["quota_announcements", undefined], + ["quota_announcements", undefined], + ["quota_announcements", "extra"], + ["pricing_refresh", undefined], + ["pricing_refresh", undefined], + ["pricing_refresh", "extra"], + ]); + }); + + it("reads project config from the location's Git worktree root, like the server plugin", async () => { + const repo = realpathSync(mkdtempSync(join(tmpdir(), "opencode-quota-tui-roots-"))); + try { + mkdirSync(join(repo, ".git")); + mkdirSync(join(repo, "packages", "app"), { recursive: true }); + const { command } = startTui({ directory: join(repo, "packages", "app") }); + + await command("quota").run(); + + expect(build.mock.calls[0][0].roots).toEqual({ + workspaceRoot: repo, + configRoot: repo, + fallbackDirectory: join(repo, "packages", "app"), + }); + } finally { + rmSync(repo, { recursive: true, force: true }); + } + }); + it("reads the session model from the OpenCode 2 session store", async () => { const { context, command } = startTui(); diff --git a/tests/tui-v2-refresh.test.ts b/tests/tui-v2-refresh.test.ts index 05d765ee..90be5c19 100644 --- a/tests/tui-v2-refresh.test.ts +++ b/tests/tui-v2-refresh.test.ts @@ -38,10 +38,17 @@ async function flushPromises(): Promise { for (let i = 0; i < 20; i += 1) await Promise.resolve(); } -function setupFooterSlots(): Map unknown> { - const renderers = new Map unknown>(); +function setupFooterSlots( + handlers = new Map void>(), +): Map unknown> { + const renderers = new Map unknown>(); plugin.setup({ - data: { on: vi.fn(() => vi.fn()) }, + data: { + on: vi.fn((event: string, handler: (event: unknown) => void) => { + handlers.set(event, handler); + return vi.fn(); + }), + }, keymap: { layer: vi.fn() }, ui: { slot: vi.fn((claim) => { @@ -55,13 +62,21 @@ function setupFooterSlots(): Map unknown> { return renderers; } -function mountSlot(render: (() => unknown) | undefined): () => void { +function mountSlot(render: ((props?: any) => unknown) | undefined, props?: unknown): () => void { return createRoot((dispose) => { - render?.(); + render?.(props); return dispose; }); } +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((next) => { + resolve = next; + }); + return { promise, resolve }; +} + describe("V2 footer refresh timer", () => { beforeEach(() => { vi.useFakeTimers(); @@ -141,4 +156,77 @@ describe("V2 footer refresh timer", () => { expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); expect(writeQuotaExport).not.toHaveBeenCalled(); }); + + it("loads Home for every enabled provider without a session, even with onlyCurrentModel", async () => { + const runtime = await resolveQuotaRuntimeContext(); + resolveQuotaRuntimeContext.mockResolvedValue({ + ...runtime, + config: { ...runtime.config, onlyCurrentModel: true, showSessionTokens: true }, + session: { sessionID: "ses_1", sessionMeta: { modelID: "m", providerID: "p" } }, + }); + const renderers = setupFooterSlots(); + const dispose = mountSlot(renderers.get("home.footer.status")); + await flushPromises(); + + const params = collectQuotaRenderData.mock.calls[0][0]; + expect(params.config).toMatchObject({ onlyCurrentModel: false, showSessionTokens: false }); + expect(params.request).toEqual({ sessionID: undefined, sessionMeta: undefined }); + dispose(); + }); + + it("drops a Home result and skips the export when the view unmounts mid-load", async () => { + const load = deferred(); + collectQuotaRenderData.mockReturnValueOnce(load.promise); + const renderers = setupFooterSlots(); + const dispose = mountSlot(renderers.get("home.footer.status")); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + + dispose(); + load.resolve({ active: [], data: { entries: [], errors: [] } }); + await flushPromises(); + expect(writeQuotaExport).not.toHaveBeenCalled(); + }); + + it("coalesces refreshes that arrive while a footer load is running into one follow-up load", async () => { + const load = deferred(); + collectQuotaRenderData.mockReturnValueOnce(load.promise); + const handlers = new Map void>(); + const renderers = setupFooterSlots(handlers); + const dispose = mountSlot(renderers.get("home.footer.status")); + await flushPromises(); + + handlers.get("session.step.ended")?.({ data: { sessionID: "ses_1" } }); + handlers.get("session.step.ended")?.({ data: { sessionID: "ses_1" } }); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + + load.resolve({ active: [], data: { entries: [], errors: [] } }); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); + expect(writeQuotaExport).toHaveBeenCalledTimes(2); + dispose(); + }); + + it("stops sidebar refreshes and drops their results after unmount", async () => { + const runtime = await resolveQuotaRuntimeContext(); + resolveQuotaRuntimeContext.mockResolvedValue({ + ...runtime, + config: { ...runtime.config, tuiSidebarPanel: { enabled: true } }, + }); + const load = deferred(); + collectQuotaRenderData.mockReturnValueOnce(load.promise); + const handlers = new Map void>(); + const renderers = setupFooterSlots(handlers); + const dispose = mountSlot(renderers.get("sidebar.content"), { sessionID: "ses_1" }); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + + handlers.get("session.step.ended")?.({ data: { sessionID: "ses_1" } }); + dispose(); + load.resolve({ active: [], data: undefined }); + await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS * 2); + await flushPromises(); + expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + }); }); diff --git a/tests/v4-migration-docs.test.ts b/tests/v4-migration-docs.test.ts index d18f2243..6ba42ed1 100644 --- a/tests/v4-migration-docs.test.ts +++ b/tests/v4-migration-docs.test.ts @@ -23,7 +23,7 @@ const configuration = await readFile( describe("v4 migration documentation contract", () => { it("keeps requirements and navigation aligned", () => { - expect(packageJson.engines?.node).toBe(">=22.13.0"); + expect(packageJson.engines?.node).toBe("^22.13.0 || >=23.4.0"); expect(packageJson.peerDependencies?.["@opencode/plugin"]).toBe("2.0.16"); expect(packageJson.engines).not.toHaveProperty("opencode"); From f16ea2f6776c45fc02e085b2e57e36d4d79e3a1c Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:33:58 +0200 Subject: [PATCH 45/97] fix: address second v5.0.0 review - P1 subagent toasts: fixed. Idle, compaction, and question triggers skip sessions whose OpenCode 2 session has a parentID, as v4 did for all three triggers. - P1 prompt footer session: fixed. prompt.footer renders with the slot's own sessionID; slash commands use the session route from ui.router.current(); activeSessionID is removed. - P1 compact line twice on Home: fixed. The Home prompt passes no sessionID, so the prompt footer (compact line and prompt bar) now renders nothing there, matching v4's session-prompt-only slot. - P2 OpenAI and the opencode Console login: fixed. `opencode` is no longer an OpenAI auth key, so no opencode OAuth entry (including imported methodID "device" logins without an orgID) reaches chatgpt.com. - P2 channel database name: docs only. troubleshooting.md tells custom/source OpenCode builds to set OPENCODE_DB to their opencode-.db path. - Bun sqlite test: not changed (vitest runs on Node). - Node ExperimentalWarning: not changed (cosmetic). --- docs/readme/troubleshooting.md | 2 +- src/lib/openai.ts | 16 +---- src/providers/openai.ts | 9 +-- src/tui-v2.tsx | 47 ++++++++------- tests/lib.openai.test.ts | 40 ++++--------- tests/plugin.command-handled-boundary.test.ts | 1 + tests/plugin.question-hook.test.ts | 3 +- tests/providers.openai.test.ts | 1 + tests/tui-v2-commands.test.ts | 15 ++++- tests/tui-v2-refresh.test.ts | 60 ++++++++++++++++++- tests/v4-phase5-cross-surface.test.ts | 22 ++++--- 11 files changed, 136 insertions(+), 80 deletions(-) diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 0c67c564..e3400168 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -13,7 +13,7 @@ Start with `/quota_status` in the OpenCode TUI, or `opencode-quota status` from If every provider is missing, confirm OpenCode Quota is listed in `opencode.jsonc` or `.json`. That one entry also loads the TUI; no `tui.json` entry is needed. -OpenCode Quota reads logins only from OpenCode 2's `opencode.db`, never `auth.json`. OpenCode 2 copies `auth.json` once, the first time it starts. If a provider is missing, log in to it again in OpenCode 2. `/quota_status` shows the `opencode.db` path in use; `OPENCODE_DB` and `XDG_DATA_HOME` change it. +OpenCode Quota reads logins only from OpenCode 2's `opencode.db`, never `auth.json`. OpenCode 2 copies `auth.json` once, the first time it starts. If a provider is missing, log in to it again in OpenCode 2. `/quota_status` shows the `opencode.db` path in use; `OPENCODE_DB` and `XDG_DATA_HOME` change it. Custom or source builds of OpenCode may use `opencode-.db` instead; set `OPENCODE_DB` to that file's path. ## Common problems diff --git a/src/lib/openai.ts b/src/lib/openai.ts index 3e1ed0bb..0d9dacfa 100644 --- a/src/lib/openai.ts +++ b/src/lib/openai.ts @@ -179,7 +179,9 @@ function derivePlanLabel(planType: string | undefined): string { const OPENAI_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"; export const DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS = 5_000; -export const OPENAI_AUTH_SOURCE_KEYS = ["openai", "codex", "chatgpt", "opencode"] as const; +// Not `opencode`: in OpenCode 2 its OAuth entry is the Console login, which +// authorizes Console APIs, not OpenAI. +export const OPENAI_AUTH_SOURCE_KEYS = ["openai", "codex", "chatgpt"] as const; export type OpenAIAuthSourceKey = (typeof OPENAI_AUTH_SOURCE_KEYS)[number]; @@ -224,18 +226,6 @@ function getOpenAIOAuthEntry( continue; } - // OpenCode 2 console credentials live under the `opencode` integration as - // OAuth entries; they authorize Console APIs, not OpenAI. - if (sourceKey === "opencode") { - const consoleEntry = entry as OpenAIOAuthData & { - methodID?: unknown; - metadata?: { orgID?: unknown } | null; - }; - if (consoleEntry.methodID === "server" || consoleEntry.metadata?.orgID != null) { - continue; - } - } - const accessToken = typeof entry.access === "string" ? entry.access.trim() : ""; if (accessToken) { return { sourceKey, entry, accessToken }; diff --git a/src/providers/openai.ts b/src/providers/openai.ts index 71d40441..e589455d 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -6,6 +6,7 @@ import type { QuotaProvider, QuotaProviderContext, QuotaProviderResult } from ". import { DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS, hasOpenAIOAuthCached, + OPENAI_AUTH_SOURCE_KEYS, queryOpenAIQuota, resolveOpenAIOAuth, } from "../lib/openai.js"; @@ -44,12 +45,8 @@ export const openaiProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { - // Console OAuth credentials under the `opencode` integration authorize - // Console APIs, not OpenAI; skip them silently. - const rows = (await readCredentialRows()).filter( - (row) => - ["openai", "codex", "chatgpt", "opencode"].includes(row.integrationId) && - !(row.integrationId === "opencode" && row.value.type === "oauth"), + const rows = (await readCredentialRows()).filter((row) => + (OPENAI_AUTH_SOURCE_KEYS as readonly string[]).includes(row.integrationId), ); const credentials = rows.flatMap((row) => { const auth = resolveOpenAIOAuth({ [row.integrationId]: row.value } as AuthData); diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 041c210c..34399bfc 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -67,7 +67,9 @@ type TuiContext = { data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void; session: { - get: (sessionID: string) => { model?: { id: string; providerID: string } } | undefined; + get: ( + sessionID: string, + ) => { parentID?: string; model?: { id: string; providerID: string } } | undefined; }; location?: { default: () => { directory: string }; @@ -94,8 +96,12 @@ type TuiContext = { claim: | { append: "app"; render: () => null } | { append: "sidebar.content"; render: (props: { sessionID: string }) => JSX.Element } - | { append: "prompt.footer" | "home.footer.status"; render: () => JSX.Element }, + | { append: "prompt.footer"; render: (props: { sessionID?: string }) => JSX.Element } + | { append: "home.footer.status"; render: () => JSX.Element }, ) => () => void; + router: { + current: () => { type: "home" } | { type: "session"; sessionID: string } | { type: "plugin" }; + }; toast: { show: (toast: Toast) => void }; dialog: { alert: (params: { title: string; message: string }) => Promise; @@ -347,6 +353,9 @@ async function getQuotaFooter( sessionID: string | undefined, surface: "prompt" | "home", ): Promise { + // OpenCode 2 also mounts the prompt footer under the Home prompt, without a + // session. As in v4, the prompt line belongs to session prompts only. + if (surface === "prompt" && !sessionID) return []; const resolvedRuntime = await resolveQuotaRuntimeContext({ client: quotaClient(context), roots: quotaRoots(context), @@ -583,7 +592,12 @@ async function runQuotaCommand( } } -function registerQuotaCommands(context: TuiContext, getSessionID: () => string | undefined): void { +function getRouteSessionID(context: TuiContext): string | undefined { + const route = context.ui.router.current(); + return route.type === "session" ? route.sessionID : undefined; +} + +function registerQuotaCommands(context: TuiContext): void { context.keymap.layer(() => ({ mode: "global", commands: QUOTA_DIALOG_COMMANDS.map((spec) => ({ @@ -594,17 +608,12 @@ function registerQuotaCommands(context: TuiContext, getSessionID: () => string | slash: spec.acceptsArguments ? { name: spec.slashName, arguments: true as const } : { name: spec.slashName }, - run: (input?: string) => runQuotaCommand(context, spec.id, getSessionID(), input), + run: (input?: string) => runQuotaCommand(context, spec.id, getRouteSessionID(context), input), })), })); } -function SidebarQuotaView(props: { - context: TuiContext; - sessionID: string; - setActiveSessionID: (sessionID: string) => void; -}): JSX.Element { - props.setActiveSessionID(props.sessionID); +function SidebarQuotaView(props: { context: TuiContext; sessionID: string }): JSX.Element { const [open, setOpen] = createSignal(true); const [quota, setQuota] = createSignal< { message: string; duration: number; activeProviderCount: number } | undefined @@ -661,17 +670,17 @@ const plugin = Plugin.define({ setup(context) { const api = context as unknown as TuiContext; let disposeEvents: (() => void) | undefined; - let activeSessionID: string | undefined; const questionToolCalls = new Set(); const disposeApp = api.ui.slot({ append: "app", render: () => { if (disposeEvents) return null; - registerQuotaCommands(api, () => activeSessionID); + registerQuotaCommands(api); const trigger = (event: TuiEvent, reason: "idle" | "compacted" | "question") => { const sessionID = getSessionID(event); if (!sessionID) return; - activeSessionID = sessionID; + // As in v4, subagent (child) sessions never show quota toasts. + if (api.data.session.get(sessionID)?.parentID) return; void getQuotaMessage(api, sessionID, reason) .then((quota) => { if (!quota) return; @@ -725,19 +734,11 @@ const plugin = Plugin.define({ }); const disposeSidebar = api.ui.slot({ append: "sidebar.content", - render: (props) => ( - { - activeSessionID = sessionID; - }} - /> - ), + render: (props) => , }); const disposePrompt = api.ui.slot({ append: "prompt.footer", - render: () => , + render: (props) => , }); const disposeHome = api.ui.slot({ append: "home.footer.status", diff --git a/tests/lib.openai.test.ts b/tests/lib.openai.test.ts index 83c97b0e..53846ccf 100644 --- a/tests/lib.openai.test.ts +++ b/tests/lib.openai.test.ts @@ -184,38 +184,24 @@ describe("openai auth resolution", () => { expect(out && out.success ? out.windows.hourly?.percentRemaining : -1).toBe(80); }); - it("reads auth from opencode when higher-priority keys are unusable", async () => { + it("never treats the OpenCode Console login under opencode as an OpenAI login", async () => { + // OpenCode 2 imports a legacy console login as `methodID: "device"`, often without an orgID. mocks.readAuthFileCached.mockResolvedValueOnce({ codex: { type: "oauth", access: " " }, openai: { type: "api", access: "ignored" }, chatgpt: { type: "oauth", access: " " }, - opencode: { type: "oauth", access: "a.b.c", expires: Date.now() + 60_000 }, - }); - - vi.stubGlobal( - "fetch", - vi.fn( - async () => - new Response( - JSON.stringify({ - plan_type: "free", - rate_limit: { - limit_reached: false, - primary_window: { - used_percent: 50, - limit_window_seconds: 18_000, - reset_after_seconds: 3600, - }, - secondary_window: null, - }, - }), - { status: 200 }, - ), - ) as any, - ); + opencode: { + type: "oauth", + methodID: "device", + access: "console-token", + expires: Date.now() + 60_000, + }, + } as any); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); - const out = await queryOpenAIQuota(); - expect(out && out.success ? out.windows.hourly?.percentRemaining : -1).toBe(50); + await expect(queryOpenAIQuota()).resolves.toBeNull(); + expect(fetchMock).not.toHaveBeenCalled(); }); it("uses cached auth reads for hasOpenAIOAuthCached", async () => { diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 7f06d7a2..866e1d8b 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -53,6 +53,7 @@ function startTui() { }, ), toast: { show: toast }, + router: { current: () => ({ type: "home" }) }, dialog: { alert, prompt, set: vi.fn() }, }, }; diff --git a/tests/plugin.question-hook.test.ts b/tests/plugin.question-hook.test.ts index 8106dd6c..8078e7d0 100644 --- a/tests/plugin.question-hook.test.ts +++ b/tests/plugin.question-hook.test.ts @@ -43,7 +43,8 @@ describe("V2 CLI question-tool accounting boundary", () => { handlers.get("session.tool.input.started")?.({ data: { name: "question", id: "call-1" } }); handlers.get("session.tool.success")?.({ data: { sessionID: "session-1", id: "call-1" } }); await vi.waitFor(() => expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1)); - expect(session.get).not.toHaveBeenCalled(); + // Only the subagent check reads the session; no model lookup follows. + expect(session.get).toHaveBeenCalledExactlyOnceWith("session-1"); expect(toast).not.toHaveBeenCalled(); }); diff --git a/tests/providers.openai.test.ts b/tests/providers.openai.test.ts index 5f002b35..9fc2bee7 100644 --- a/tests/providers.openai.test.ts +++ b/tests/providers.openai.test.ts @@ -14,6 +14,7 @@ import { createProviderAvailabilityContext } from "./helpers/provider-test-harne vi.mock("../src/lib/openai.js", () => ({ DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS: 5_000, + OPENAI_AUTH_SOURCE_KEYS: ["openai", "codex", "chatgpt"], hasOpenAIOAuthCached: vi.fn(), resolveOpenAIOAuth: vi.fn(() => ({ state: "none" })), queryOpenAIQuota: vi.fn(), diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index b68436b5..9e7e7ad4 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -18,7 +18,10 @@ type RegisteredCommand = { run: (input?: string) => Promise; }; -function startTui(location?: { directory: string }) { +function startTui( + location?: { directory: string }, + route: { type: "home" } | { type: "session"; sessionID: string } = { type: "home" }, +) { let layer: { commands: RegisteredCommand[] } | undefined; const context = { location, @@ -43,6 +46,7 @@ function startTui(location?: { directory: string }) { return vi.fn(); }), toast: { show: vi.fn() }, + router: { current: vi.fn(() => route) }, dialog: { alert: vi.fn().mockResolvedValue(undefined), prompt: vi.fn().mockResolvedValue(undefined), @@ -171,6 +175,15 @@ describe("V2 quota TUI commands", () => { } }); + it("runs commands for the session on screen, and without a session elsewhere", async () => { + await startTui(undefined, { type: "session", sessionID: "ses_open" }) + .command("tokens_session") + .run(); + await startTui(undefined, { type: "home" }).command("tokens_session").run(); + + expect(build.mock.calls.map(([params]) => params.sessionID)).toEqual(["ses_open", undefined]); + }); + it("reads the session model from the OpenCode 2 session store", async () => { const { context, command } = startTui(); diff --git a/tests/tui-v2-refresh.test.ts b/tests/tui-v2-refresh.test.ts index 90be5c19..c7ed321d 100644 --- a/tests/tui-v2-refresh.test.ts +++ b/tests/tui-v2-refresh.test.ts @@ -48,6 +48,10 @@ function setupFooterSlots( handlers.set(event, handler); return vi.fn(); }), + session: { + get: (sessionID: string) => + sessionID === "ses_child" ? { parentID: "ses_parent" } : { id: sessionID }, + }, }, keymap: { layer: vi.fn() }, ui: { @@ -142,7 +146,7 @@ describe("V2 footer refresh timer", () => { it("refreshes the prompt footer every minute without writing the export", async () => { const renderers = setupFooterSlots(); - const dispose = mountSlot(renderers.get("prompt.footer")); + const dispose = mountSlot(renderers.get("prompt.footer"), { sessionID: "ses_1" }); await flushPromises(); expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); @@ -157,6 +161,60 @@ describe("V2 footer refresh timer", () => { expect(writeQuotaExport).not.toHaveBeenCalled(); }); + it("loads the prompt footer for the session its composer passes in", async () => { + const handlers = new Map void>(); + const renderers = setupFooterSlots(handlers); + const dispose = mountSlot(renderers.get("prompt.footer"), { sessionID: "ses_2" }); + await flushPromises(); + expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1); + expect(resolveQuotaRuntimeContext.mock.calls[0][0].sessionID).toBe("ses_2"); + + handlers.get("session.step.ended")?.({ data: { sessionID: "ses_1" } }); + await flushPromises(); + expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1); + + handlers.get("session.step.ended")?.({ data: { sessionID: "ses_2" } }); + await flushPromises(); + expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(2); + expect(resolveQuotaRuntimeContext.mock.calls[1][0].sessionID).toBe("ses_2"); + dispose(); + }); + + it("renders nothing under the Home prompt, which has no session", async () => { + const renderers = setupFooterSlots(); + const dispose = mountSlot(renderers.get("prompt.footer"), { + sessionID: undefined, + mode: "normal", + showDetails: true, + }); + await flushPromises(); + await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS); + await flushPromises(); + + expect(resolveQuotaRuntimeContext).not.toHaveBeenCalled(); + expect(collectQuotaRenderData).not.toHaveBeenCalled(); + dispose(); + }); + + it("skips idle, compaction, and question toasts for subagent sessions, as in v4", async () => { + const handlers = new Map void>(); + const renderers = setupFooterSlots(handlers); + mountSlot(renderers.get("app")); + handlers.get("session.execution.succeeded")?.({ data: { sessionID: "ses_child" } }); + handlers.get("session.compaction.ended")?.({ data: { sessionID: "ses_child" } }); + handlers.get("session.tool.input.started")?.({ + data: { sessionID: "ses_child", id: "call_1", name: "question" }, + }); + handlers.get("session.tool.success")?.({ data: { sessionID: "ses_child", id: "call_1" } }); + await flushPromises(); + expect(resolveQuotaRuntimeContext).not.toHaveBeenCalled(); + + handlers.get("session.execution.succeeded")?.({ data: { sessionID: "ses_parent" } }); + await flushPromises(); + expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1); + expect(resolveQuotaRuntimeContext.mock.calls[0][0].sessionID).toBe("ses_parent"); + }); + it("loads Home for every enabled provider without a session, even with onlyCurrentModel", async () => { const runtime = await resolveQuotaRuntimeContext(); resolveQuotaRuntimeContext.mockResolvedValue({ diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 6e4d93cb..d2d902f1 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -179,6 +179,7 @@ async function setupV2Surfaces(client: ReturnType, provider const toast = vi.fn(); const slots: string[] = []; const renderers = new Map unknown>(); + let route: { type: "home" } | { type: "session"; sessionID: string } = { type: "home" }; vi.stubGlobal("React", { createElement: (type: unknown, props: Record | null) => typeof type === "function" ? type(props ?? {}) : { type, props }, @@ -194,6 +195,7 @@ async function setupV2Surfaces(client: ReturnType, provider events.set(event, callbacks); return () => callbacks.delete(callback); }, + session: { get: () => ({}) }, location: { default: () => ({ directory: process.cwd() }), provider: { list: () => providerIds.map((id) => ({ id })) }, @@ -212,6 +214,7 @@ async function setupV2Surfaces(client: ReturnType, provider return () => {}; }, toast: { show: toast }, + router: { current: () => route }, dialog: { alert, prompt: vi.fn(), set: vi.fn() }, }, } as never); @@ -237,7 +240,11 @@ async function setupV2Surfaces(client: ReturnType, provider quota: quota!, renderSidebar: (sessionID: string) => renderSurface("sidebar.content", renderedSurfaces.sidebar, { sessionID }), - renderSessionPrompt: () => renderSurface("prompt.footer", renderedSurfaces.compact), + renderSessionPrompt: (sessionID: string) => + renderSurface("prompt.footer", renderedSurfaces.compact, { sessionID }), + openSession: (sessionID: string) => { + route = { type: "session", sessionID }; + }, renderHomeBottom: () => renderSurface("home.footer.status", renderedSurfaces.compact), dispose: dispose as () => void, }; @@ -496,6 +503,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const v2 = await setupV2Surfaces(client, PHASE5_RUNTIME_PROVIDER_IDS); v2.emit("session.execution.succeeded", "phase5-session"); await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(1)); + v2.openSession("phase5-session"); await v2.quota.run(); expect(v2.alert).toHaveBeenCalledOnce(); expect(client.session.prompt).not.toHaveBeenCalled(); @@ -615,7 +623,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { assertFixtureContent(allWindowsSidebar); assertTreeSessionTokenTotals(allWindowsSidebar); expect(allWindowsSidebar).toContain("tree-model"); - const sessionPromptCompact = await v2.renderSessionPrompt(); + const sessionPromptCompact = await v2.renderSessionPrompt("phase5-session"); expect(sessionPromptCompact).toContain("64%"); expect(sessionPromptCompact).toContain("$12.34"); expect(sessionPromptCompact).toContain("80%"); @@ -813,7 +821,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(sidebarOutput).toContain("Remaining: -5 requests"); expect(sidebarOutput).toContain("Remaining: -20 requests"); - const compactOutput = await v2.renderSessionPrompt(); + const compactOutput = await v2.renderSessionPrompt("minimax-session"); expect(compactOutput.match(/0%/gu)).toHaveLength(2); expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(1); @@ -871,7 +879,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(sidebarOutput).toContain("Fable"); expect(sidebarOutput).toContain("98%"); - const compactOutput = await v2.renderSessionPrompt(); + const compactOutput = await v2.renderSessionPrompt("anthropic-fable-session"); expect(compactOutput).toContain("Fable"); expect(compactOutput).toContain("98%"); @@ -939,7 +947,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(sidebarOutput).toContain("46%"); expect(sidebarOutput).not.toContain("video"); - const compactOutput = await v2.renderSessionPrompt(); + const compactOutput = await v2.renderSessionPrompt("minimax-china-session"); expect(compactOutput).toContain("33%"); expect(compactOutput).toContain("46%"); expect(compactOutput).not.toContain("video"); @@ -959,7 +967,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(toastOutput).toContain("33%"); expect(toastOutput).toContain("46%"); - const sessionPromptCompact = await v2.renderSessionPrompt(); + const sessionPromptCompact = await v2.renderSessionPrompt("minimax-china-session"); expect(sessionPromptCompact).toContain("33%"); expect(sessionPromptCompact).not.toContain("46%"); const homeCompact = await v2.renderHomeBottom(); @@ -1075,7 +1083,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const v2 = await setupMiniMaxChinaSurfaces(); await v2.renderHomeBottom(); - await v2.renderSessionPrompt(); + await v2.renderSessionPrompt("minimax-china-session"); await new Promise((resolve) => setTimeout(resolve, 50)); await expect(readFile(exportPath, "utf8")).rejects.toThrow(); From 34d257345f38569561de0332d697d4a0c5a2bee1 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:30:20 +0200 Subject: [PATCH 46/97] fix(tui): make quota command dialogs scrollable --- src/tui-v2.tsx | 126 +++++++++++++++--- tests/plugin.command-handled-boundary.test.ts | 14 +- tests/tui-v2-commands.test.ts | 87 +++++++++++- tests/tui-v2-format-style.test.ts | 2 +- tests/tui-v2-refresh.test.ts | 2 +- tests/v4-phase5-cross-surface.test.ts | 57 ++++++-- 6 files changed, 247 insertions(+), 41 deletions(-) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 34399bfc..72bde915 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -1,8 +1,8 @@ /** @jsxImportSource @opentui/solid */ import { Plugin } from "@opencode/plugin/tui"; -import { RGBA } from "@opentui/core"; -import type { JSX } from "@opentui/solid"; +import { RGBA, type ScrollBoxRenderable, TextAttributes } from "@opentui/core"; +import { type JSX, useTerminalDimensions } from "@opentui/solid"; import { createSignal, onCleanup, Show } from "solid-js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; @@ -62,8 +62,22 @@ type Toast = { message: string; duration?: number; }; +type KeymapCommand = { + id?: string; + title: string; + group: string; + bind?: string; + palette?: true; + slash?: { name: string; arguments?: true }; + run: (input?: string) => void | Promise; +}; +type DialogTheme = { + text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA } } }; + background: { action: { primary: { focused: RGBA } } }; +}; type TuiContext = { location?: { directory: string }; + theme: { surface: (name: "dialog") => DialogTheme }; data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void; session: { @@ -77,19 +91,7 @@ type TuiContext = { }; }; keymap: { - layer: ( - build: () => { - mode: "global"; - commands: Array<{ - id: string; - title: string; - group: string; - palette: true; - slash: { name: string; arguments?: true }; - run: (input?: string) => Promise; - }>; - }, - ) => void; + layer: (build: () => { mode: "global" | "modal"; commands: KeymapCommand[] }) => void; }; ui: { slot: ( @@ -104,7 +106,8 @@ type TuiContext = { }; toast: { show: (toast: Toast) => void }; dialog: { - alert: (params: { title: string; message: string }) => Promise; + show: (render: () => JSX.Element, onClose?: () => void) => void; + clear: () => void; prompt: (params: { title: string; placeholder?: string }) => Promise; set: (params: { size: "medium" | "large" | "xlarge" }) => void; }; @@ -551,6 +554,87 @@ function reportFailure(error: unknown): void { console.warn(`[opencode-quota] failed to load quota: ${message}`); } +/** + * Shows command output like OpenCode's alert dialog, but inside a scrollbox so long + * reports (/quota_status, /tokens_*) stay reachable. The mouse wheel scrolls the box; + * arrows, PageUp/PageDown, and Home/End scroll it from the keyboard. Esc is handled by + * the host dialog; Enter and the ok/esc labels close it. + */ +function QuotaOutputDialog(props: { + context: TuiContext; + title: string; + message: string; +}): JSX.Element { + const theme = () => props.context.theme.surface("dialog"); + const dimensions = useTerminalDimensions(); + // The host dialog starts a quarter of the way down the terminal. The remaining + // 8 rows cover the title, ok button, paddings, gaps, and one spare row. + const maxHeight = () => Math.max(1, Math.floor(dimensions().height * 0.75) - 8); + let scroll: ScrollBoxRenderable | undefined; + const close = () => props.context.ui.dialog.clear(); + + props.context.keymap.layer(() => ({ + mode: "modal", + commands: [ + { bind: "return", title: "Close", group: "Dialog", run: close }, + { bind: "up", title: "Scroll up", group: "Dialog", run: () => scroll?.scrollBy(-1) }, + { bind: "down", title: "Scroll down", group: "Dialog", run: () => scroll?.scrollBy(1) }, + { + bind: "pageup", + title: "Scroll up one page", + group: "Dialog", + run: () => scroll?.scrollBy(-maxHeight()), + }, + { + bind: "pagedown", + title: "Scroll down one page", + group: "Dialog", + run: () => scroll?.scrollBy(maxHeight()), + }, + { bind: "home", title: "Scroll to top", group: "Dialog", run: () => scroll?.scrollTo(0) }, + { + bind: "end", + title: "Scroll to bottom", + group: "Dialog", + run: () => scroll?.scrollTo(scroll.scrollHeight), + }, + ], + })); + + return ( + + + + {props.title} + + + esc + + + + { + scroll = element; + }} + maxHeight={maxHeight()} + > + {props.message} + + + + + ok + + + + ); +} + async function runQuotaCommand( context: TuiContext, command: QuotaDialogCommandId, @@ -580,9 +664,13 @@ async function runQuotaCommand( resolveSessionMeta: (id) => getSessionModelMeta(context, id), }); if (result.state === "noop") return; - const alert = context.ui.dialog.alert({ title: result.title, message: result.output }); - context.ui.dialog.set({ size: result.dialogSize }); - await alert; + await new Promise((resolve) => { + context.ui.dialog.show( + () => , + resolve, + ); + context.ui.dialog.set({ size: result.dialogSize }); + }); } catch (error) { context.ui.toast.show({ variant: "error", diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 866e1d8b..928bbcd9 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -16,7 +16,8 @@ function startTui() { { run: (input?: string) => Promise; slash: { name: string } } >(); const listeners = new Map }) => void>(); - const alert = vi.fn().mockResolvedValue(undefined); + const show = vi.fn((_render: () => unknown, onClose?: () => void) => onClose?.()); + const set = vi.fn(); const prompt = vi.fn().mockResolvedValue(undefined); const toast = vi.fn(); const slots = new Map unknown }>(); @@ -54,11 +55,11 @@ function startTui() { ), toast: { show: toast }, router: { current: () => ({ type: "home" }) }, - dialog: { alert, prompt, set: vi.fn() }, + dialog: { show, clear: vi.fn(), prompt, set }, }, }; const dispose = tuiPlugin.setup(context as never); - return { commands, listeners, alert, prompt, toast, slots, context, dispose }; + return { commands, listeners, show, set, prompt, toast, slots, context, dispose }; } describe("V2 CLI command boundary", () => { @@ -92,7 +93,8 @@ describe("V2 CLI command boundary", () => { roots: resolveOpenCodeLocationRoots(process.cwd()), }), ); - expect(tui.alert).toHaveBeenCalledWith({ title: "Quota", message: "Quota ready" }); + expect(tui.show).toHaveBeenCalledOnce(); + expect(tui.set).toHaveBeenCalledWith({ size: "large" }); expect(tui.context.data.session.get).not.toHaveBeenCalled(); tui.dispose?.(); }); @@ -124,7 +126,7 @@ describe("V2 CLI command boundary", () => { mocks.build.mockResolvedValue({ state: "noop", command: "quota", reason: "disabled" }); const tui = startTui(); await tui.commands.get("quota.quota")?.run(); - expect(tui.alert).not.toHaveBeenCalled(); + expect(tui.show).not.toHaveBeenCalled(); expect(tui.toast).not.toHaveBeenCalled(); tui.dispose?.(); }); @@ -139,7 +141,7 @@ describe("V2 CLI command boundary", () => { message: "quota unavailable", }), ); - expect(tui.alert).not.toHaveBeenCalled(); + expect(tui.show).not.toHaveBeenCalled(); tui.dispose?.(); }); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 9e7e7ad4..07db8bf0 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -8,9 +8,28 @@ vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ ...(await importOriginal()), buildQuotaDialogCommandOutput: build, })); +vi.mock("@opentui/solid", () => ({ + useTerminalDimensions: () => () => ({ width: 120, height: 40 }), +})); import plugin from "../src/tui-v2.tsx"; +type Node = { type: string; props: Record }; + +function findNode(node: unknown, type: string): Node | undefined { + if (Array.isArray(node)) { + for (const child of node) { + const found = findNode(child, type); + if (found) return found; + } + return undefined; + } + if (!node || typeof node !== "object") return undefined; + const element = node as Node; + if (element.type === type) return element; + return findNode(element.props?.children, type); +} + type RegisteredCommand = { id: string; slash: { name: string; arguments?: true }; @@ -25,6 +44,12 @@ function startTui( let layer: { commands: RegisteredCommand[] } | undefined; const context = { location, + theme: { + surface: vi.fn(() => ({ + text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, + background: { action: { primary: { focused: "action-bg" } } }, + })), + }, data: { on: vi.fn(() => vi.fn()), session: { @@ -37,7 +62,7 @@ function startTui( }, keymap: { layer: vi.fn((next) => { - layer = next(); + layer ??= next(); }), }, ui: { @@ -48,7 +73,8 @@ function startTui( toast: { show: vi.fn() }, router: { current: vi.fn(() => route) }, dialog: { - alert: vi.fn().mockResolvedValue(undefined), + show: vi.fn((_render: () => unknown, onClose?: () => void) => onClose?.()), + clear: vi.fn(), prompt: vi.fn().mockResolvedValue(undefined), set: vi.fn(), }, @@ -197,4 +223,61 @@ describe("V2 quota TUI commands", () => { await expect(resolveSessionMeta("ses_unknown")).resolves.toEqual({}); expect(context.data.session.get).toHaveBeenCalledWith("ses_known"); }); + + it("shows command output in a scrollable dialog that fits the terminal", async () => { + vi.stubGlobal("React", { + createElement: ( + type: unknown, + props: Record | null, + ...children: unknown[] + ) => { + const all = { ...props, children: children.length > 1 ? children : children[0] }; + return typeof type === "function" ? type(all) : { type, props: all }; + }, + }); + const output = Array.from({ length: 80 }, (_, index) => `line ${index + 1}`).join("\n"); + build.mockResolvedValue({ + state: "output", + title: "Quota Status", + output, + dialogSize: "xlarge", + }); + const { context, command } = startTui(); + + await command("quota_status").run(); + + expect(context.ui.dialog.show).toHaveBeenCalledOnce(); + expect(context.ui.dialog.set).toHaveBeenCalledWith({ size: "xlarge" }); + const render = context.ui.dialog.show.mock.calls[0][0] as () => unknown; + const tree = render(); + const scrollbox = findNode(tree, "scrollbox"); + // 40 terminal rows: three quarters is 30, minus 8 rows of dialog chrome. + expect(scrollbox?.props.maxHeight).toBe(22); + expect(findNode(scrollbox?.props.children, "text")?.props.children).toBe(output); + + const dialogLayer = context.keymap.layer.mock.calls[1][0]() as { + mode: string; + commands: Array<{ bind: string; run: () => void }>; + }; + expect(dialogLayer.mode).toBe("modal"); + expect(dialogLayer.commands.map((item) => item.bind)).toEqual([ + "return", + "up", + "down", + "pageup", + "pagedown", + "home", + "end", + ]); + const scroll = { scrollBy: vi.fn(), scrollTo: vi.fn(), scrollHeight: 80 }; + scrollbox?.props.ref(scroll); + const run = (bind: string) => dialogLayer.commands.find((item) => item.bind === bind)!.run(); + run("pagedown"); + run("up"); + run("end"); + expect(scroll.scrollBy.mock.calls).toEqual([[22], [-1]]); + expect(scroll.scrollTo).toHaveBeenCalledWith(80); + run("return"); + expect(context.ui.dialog.clear).toHaveBeenCalledOnce(); + }); }); diff --git a/tests/tui-v2-format-style.test.ts b/tests/tui-v2-format-style.test.ts index e6f46fcf..a4121a85 100644 --- a/tests/tui-v2-format-style.test.ts +++ b/tests/tui-v2-format-style.test.ts @@ -83,7 +83,7 @@ describe("V2 sidebar format style", () => { return vi.fn(); }), toast: { show: vi.fn() }, - dialog: { alert: vi.fn(), prompt: vi.fn(), set: vi.fn() }, + dialog: { show: vi.fn(), clear: vi.fn(), prompt: vi.fn(), set: vi.fn() }, }, } as any); diff --git a/tests/tui-v2-refresh.test.ts b/tests/tui-v2-refresh.test.ts index c7ed321d..27c93f86 100644 --- a/tests/tui-v2-refresh.test.ts +++ b/tests/tui-v2-refresh.test.ts @@ -60,7 +60,7 @@ function setupFooterSlots( return vi.fn(); }), toast: { show: vi.fn() }, - dialog: { alert: vi.fn(), prompt: vi.fn(), set: vi.fn() }, + dialog: { show: vi.fn(), clear: vi.fn(), prompt: vi.fn(), set: vi.fn() }, }, } as any); return renderers; diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index d2d902f1..30993ad9 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -72,6 +72,10 @@ const otel = vi.hoisted(() => { }; }); +vi.mock("@opentui/solid", () => ({ + useTerminalDimensions: () => () => ({ width: 120, height: 40 }), +})); + const resetNotifications = vi.hoisted(() => ({ observe: vi.fn() })); vi.mock("../src/lib/quota-reset-notifications.js", async (importOriginal) => ({ ...(await importOriginal()), @@ -175,19 +179,47 @@ async function setupV2Surfaces(client: ReturnType, provider for (const callback of events.get(event) ?? []) callback({ data: { sessionID } }); }; let commands: Array<{ slash: { name: string }; run: (input?: unknown) => Promise }> = []; - const alert = vi.fn(async (_input: { title: string; message: string }) => {}); + // Records the title and scrollbox text of each quota output dialog the TUI shows. + const dialog = vi.fn((_input: { title: string; message: string }) => {}); + type Node = { type: string; props: Record }; + const find = (node: unknown, type: string): Node | undefined => { + if (Array.isArray(node)) return node.map((child) => find(child, type)).find(Boolean); + if (!node || typeof node !== "object") return undefined; + return (node as Node).type === type + ? (node as Node) + : find((node as Node).props?.children, type); + }; + const show = (render: () => unknown, onClose?: () => void) => { + const tree = render(); + const title = find(tree, "text")?.props.children; + const message = find(find(tree, "scrollbox"), "text")?.props.children; + dialog({ title, message }); + onClose?.(); + }; const toast = vi.fn(); const slots: string[] = []; const renderers = new Map unknown>(); let route: { type: "home" } | { type: "session"; sessionID: string } = { type: "home" }; vi.stubGlobal("React", { - createElement: (type: unknown, props: Record | null) => - typeof type === "function" ? type(props ?? {}) : { type, props }, + createElement: ( + type: unknown, + props: Record | null, + ...children: unknown[] + ) => { + const all = { ...props, children: children.length > 1 ? children : children[0] }; + return typeof type === "function" ? type(all) : { type, props: all }; + }, }); const { default: tuiPlugin } = await import("../src/tui-v2.js"); const dispose = tuiPlugin.setup({ client, location: { directory: process.cwd() }, + theme: { + surface: () => ({ + text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, + background: { action: { primary: { focused: "action-bg" } } }, + }), + }, data: { on: (event: string, callback: (event: { data: { sessionID: string } }) => void) => { const callbacks = events.get(event) ?? new Set(); @@ -202,8 +234,9 @@ async function setupV2Surfaces(client: ReturnType, provider }, }, keymap: { - layer: (build: () => { commands: typeof commands }) => { - commands = build().commands; + layer: (build: () => { mode: string; commands: typeof commands }) => { + const layer = build(); + if (layer.mode === "global") commands = layer.commands; }, }, ui: { @@ -215,7 +248,7 @@ async function setupV2Surfaces(client: ReturnType, provider }, toast: { show: toast }, router: { current: () => route }, - dialog: { alert, prompt: vi.fn(), set: vi.fn() }, + dialog: { show, clear: vi.fn(), prompt: vi.fn(), set: vi.fn() }, }, } as never); expect(slots).toEqual(["app", "sidebar.content", "prompt.footer", "home.footer.status"]); @@ -234,7 +267,7 @@ async function setupV2Surfaces(client: ReturnType, provider }; return { tool: tool!, - alert, + dialog, toast, emit, quota: quota!, @@ -505,9 +538,9 @@ describe("v4 Phase 5 cross-surface release evidence", () => { await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(1)); v2.openSession("phase5-session"); await v2.quota.run(); - expect(v2.alert).toHaveBeenCalledOnce(); + expect(v2.dialog).toHaveBeenCalledOnce(); expect(client.session.prompt).not.toHaveBeenCalled(); - const serverOutput = v2.alert.mock.calls[0][0].message; + const serverOutput = v2.dialog.mock.calls[0][0].message; expect(serverOutput).toMatch(/^Quota \(\/quota\)/); expect(serverOutput).not.toContain("```"); expect(serverOutput).not.toMatch(/^#{1,6} /mu); @@ -764,7 +797,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const v2 = await setupV2Surfaces(client, ["minimax-coding-plan"]); await v2.quota.run(); - const serverOutput = v2.alert.mock.calls[0][0].message; + const serverOutput = v2.dialog.mock.calls[0][0].message; expect(serverOutput).toContain("MiniMax Token Plan"); expect(serverOutput).toContain("5h quota"); expect(serverOutput).toContain("Weekly quota"); @@ -864,7 +897,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const v2 = await setupV2Surfaces(client, ["anthropic"]); await v2.quota.run(); - const serverOutput = v2.alert.mock.calls[0][0].message; + const serverOutput = v2.dialog.mock.calls[0][0].message; expect(serverOutput).toContain("Claude"); expect(serverOutput).toContain("Fable"); expect(serverOutput).toContain("98% left"); @@ -917,7 +950,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { currentConfig = configForSingleProvider("minimax-china-coding-plan"); const v2 = await setupMiniMaxChinaSurfaces(); await v2.quota.run(); - const serverOutput = v2.alert.mock.calls[0][0].message; + const serverOutput = v2.dialog.mock.calls[0][0].message; expect(serverOutput).toContain("MiniMax Token Plan"); expect(serverOutput).toContain("(CN)"); expect(serverOutput).toContain("5h quota"); From b62da7fa27334362b61570a4876b6bb382cb5401 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:00:47 +0200 Subject: [PATCH 47/97] feat(web): add quota slash commands for OpenCode 2 Web and Desktop --- README.md | 6 +- docs/readme/manual-install.md | 9 +- docs/readme/troubleshooting.md | 4 +- docs/readme/updating.md | 2 +- src/lib/init-installer.ts | 2 +- src/plugin.ts | 120 +++++++++-- tests/lib.init-installer.test.ts | 4 +- tests/plugin-v2.test.ts | 201 +++++++++++++++--- tests/plugin.command-handled-boundary.test.ts | 10 +- tests/plugin.quota-command.test.ts | 11 +- tests/provider-surfaces.opencode-go.test.ts | 2 + tests/v4-phase5-cross-surface.test.ts | 2 + 12 files changed, 308 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index a97bf0ac..f7cf37ac 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The updater prints the complete preview before its own config or cache changes. - OpenCode 1 is no longer supported. Stay on `4.10.3` if you still use OpenCode 1, or follow [Moving to OpenCode 2](docs/readme/updating.md#moving-to-opencode-2). - Logins are read only from OpenCode 2's database (`opencode.db`), never from `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. -- Web and Desktop show no quota slash commands, toasts, or panels, because OpenCode 2 gives plugins no Web UI hooks. Run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to run `quota_status`, a diagnostics tool it can call. +- Web and Desktop have the quota slash commands but no toasts or panels, because OpenCode 2 gives plugins no Web UI hooks. A command's result appears as a one-line session notice, cut off at the window edge. For the full report, run `npx @slkiser/opencode-quota show` in a terminal. - One `"plugin"` entry in `opencode.json` now loads both the server and the TUI; no `tui.json` entry is needed. TUI slash commands open a dialog. - `tuiCommandDisplay` and `tuiCompactStatus.suppressWhenNativeProviderQuota` were removed. Old keys are ignored. @@ -95,7 +95,7 @@ The updater prints the complete preview before its own config or cache changes. More ways to use it: -- Check quota outside the TUI: run `npx @slkiser/opencode-quota show` in a terminal. Web and Desktop have no quota UI; there the assistant can call the `quota_status` diagnostics tool. +- Check quota outside the TUI: run `npx @slkiser/opencode-quota show` in a terminal. In Web and Desktop, slash commands show a one-line notice, and the assistant can call the `quota_status` diagnostics tool. - See every OpenCode login: each connection for a provider gets its own rows, and `*` marks the active one. - Automate quota checks with JSON output for scripts, status bars, and CI. Optional OpenTelemetry metrics support monitoring tools. - Customize the display with [`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings), OpenCode Go's preferred collapsed-sidebar window, spaced reset countdowns by default with a `resetTimeSpaced: false` dense opt-out, decimal reset precision, bare percent labels, and [`accountingDetail`](docs/readme/configuration.md#show-accounting-detail). @@ -109,7 +109,7 @@ See [Configuration](docs/readme/configuration.md) for UI options and [Manual ins ### Core slash commands -These run in the OpenCode TUI and show results in a dialog. Type arguments after the command, such as `/tokens_between 2026-09-01 2026-09-25`, or leave them out and a prompt asks for them. +These run in the OpenCode TUI and show results in a dialog. Type arguments after the command, such as `/tokens_between 2026-09-01 2026-09-25`, or leave them out and a prompt asks for them. Web and Desktop list the same commands and show the result as a one-line session notice that is never sent to the model; see [Web and Desktop notes](docs/readme/manual-install.md#web-and-desktop-notes). | Command | Use when | | --------------------------------------- | --------------------------------------------------------------- | diff --git a/docs/readme/manual-install.md b/docs/readme/manual-install.md index eab66b44..ac1165bb 100644 --- a/docs/readme/manual-install.md +++ b/docs/readme/manual-install.md @@ -82,10 +82,17 @@ Put these settings in `quota-toast.jsonc`. | Quota bar under the prompt | `tuiPromptBar.enabled: true` | | Manual slash commands only | Disable sidebar, toast, and compact line | -TUI slash commands always open a dialog. Web and Desktop show none of these; run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to run its `quota_status` tool. +TUI slash commands always open a dialog. Web and Desktop show none of the settings above. See [Configuration](configuration.md) for more examples and every setting. +## Web and Desktop notes + +- Web and Desktop list the same slash commands. The result appears as a one-line notice in the session, cut off at the window edge. For the full report, run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to run its `quota_status` tool. +- The assistant never sees these notices: OpenCode Quota removes them from every model request. They stay in the session history. +- Known limitation: when OpenCode compacts a long session, a recent notice can leave one short placeholder line (`[OpenCode Quota slash command output...]`) in the summary. The report itself is never included. +- The TUI `/` menu lists each quota command twice: one opens the dialog, the other is the Web and Desktop version and adds a notice to the session. + ## Update safely Close OpenCode, preview the update, then apply it: diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index e3400168..27ae3c2b 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -19,7 +19,7 @@ OpenCode Quota reads logins only from OpenCode 2's `opencode.db`, never `auth.js | Problem | Try this | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Slash commands are missing | They exist only in the TUI. Check the plugin entry above, then restart OpenCode. | +| Slash commands are missing | Check the plugin entry above, then restart OpenCode. | | `/quota` shows no providers | Run `/quota_status` or `opencode-quota status`, then check provider detection and authentication. | | Sidebar is missing | Confirm the plugin is installed and `tuiSidebarPanel.enabled` is `true`. | | Compact line is missing | Confirm the plugin is installed and `tuiCompactStatus.enabled` is `true`. | @@ -27,7 +27,7 @@ OpenCode Quota reads logins only from OpenCode 2's `opencode.db`, never `auth.js | TUI toast is missing | Check `enableToast`, `showOnIdle`, `showOnQuestion`, and `showOnCompact`. Toasts appear only in the TUI. | | Token reports are empty | Start OpenCode once, then use a model so `opencode.db` contains usage. | | Pricing looks old | Run `/pricing_refresh`. | -| Web or Desktop shows no quota | Expected: OpenCode 2 gives plugins no Web UI. Run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to run `quota_status`. | +| Web or Desktop shows only one line | Expected: OpenCode 2 shows command results as a one-line notice. Run `npx @slkiser/opencode-quota show` in a terminal for the full report. | ## Update safely diff --git a/docs/readme/updating.md b/docs/readme/updating.md index 40b7e309..12ae2722 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -46,7 +46,7 @@ The updater builds one plan, prints it in full, and then either stops or applies 2. Run the update steps above. 3. Run `/quota_status` in the OpenCode TUI. If a provider you use is missing, log in to it again in OpenCode 2 (`/connect`, or `opencode auth login `). OpenCode Quota never reads `auth.json`. -The `opencode.json` plugin entry loads both the server and the TUI; no `tui.json` entry is needed. Web and Desktop have no quota slash commands, toasts, or panels on OpenCode 2. Use `npx @slkiser/opencode-quota show` in a terminal instead. +The `opencode.json` plugin entry loads both the server and the TUI; no `tui.json` entry is needed. Web and Desktop have the quota slash commands but no toasts or panels on OpenCode 2; see [Web and Desktop notes](manual-install.md#web-and-desktop-notes). For the full report there, use `npx @slkiser/opencode-quota show` in a terminal. ## Read the preview diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index 760477f0..96abf9c2 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -1185,7 +1185,7 @@ async function promptForSelections( if (interfaces === "web") { prompts.log.info( - "Web and Desktop show no quota UI. Run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to use its quota_status tool.", + "Web and Desktop show quota slash commands as one-line session notices, with no toasts or panels. For the full report, run `npx @slkiser/opencode-quota show` in a terminal.", ); } diff --git a/src/plugin.ts b/src/plugin.ts index 132fe540..b51ec153 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,9 +1,40 @@ -/** OpenCode V2 server plugin: deterministic quota diagnostics tool. */ +/** OpenCode V2 server plugin: quota slash commands for Web and Desktop, plus a diagnostics tool. */ import { Plugin } from "@opencode/plugin"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { reconcileDetectedProvidersInGlobalConfig } from "./lib/opencode-config-providers.js"; -import { buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js"; +import { + buildQuotaDialogCommandOutput, + QUOTA_DIALOG_COMMANDS, + type QuotaDialogCommandId, +} from "./lib/quota-dialog-commands.js"; + +/** + * The model-facing text of every quota slash command message. The report itself goes in + * the synthetic message's `description`, which Web, Desktop, and the TUI display but + * OpenCode never sends to the model. The session hooks below drop messages with exactly + * this text, so they never reach the model either. The text is stored with the message, + * so the filter keeps working after a restart. + */ +const QUOTA_COMMAND_MESSAGE_TEXT = + "[OpenCode Quota slash command output. Shown to the user only; not part of the conversation.]"; + +type ModelMessage = { + readonly role: string; + readonly content: ReadonlyArray<{ readonly type: string; readonly text?: string | null }>; +}; + +function withoutQuotaCommandMessages(messages: M[]): M[] { + return messages.filter( + (message) => + !( + message.role === "user" && + message.content.length === 1 && + message.content[0].type === "text" && + message.content[0].text === QUOTA_COMMAND_MESSAGE_TEXT + ), + ); +} export const QuotaToastPlugin = Plugin.define({ id: "@slkiser/opencode-quota.server", @@ -23,6 +54,34 @@ export const QuotaToastPlugin = Plugin.define({ }, }; + const buildOutput = ( + command: QuotaDialogCommandId, + sessionID: string, + argumentsText?: string, + ) => + buildQuotaDialogCommandOutput({ + command, + arguments: argumentsText, + client, + roots, + sessionID, + resolveSessionMeta: async (id) => { + const session = await ctx.session.get({ sessionID: id }); + return { modelID: session.model?.id, providerID: session.model?.providerID }; + }, + onDetectedProviderIds: async (providerIds) => { + if (providerIds.length === 0) return; + try { + await reconcileDetectedProvidersInGlobalConfig({ + configRootDir: roots.configRoot, + detectedProviderIds: providerIds, + }); + } catch (error) { + console.warn("Failed to add detected providers to global OpenCode config", error); + } + }, + }); + await ctx.tool.transform((editor) => { editor.add({ name: "quota_status", @@ -33,31 +92,46 @@ export const QuotaToastPlugin = Plugin.define({ additionalProperties: false, }, async execute(_input, context) { - const result = await buildQuotaDialogCommandOutput({ - command: "quota_status", - client, - roots, - sessionID: context.sessionID, - resolveSessionMeta: async (sessionID) => { - const session = await ctx.session.get({ sessionID }); - return { modelID: session.model?.id, providerID: session.model?.providerID }; - }, - onDetectedProviderIds: async (providerIds) => { - if (providerIds.length === 0) return; - try { - await reconcileDetectedProvidersInGlobalConfig({ - configRootDir: roots.configRoot, - detectedProviderIds: providerIds, - }); - } catch (error) { - console.warn("Failed to add detected providers to global OpenCode config", error); - } - }, - }); + const result = await buildOutput("quota_status", context.sessionID); return { content: result.state === "output" ? sanitizeDisplayText(result.output) : "" }; }, }); }); + + // Web and Desktop list these in their "/" menu. The TUI also registers dialog + // commands with the same names, so the TUI "/" menu lists each name twice. + await ctx.command.transform((editor) => { + for (const spec of QUOTA_DIALOG_COMMANDS) { + editor.add({ + name: spec.slashName, + description: spec.description, + async execute(invocation) { + const result = await buildOutput( + spec.id, + invocation.sessionID, + invocation.prompt.text.trim() || undefined, + ); + if (result.state === "noop") return; + // resume: false admits the message without starting a model turn. It waits in the + // session inbox until the next prompt; Web, Desktop, and the TUI show it right away. + await ctx.session.synthetic({ + sessionID: invocation.sessionID, + text: QUOTA_COMMAND_MESSAGE_TEXT, + description: sanitizeDisplayText(result.output), + resume: false, + }); + }, + }); + } + }); + + // Keep quota command messages out of every model request built from session history: + // normal turns, compaction summaries, and generate requests. + for (const hook of ["context", "compaction", "generate"] as const) { + await ctx.session.hook(hook, (event) => { + event.messages = withoutQuotaCommandMessages(event.messages); + }); + } }, }); diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index 713a68bd..4ba80765 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -1096,14 +1096,14 @@ describe("init installer planning and merge behavior", () => { expect(secondPlan.edits.find((edit) => edit.kind === "opencode")?.changed).toBe(false); }); - it("tells Web users that Web and Desktop show no quota UI", async () => { + it("tells Web users what Web and Desktop show", async () => { const prompts = createPromptStub({ selectValues: ["web", Symbol.for("cancel")] }); const code = await runInitInstaller({ cwd: tempDir, prompts: prompts as any }); expect(code).toBe(0); expect(prompts.infoCalls).toContain( - "Web and Desktop show no quota UI. Run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to use its quota_status tool.", + "Web and Desktop show quota slash commands as one-line session notices, with no toasts or panels. For the full report, run `npx @slkiser/opencode-quota show` in a terminal.", ); }); diff --git a/tests/plugin-v2.test.ts b/tests/plugin-v2.test.ts index c363bbd4..2dc79743 100644 --- a/tests/plugin-v2.test.ts +++ b/tests/plugin-v2.test.ts @@ -1,41 +1,86 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const buildOutput = vi.hoisted(() => vi.fn()); -vi.mock("../src/lib/quota-dialog-commands.js", () => ({ +vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ + ...(await importOriginal()), buildQuotaDialogCommandOutput: buildOutput, })); +import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-commands.js"; import plugin from "../src/plugin.js"; +type RegisteredTool = { + name: string; + execute: (input: unknown, context: { sessionID: string }) => Promise<{ content: string }>; +}; +type RegisteredCommand = { + name: string; + description?: string; + execute: (invocation: { + sessionID: string; + prompt: { text: string }; + delivery: "steer" | "queue"; + }) => Promise; +}; +type HookEvent = { + messages: Array<{ role: string; content: Array<{ type: string; text?: string }> }>; +}; + +function createContext() { + const tools: RegisteredTool[] = []; + const commands: RegisteredCommand[] = []; + const hooks = new Map void>(); + const ctx = { + location: { directory: "/tmp/opencode/quota-plugin-v2" }, + provider: { list: vi.fn().mockResolvedValue({ data: [{ id: "openai" }] }) }, + session: { + get: vi.fn().mockResolvedValue({ model: { id: "gpt-5", providerID: "openai" } }), + synthetic: vi.fn().mockResolvedValue({}), + hook: vi.fn(async (name: string, callback: (event: HookEvent) => void) => { + hooks.set(name, callback); + return { dispose: async () => {} }; + }), + }, + tool: { + transform: vi.fn(async (callback) => { + callback({ add: (tool: RegisteredTool) => tools.push(tool) }); + }), + }, + command: { + transform: vi.fn(async (callback) => { + callback({ add: (command: RegisteredCommand) => commands.push(command) }); + }), + }, + }; + return { ctx, tools, commands, hooks }; +} + +function findCommand(commands: RegisteredCommand[], name: string): RegisteredCommand { + const found = commands.find((item) => item.name === name); + if (!found) throw new Error(`Command not registered: ${name}`); + return found; +} + +function runCommand(commands: RegisteredCommand[], name: string, text = "") { + return findCommand(commands, name).execute({ + sessionID: "session-web", + prompt: { text }, + delivery: "steer", + }); +} + describe("V2 server plugin", () => { + beforeEach(() => { + buildOutput.mockReset(); + }); + it("registers a structured quota diagnostics tool without a server toast dependency", async () => { - let registered: - | { - name: string; - execute: (input: unknown, context: { sessionID: string }) => Promise<{ content: string }>; - } - | undefined; - const ctx = { - location: { directory: "/tmp/opencode/quota-plugin-v2" }, - provider: { list: vi.fn().mockResolvedValue({ data: [{ id: "openai" }] }) }, - session: { - get: vi.fn().mockResolvedValue({ model: { id: "gpt-5", providerID: "openai" } }), - }, - tool: { - transform: vi.fn(async (callback) => { - callback({ - add: (tool: typeof registered) => { - registered = tool; - }, - }); - }), - }, - }; + const { ctx, tools } = createContext(); buildOutput.mockResolvedValue({ state: "output", output: "Quota ready" }); await plugin.setup(ctx as never); - expect(registered?.name).toBe("quota_status"); - const output = await registered?.execute({}, { sessionID: "session-test" }); + expect(tools.map((tool) => tool.name)).toEqual(["quota_status"]); + const output = await tools[0].execute({}, { sessionID: "session-test" }); expect(output).toEqual({ content: "Quota ready" }); expect(buildOutput).toHaveBeenCalledWith( expect.objectContaining({ @@ -45,4 +90,108 @@ describe("V2 server plugin", () => { ); expect(ctx.provider.list).toHaveBeenCalledTimes(0); }); + + it("registers the same slash commands as the TUI", async () => { + const { ctx, commands } = createContext(); + + await plugin.setup(ctx as never); + expect(commands.map((item) => ({ name: item.name, description: item.description }))).toEqual( + QUOTA_DIALOG_COMMANDS.map((spec) => ({ + name: spec.slashName, + description: spec.description, + })), + ); + }); + + it("shows command output as a display-only synthetic message without a model turn", async () => { + const { ctx, commands } = createContext(); + buildOutput.mockResolvedValue({ state: "output", output: "# Quota\nopenai 42%\u001b[31m" }); + + await plugin.setup(ctx as never); + await runCommand(commands, "quota"); + + expect(buildOutput).toHaveBeenCalledWith( + expect.objectContaining({ command: "quota", sessionID: "session-web", arguments: undefined }), + ); + expect(ctx.session.synthetic).toHaveBeenCalledTimes(1); + const input = ctx.session.synthetic.mock.calls[0][0]; + expect(input).toEqual({ + sessionID: "session-web", + text: expect.any(String), + description: "# Quota\nopenai 42%", + resume: false, + }); + expect(input.text).not.toContain("openai"); + }); + + it("passes typed arguments to the output builder", async () => { + const { ctx, commands } = createContext(); + buildOutput.mockResolvedValue({ state: "output", output: "report" }); + + await plugin.setup(ctx as never); + await runCommand(commands, "tokens_between", " 2026-09-01 2026-09-25 "); + + expect(buildOutput).toHaveBeenCalledWith( + expect.objectContaining({ command: "tokens_between", arguments: "2026-09-01 2026-09-25" }), + ); + }); + + it("shows the builder's usage text when /tokens_between has no arguments", async () => { + const { ctx, commands } = createContext(); + const usage = "Invalid arguments for /tokens_between\n\nExpected: /tokens_between YYYY-MM-DD"; + buildOutput.mockResolvedValue({ state: "output", output: usage }); + + await plugin.setup(ctx as never); + await runCommand(commands, "tokens_between", " "); + + expect(buildOutput).toHaveBeenCalledWith( + expect.objectContaining({ command: "tokens_between", arguments: undefined }), + ); + expect(ctx.session.synthetic.mock.calls[0][0].description).toBe(usage); + }); + + it("shows nothing when the plugin is disabled", async () => { + const { ctx, commands } = createContext(); + buildOutput.mockResolvedValue({ state: "noop", command: "quota", reason: "disabled" }); + + await plugin.setup(ctx as never); + await runCommand(commands, "quota"); + + expect(ctx.session.synthetic).not.toHaveBeenCalled(); + }); + + it("removes quota command messages from model requests and keeps everything else", async () => { + const { ctx, commands, hooks } = createContext(); + buildOutput.mockResolvedValue({ state: "output", output: "report" }); + + await plugin.setup(ctx as never); + await runCommand(commands, "quota"); + const marker = ctx.session.synthetic.mock.calls[0][0].text as string; + + expect([...hooks.keys()]).toEqual(["context", "compaction", "generate"]); + for (const hook of hooks.values()) { + const user = { role: "user", content: [{ type: "text", text: "What is my quota?" }] }; + const assistant = { role: "assistant", content: [{ type: "text", text: marker }] }; + const quoted = { + role: "user", + content: [ + { type: "text", text: marker }, + { type: "text", text: "Please explain the line above." }, + ], + }; + const event: HookEvent = { + messages: [ + { role: "user", content: [{ type: "text", text: marker }] }, + user, + assistant, + quoted, + { role: "user", content: [{ type: "text", text: marker }] }, + ], + }; + + hook(event); + + expect(event.messages).toEqual([user, assistant, quoted]); + } + }); }); diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 928bbcd9..7f372a45 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -145,17 +145,21 @@ describe("V2 CLI command boundary", () => { tui.dispose?.(); }); - it("does not offer V1 web/server slash interception or agent-config normalization", async () => { - // V2 CLI commands are local keymap registrations. The server plugin only provides a tool; - // there is no V2 server command.execute.before hook to inject output into web sessions. + it("does not offer V1 slash interception or agent-config normalization", async () => { + // V2 TUI commands are local keymap registrations. The server plugin registers a tool and + // V2 server commands for Web and Desktop; it has no V1 command.execute.before hook. const server = (await import("../src/plugin.js")).default; const transform = vi.fn(); + const commandTransform = vi.fn(); await server.setup({ location: { directory: process.cwd() }, tool: { transform }, + command: { transform: commandTransform }, + session: { hook: vi.fn() }, provider: { list: vi.fn() }, } as never); expect(transform).toHaveBeenCalledOnce(); + expect(commandTransform).toHaveBeenCalledOnce(); expect((server as Record)["command.execute.before"]).toBeUndefined(); expect((server as Record).config).toBeUndefined(); }); diff --git a/tests/plugin.quota-command.test.ts b/tests/plugin.quota-command.test.ts index 8095cd7c..57b64fd2 100644 --- a/tests/plugin.quota-command.test.ts +++ b/tests/plugin.quota-command.test.ts @@ -68,7 +68,8 @@ async function createV2StatusTool(directory: string) { await server.setup({ location: { directory }, provider: { list: vi.fn().mockResolvedValue({ data: [] }) }, - session: { get: vi.fn().mockResolvedValue({}) }, + session: { get: vi.fn().mockResolvedValue({}), hook: vi.fn() }, + command: { transform: vi.fn() }, tool: { transform: async ( register: (editor: { add: (tool: { execute: typeof execute }) => void }) => void, @@ -165,7 +166,7 @@ describe("/quota command behavior", () => { await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); - it("registers the V2 server diagnostics tool with provider repair and no command hooks", async () => { + it("registers the V2 server diagnostics tool with provider repair and no V1 command hooks", async () => { const dialogModule = await import("../src/lib/quota-dialog-commands.js"); const output = vi .spyOn(dialogModule, "buildQuotaDialogCommandOutput") @@ -196,7 +197,11 @@ describe("/quota command behavior", () => { ), }, provider: { list: vi.fn().mockResolvedValue({ data: [] }) }, - session: { get: vi.fn().mockResolvedValue({ model: { providerID: "openai", id: "gpt-5" } }) }, + session: { + get: vi.fn().mockResolvedValue({ model: { providerID: "openai", id: "gpt-5" } }), + hook: vi.fn(), + }, + command: { transform: vi.fn() }, }; await server.setup(ctx as never); expect(tool?.name).toBe("quota_status"); diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 083b24a8..3ec13722 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -118,7 +118,9 @@ async function runQuotaStatus(sessionID: string): Promise { provider: { list: vi.fn().mockResolvedValue({ data: [{ id: "opencode-go" }] }) }, session: { get: vi.fn().mockResolvedValue({ model: { id: "model", providerID: "opencode-go" } }), + hook: vi.fn(), }, + command: { transform: vi.fn() }, tool: { transform: async (callback: (editor: { add: (tool: typeof quotaTool) => void }) => void) => callback({ diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 30993ad9..0dec59fa 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -161,7 +161,9 @@ async function setupV2Surfaces(client: ReturnType, provider provider: { list: vi.fn(async () => ({ data: providerIds.map((id) => ({ id })) })) }, session: { get: vi.fn(async () => ({ model: { id: "model-one", providerID: providerIds[0] } })), + hook: vi.fn(), }, + command: { transform: vi.fn() }, tool: { transform: vi.fn(async (callback: (editor: { add(value: RegisteredTool): void }) => void) => { callback({ From 28c613990114c8eaeebb5ca4707a5a57b6b79aab Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:53:31 +0200 Subject: [PATCH 48/97] feat(web): show full quota reports on Web and Desktop without duplicate TUI commands --- README.md | 8 +- docs/readme/manual-install.md | 11 +- docs/readme/troubleshooting.md | 2 +- docs/readme/updating.md | 2 +- src/lib/init-installer.ts | 2 +- src/lib/quota-dialog-commands.ts | 4 - src/lib/quota-report-message.ts | 58 ++++++ src/plugin.ts | 90 ++++++--- src/tui-v2.tsx | 68 +++++-- tests/lib.init-installer.test.ts | 2 +- tests/lib.quota-report-message.test.ts | 44 +++++ tests/plugin-v2.test.ts | 178 +++++++++++++----- tests/plugin.command-handled-boundary.test.ts | 24 +-- tests/plugin.tokens-session-all.test.ts | 10 +- tests/tui-v2-commands.test.ts | 122 +++++++----- tests/v4-phase5-cross-surface.test.ts | 6 +- 16 files changed, 448 insertions(+), 183 deletions(-) create mode 100644 src/lib/quota-report-message.ts create mode 100644 tests/lib.quota-report-message.test.ts diff --git a/README.md b/README.md index f7cf37ac..3b6ab2f7 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ The updater prints the complete preview before its own config or cache changes. - OpenCode 1 is no longer supported. Stay on `4.10.3` if you still use OpenCode 1, or follow [Moving to OpenCode 2](docs/readme/updating.md#moving-to-opencode-2). - Logins are read only from OpenCode 2's database (`opencode.db`), never from `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. -- Web and Desktop have the quota slash commands but no toasts or panels, because OpenCode 2 gives plugins no Web UI hooks. A command's result appears as a one-line session notice, cut off at the window edge. For the full report, run `npx @slkiser/opencode-quota show` in a terminal. -- One `"plugin"` entry in `opencode.json` now loads both the server and the TUI; no `tui.json` entry is needed. TUI slash commands open a dialog. +- Web and Desktop have the quota slash commands but no toasts or panels, because OpenCode 2 gives plugins no Web UI hooks. On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. +- One `"plugin"` entry in `opencode.json` now loads both the server and the TUI; no `tui.json` entry is needed. TUI slash commands post the report in the chat and open a dialog; the command palette opens only the dialog. - `tuiCommandDisplay` and `tuiCompactStatus.suppressWhenNativeProviderQuota` were removed. Old keys are ignored. ## Choose your setup @@ -95,7 +95,7 @@ The updater prints the complete preview before its own config or cache changes. More ways to use it: -- Check quota outside the TUI: run `npx @slkiser/opencode-quota show` in a terminal. In Web and Desktop, slash commands show a one-line notice, and the assistant can call the `quota_status` diagnostics tool. +- Check quota outside the TUI: run `npx @slkiser/opencode-quota show` in a terminal. In Web and Desktop, slash commands post the report in the chat, and the assistant can call the `quota_status` diagnostics tool. - See every OpenCode login: each connection for a provider gets its own rows, and `*` marks the active one. - Automate quota checks with JSON output for scripts, status bars, and CI. Optional OpenTelemetry metrics support monitoring tools. - Customize the display with [`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings), OpenCode Go's preferred collapsed-sidebar window, spaced reset countdowns by default with a `resetTimeSpaced: false` dense opt-out, decimal reset precision, bare percent labels, and [`accountingDetail`](docs/readme/configuration.md#show-accounting-detail). @@ -109,7 +109,7 @@ See [Configuration](docs/readme/configuration.md) for UI options and [Manual ins ### Core slash commands -These run in the OpenCode TUI and show results in a dialog. Type arguments after the command, such as `/tokens_between 2026-09-01 2026-09-25`, or leave them out and a prompt asks for them. Web and Desktop list the same commands and show the result as a one-line session notice that is never sent to the model; see [Web and Desktop notes](docs/readme/manual-install.md#web-and-desktop-notes). +These run in the OpenCode TUI, Web, and Desktop. Each posts its report in the chat as your message, and the AI never answers it; the TUI also opens the report in a dialog. Type arguments after the command, such as `/tokens_between 2026-09-01 2026-09-25`. In the TUI command palette, the same commands open only the dialog and ask for missing dates. See [Web and Desktop notes](docs/readme/manual-install.md#web-and-desktop-notes). | Command | Use when | | --------------------------------------- | --------------------------------------------------------------- | diff --git a/docs/readme/manual-install.md b/docs/readme/manual-install.md index ac1165bb..a7f757d2 100644 --- a/docs/readme/manual-install.md +++ b/docs/readme/manual-install.md @@ -82,16 +82,17 @@ Put these settings in `quota-toast.jsonc`. | Quota bar under the prompt | `tuiPromptBar.enabled: true` | | Manual slash commands only | Disable sidebar, toast, and compact line | -TUI slash commands always open a dialog. Web and Desktop show none of the settings above. +TUI slash commands post the report in the chat and open a dialog. Web and Desktop show none of the settings above. See [Configuration](configuration.md) for more examples and every setting. ## Web and Desktop notes -- Web and Desktop list the same slash commands. The result appears as a one-line notice in the session, cut off at the window edge. For the full report, run `npx @slkiser/opencode-quota show` in a terminal, or ask the assistant to run its `quota_status` tool. -- The assistant never sees these notices: OpenCode Quota removes them from every model request. They stay in the session history. -- Known limitation: when OpenCode compacts a long session, a recent notice can leave one short placeholder line (`[OpenCode Quota slash command output...]`) in the summary. The report itself is never included. -- The TUI `/` menu lists each quota command twice: one opens the dialog, the other is the Web and Desktop version and adds a notice to the session. +- On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. +- Each report starts with `[OpenCode Quota report]` and ends with `[End of OpenCode Quota report]`. These lines keep the report out of compaction summaries too. +- Web shows chat messages in a proportional font, so report columns may not line up. For aligned columns, use the TUI or run `npx @slkiser/opencode-quota show` in a terminal. +- If the AI is working when you run a command, the report appears after the AI finishes. +- A new session whose first message is a report keeps its default title. ## Update safely diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 27ae3c2b..1a933af2 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -27,7 +27,7 @@ OpenCode Quota reads logins only from OpenCode 2's `opencode.db`, never `auth.js | TUI toast is missing | Check `enableToast`, `showOnIdle`, `showOnQuestion`, and `showOnCompact`. Toasts appear only in the TUI. | | Token reports are empty | Start OpenCode once, then use a model so `opencode.db` contains usage. | | Pricing looks old | Run `/pricing_refresh`. | -| Web or Desktop shows only one line | Expected: OpenCode 2 shows command results as a one-line notice. Run `npx @slkiser/opencode-quota show` in a terminal for the full report. | +| Web report columns do not line up | Expected: Web uses a proportional font. Use the TUI or run `npx @slkiser/opencode-quota show` in a terminal. | ## Update safely diff --git a/docs/readme/updating.md b/docs/readme/updating.md index 12ae2722..3b9f575e 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -46,7 +46,7 @@ The updater builds one plan, prints it in full, and then either stops or applies 2. Run the update steps above. 3. Run `/quota_status` in the OpenCode TUI. If a provider you use is missing, log in to it again in OpenCode 2 (`/connect`, or `opencode auth login `). OpenCode Quota never reads `auth.json`. -The `opencode.json` plugin entry loads both the server and the TUI; no `tui.json` entry is needed. Web and Desktop have the quota slash commands but no toasts or panels on OpenCode 2; see [Web and Desktop notes](manual-install.md#web-and-desktop-notes). For the full report there, use `npx @slkiser/opencode-quota show` in a terminal. +The `opencode.json` plugin entry loads both the server and the TUI; no `tui.json` entry is needed. Web and Desktop have the quota slash commands, which post the report in the chat, but no toasts or panels on OpenCode 2; see [Web and Desktop notes](manual-install.md#web-and-desktop-notes). ## Read the preview diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index 96abf9c2..ffd44a84 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -1185,7 +1185,7 @@ async function promptForSelections( if (interfaces === "web") { prompts.log.info( - "Web and Desktop show quota slash commands as one-line session notices, with no toasts or panels. For the full report, run `npx @slkiser/opencode-quota show` in a terminal.", + "Web and Desktop have quota slash commands but no toasts or panels. A command posts its report in the chat as your message; the AI never answers it.", ); } diff --git a/src/lib/quota-dialog-commands.ts b/src/lib/quota-dialog-commands.ts index a4884492..f9bc295c 100644 --- a/src/lib/quota-dialog-commands.ts +++ b/src/lib/quota-dialog-commands.ts @@ -65,7 +65,6 @@ export type QuotaDialogCommandSpec = { description: string; dialogSize: "medium" | "large" | "xlarge"; requiresSession?: boolean; - acceptsArguments?: boolean; }; export type QuotaDialogCommandOutputResult = @@ -225,7 +224,6 @@ export const QUOTA_DIALOG_COMMANDS: readonly QuotaDialogCommandSpec[] = [ title: "OpenCode Quota Announcements", description: "List active bundled maintainer announcements.", dialogSize: "xlarge", - acceptsArguments: true, }, { id: "pricing_refresh", @@ -233,7 +231,6 @@ export const QUOTA_DIALOG_COMMANDS: readonly QuotaDialogCommandSpec[] = [ title: "OpenCode Quota Pricing Refresh", description: "Refresh the local runtime pricing snapshot from models.dev.", dialogSize: "xlarge", - acceptsArguments: true, }, ...TOKEN_REPORT_COMMANDS.map( (spec): QuotaDialogCommandSpec => ({ @@ -243,7 +240,6 @@ export const QUOTA_DIALOG_COMMANDS: readonly QuotaDialogCommandSpec[] = [ description: spec.description, dialogSize: "xlarge", requiresSession: spec.kind === "session" || spec.kind === "session_tree", - acceptsArguments: spec.kind === "between", }), ), ] as const; diff --git a/src/lib/quota-report-message.ts b/src/lib/quota-report-message.ts new file mode 100644 index 00000000..fe9b1c61 --- /dev/null +++ b/src/lib/quota-report-message.ts @@ -0,0 +1,58 @@ +/** + * On OpenCode 2, quota slash commands post their report into the chat as a user message + * that never starts a model turn. These helpers mark such messages so the server plugin + * can keep them out of every model request, and the TUI can show them in its dialog. + */ +import type { QuotaDialogCommandId } from "./quota-dialog-commands.js"; + +/** Metadata key on every quota report message. OpenCode stores message metadata with the message. */ +export const QUOTA_REPORT_METADATA_KEY = "opencodeQuota"; + +export type QuotaReportMetadata = { + command: QuotaDialogCommandId; + title: string; + at: number; +}; + +/** + * First and last line of every quota report message; Web and Desktop show them too. + * When OpenCode compacts a session, it copies recent message text into the checkpoint + * before plugin hooks run, so the metadata is lost there. These lines let the plugin cut + * the report back out of that text. + */ +const QUOTA_REPORT_START = "[OpenCode Quota report]"; +const QUOTA_REPORT_END = "[End of OpenCode Quota report]"; +const QUOTA_REPORT_BLOCKS = /\[OpenCode Quota report\][\s\S]*?\[End of OpenCode Quota report\]/g; + +export function formatQuotaReportMessage(report: string): string { + return `${QUOTA_REPORT_START}\n${report}\n${QUOTA_REPORT_END}`; +} + +/** The report inside a quota report message, without its first and last line. */ +export function readQuotaReport(text: string): string { + const prefix = `${QUOTA_REPORT_START}\n`; + const suffix = `\n${QUOTA_REPORT_END}`; + if (!text.startsWith(prefix) || !text.endsWith(suffix)) return text; + return text.slice(prefix.length, text.length - suffix.length); +} + +export function containsQuotaReport(text: string): boolean { + return text.includes(QUOTA_REPORT_START); +} + +/** Cuts every complete quota report, first to last line, out of the text. */ +export function removeQuotaReports(text: string): string { + return text.replace(QUOTA_REPORT_BLOCKS, ""); +} + +export function readQuotaReportMetadata( + metadata: Readonly> | undefined, +): QuotaReportMetadata | undefined { + const value = metadata?.[QUOTA_REPORT_METADATA_KEY]; + if (!value || typeof value !== "object") return undefined; + const { command, title, at } = value as Record; + if (typeof command !== "string" || typeof title !== "string" || typeof at !== "number") { + return undefined; + } + return { command: command as QuotaDialogCommandId, title, at }; +} diff --git a/src/plugin.ts b/src/plugin.ts index b51ec153..cf619b56 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,4 +1,4 @@ -/** OpenCode V2 server plugin: quota slash commands for Web and Desktop, plus a diagnostics tool. */ +/** OpenCode V2 server plugin: quota slash commands for every client, plus a diagnostics tool. */ import { Plugin } from "@opencode/plugin"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; @@ -8,32 +8,41 @@ import { QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, } from "./lib/quota-dialog-commands.js"; - -/** - * The model-facing text of every quota slash command message. The report itself goes in - * the synthetic message's `description`, which Web, Desktop, and the TUI display but - * OpenCode never sends to the model. The session hooks below drop messages with exactly - * this text, so they never reach the model either. The text is stored with the message, - * so the filter keeps working after a restart. - */ -const QUOTA_COMMAND_MESSAGE_TEXT = - "[OpenCode Quota slash command output. Shown to the user only; not part of the conversation.]"; +import { + containsQuotaReport, + formatQuotaReportMessage, + QUOTA_REPORT_METADATA_KEY, + type QuotaReportMetadata, + removeQuotaReports, +} from "./lib/quota-report-message.js"; type ModelMessage = { readonly role: string; + readonly metadata?: Readonly>; readonly content: ReadonlyArray<{ readonly type: string; readonly text?: string | null }>; }; -function withoutQuotaCommandMessages(messages: M[]): M[] { - return messages.filter( - (message) => - !( - message.role === "user" && - message.content.length === 1 && - message.content[0].type === "text" && - message.content[0].text === QUOTA_COMMAND_MESSAGE_TEXT - ), - ); +type ModelMessagePart = ModelMessage["content"][number]; + +function hasQuotaReportText(part: ModelMessagePart): boolean { + return part.type === "text" && typeof part.text === "string" && containsQuotaReport(part.text); +} + +/** + * Drops quota report messages, found by their metadata, and cuts reports out of the text of + * the remaining user messages. The cut covers compaction checkpoints, which hold copied + * message text without its metadata, and title requests, which get plain text only. + */ +function withoutQuotaReports(messages: M[]): M[] { + return messages.flatMap((message): M[] => { + if (message.role !== "user") return [message]; + if (message.metadata?.[QUOTA_REPORT_METADATA_KEY] !== undefined) return []; + if (!message.content.some(hasQuotaReportText)) return [message]; + const content = message.content.map((part) => + hasQuotaReportText(part) ? { ...part, text: removeQuotaReports(part.text as string) } : part, + ); + return [{ ...message, content }]; + }); } export const QuotaToastPlugin = Plugin.define({ @@ -98,8 +107,8 @@ export const QuotaToastPlugin = Plugin.define({ }); }); - // Web and Desktop list these in their "/" menu. The TUI also registers dialog - // commands with the same names, so the TUI "/" menu lists each name twice. + // Web, Desktop, and the TUI list these in their "/" menu. The TUI opens its dialog when + // a report arrives; its command palette runs the same reports without posting them. await ctx.command.transform((editor) => { for (const spec of QUOTA_DIALOG_COMMANDS) { editor.add({ @@ -112,12 +121,23 @@ export const QuotaToastPlugin = Plugin.define({ invocation.prompt.text.trim() || undefined, ); if (result.state === "noop") return; - // resume: false admits the message without starting a model turn. It waits in the - // session inbox until the next prompt; Web, Desktop, and the TUI show it right away. - await ctx.session.synthetic({ + // A message admitted while the AI works is delivered into that turn at its next + // step, and can make the AI take one more step. Wait until the session is idle. + await ctx.session.wait({ sessionID: invocation.sessionID }); + const metadata: QuotaReportMetadata = { + command: spec.id, + title: result.title, + at: Date.now(), + }; + // The report is posted as the user's message. resume: false admits it without + // starting a model turn, so it waits in the session inbox, where Web, Desktop, + // and the TUI show it. "steer" delivers it together with the user's next message; + // "queue" would later give it a model turn of its own. + await ctx.session.prompt({ sessionID: invocation.sessionID, - text: QUOTA_COMMAND_MESSAGE_TEXT, - description: sanitizeDisplayText(result.output), + text: formatQuotaReportMessage(sanitizeDisplayText(result.output)), + metadata: { [QUOTA_REPORT_METADATA_KEY]: metadata }, + delivery: "steer", resume: false, }); }, @@ -125,13 +145,23 @@ export const QuotaToastPlugin = Plugin.define({ } }); - // Keep quota command messages out of every model request built from session history: + // Keep quota reports out of every model request built from session history: // normal turns, compaction summaries, and generate requests. for (const hook of ["context", "compaction", "generate"] as const) { await ctx.session.hook(hook, (event) => { - event.messages = withoutQuotaCommandMessages(event.messages); + event.messages = withoutQuotaReports(event.messages); }); } + // OpenCode titles a new session from its first user message, which can be a report. + // With nothing else to title from, skip the title request and keep the default title. + await ctx.session.hook("title", (event) => { + if (!event.messages.some((message) => message.content.some(hasQuotaReportText))) return; + event.messages = withoutQuotaReports(event.messages); + const textLeft = event.messages.some((message) => + message.content.some((part) => part.type === "text" && part.text.trim() !== ""), + ); + if (!textLeft) event.result = ""; + }); }, }); diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 72bde915..75c6d768 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -32,6 +32,7 @@ import { collectConcreteEnabledProviderIds, collectQuotaRenderData, } from "./lib/quota-render-data.js"; +import { readQuotaReport, readQuotaReportMetadata } from "./lib/quota-report-message.js"; import { formatQuotaResetNotification, observeQuotaResetNotifications, @@ -68,8 +69,7 @@ type KeymapCommand = { group: string; bind?: string; palette?: true; - slash?: { name: string; arguments?: true }; - run: (input?: string) => void | Promise; + run: () => void | Promise; }; type DialogTheme = { text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA } } }; @@ -635,17 +635,29 @@ function QuotaOutputDialog(props: { ); } +function showQuotaOutputDialog( + context: TuiContext, + output: { title: string; message: string; dialogSize: "medium" | "large" | "xlarge" }, +): Promise { + return new Promise((resolve) => { + context.ui.dialog.show( + () => , + resolve, + ); + context.ui.dialog.set({ size: output.dialogSize }); + }); +} + +/** Runs a quota command from the command palette. It opens the dialog and posts no message. */ async function runQuotaCommand( context: TuiContext, command: QuotaDialogCommandId, sessionID: string | undefined, - input?: string, ): Promise { const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.id === command)!; - let argumentsText = input?.trim() || undefined; - // Only /tokens_between needs arguments. /quota_announcements and /pricing_refresh - // accept typed arguments only to reject them, so they run without a prompt. - if (command === "tokens_between" && argumentsText === undefined) { + let argumentsText: string | undefined; + // Only /tokens_between needs arguments; the palette asks for them. + if (command === "tokens_between") { const value = await context.ui.dialog.prompt({ title: spec.title, placeholder: "YYYY-MM-DD YYYY-MM-DD", @@ -664,12 +676,10 @@ async function runQuotaCommand( resolveSessionMeta: (id) => getSessionModelMeta(context, id), }); if (result.state === "noop") return; - await new Promise((resolve) => { - context.ui.dialog.show( - () => , - resolve, - ); - context.ui.dialog.set({ size: result.dialogSize }); + await showQuotaOutputDialog(context, { + title: result.title, + message: result.output, + dialogSize: result.dialogSize, }); } catch (error) { context.ui.toast.show({ @@ -680,6 +690,28 @@ async function runQuotaCommand( } } +/** + * Typed quota slash commands run on the server, which posts the report into the chat. + * When a report arrives for the session on screen, open it in the dialog too. + */ +function showPostedQuotaReport(context: TuiContext, event: TuiEvent): void { + const sessionID = getSessionID(event); + if (!sessionID || sessionID !== getRouteSessionID(context)) return; + const item = event.data?.item as + | { type?: string; payload?: { text?: unknown; metadata?: Record } } + | undefined; + if (item?.type !== "user" || typeof item.payload?.text !== "string") return; + const report = readQuotaReportMetadata(item.payload.metadata); + if (!report) return; + const spec = QUOTA_DIALOG_COMMANDS.find((candidate) => candidate.id === report.command); + if (!spec) return; + void showQuotaOutputDialog(context, { + title: report.title, + message: readQuotaReport(item.payload.text), + dialogSize: spec.dialogSize, + }); +} + function getRouteSessionID(context: TuiContext): string | undefined { const route = context.ui.router.current(); return route.type === "session" ? route.sessionID : undefined; @@ -692,11 +724,9 @@ function registerQuotaCommands(context: TuiContext): void { id: `quota.${spec.id}`, title: spec.title, group: "OpenCode Quota", + // No slash entry: the server plugin registers the "/" commands, so each is listed once. palette: true, - slash: spec.acceptsArguments - ? { name: spec.slashName, arguments: true as const } - : { name: spec.slashName }, - run: (input?: string) => runQuotaCommand(context, spec.id, getRouteSessionID(context), input), + run: () => runQuotaCommand(context, spec.id, getRouteSessionID(context)), })), })); } @@ -809,12 +839,16 @@ const plugin = Plugin.define({ const id = event.data?.id; if (typeof id === "string") questionToolCalls.delete(id); }); + const onInboxEnqueued = api.data.on("session.inbox.enqueued", (event) => + showPostedQuotaReport(api, event), + ); disposeEvents = () => { onExecutionSucceeded(); onCompacted(); onQuestionStarted(); onQuestionSucceeded(); onQuestionFailed(); + onInboxEnqueued(); questionToolCalls.clear(); }; return null; diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index 4ba80765..e04970a7 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -1103,7 +1103,7 @@ describe("init installer planning and merge behavior", () => { expect(code).toBe(0); expect(prompts.infoCalls).toContain( - "Web and Desktop show quota slash commands as one-line session notices, with no toasts or panels. For the full report, run `npx @slkiser/opencode-quota show` in a terminal.", + "Web and Desktop have quota slash commands but no toasts or panels. A command posts its report in the chat as your message; the AI never answers it.", ); }); diff --git a/tests/lib.quota-report-message.test.ts b/tests/lib.quota-report-message.test.ts new file mode 100644 index 00000000..84a65b03 --- /dev/null +++ b/tests/lib.quota-report-message.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; + +import { + containsQuotaReport, + formatQuotaReportMessage, + readQuotaReport, + readQuotaReportMetadata, + removeQuotaReports, +} from "../src/lib/quota-report-message.js"; + +describe("quota report messages", () => { + it("wraps the report in a fixed first and last line and reads it back", () => { + const message = formatQuotaReportMessage("# Quota\nopenai ███░░ 42%"); + + expect(message).toBe( + "[OpenCode Quota report]\n# Quota\nopenai ███░░ 42%\n[End of OpenCode Quota report]", + ); + expect(containsQuotaReport(message)).toBe(true); + expect(readQuotaReport(message)).toBe("# Quota\nopenai ███░░ 42%"); + expect(readQuotaReport("plain text")).toBe("plain text"); + }); + + it("cuts every complete report out of text and leaves everything else", () => { + const first = formatQuotaReportMessage("openai 42%"); + const second = formatQuotaReportMessage("anthropic 7%"); + const text = `[User]: hi\n\n[User]: ${first}\n\n[Assistant]: ok\n\n[User]: ${second}`; + + expect(removeQuotaReports(text)).toBe("[User]: hi\n\n[User]: \n\n[Assistant]: ok\n\n[User]: "); + expect(removeQuotaReports("[OpenCode Quota report] without an end line")).toBe( + "[OpenCode Quota report] without an end line", + ); + expect(removeQuotaReports("no report")).toBe("no report"); + }); + + it("reads only well-formed report metadata", () => { + const metadata = { command: "quota", title: "OpenCode Quota", at: 1 }; + + expect(readQuotaReportMetadata({ opencodeQuota: metadata })).toEqual(metadata); + expect(readQuotaReportMetadata(undefined)).toBeUndefined(); + expect(readQuotaReportMetadata({ displayText: "hi" })).toBeUndefined(); + expect(readQuotaReportMetadata({ opencodeQuota: "quota" })).toBeUndefined(); + expect(readQuotaReportMetadata({ opencodeQuota: { command: "quota" } })).toBeUndefined(); + }); +}); diff --git a/tests/plugin-v2.test.ts b/tests/plugin-v2.test.ts index 2dc79743..9e444be6 100644 --- a/tests/plugin-v2.test.ts +++ b/tests/plugin-v2.test.ts @@ -7,6 +7,7 @@ vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ })); import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-commands.js"; +import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; import plugin from "../src/plugin.js"; type RegisteredTool = { @@ -22,20 +23,27 @@ type RegisteredCommand = { delivery: "steer" | "queue"; }) => Promise; }; -type HookEvent = { - messages: Array<{ role: string; content: Array<{ type: string; text?: string }> }>; -}; +type Part = { type: string; text?: string }; +type Message = { role: string; metadata?: Record; content: Part[] }; +type HookEvent = { messages: Message[]; result?: string }; function createContext() { const tools: RegisteredTool[] = []; const commands: RegisteredCommand[] = []; const hooks = new Map void>(); + const calls: string[] = []; const ctx = { location: { directory: "/tmp/opencode/quota-plugin-v2" }, provider: { list: vi.fn().mockResolvedValue({ data: [{ id: "openai" }] }) }, session: { get: vi.fn().mockResolvedValue({ model: { id: "gpt-5", providerID: "openai" } }), - synthetic: vi.fn().mockResolvedValue({}), + wait: vi.fn(async () => { + calls.push("wait"); + }), + prompt: vi.fn(async () => { + calls.push("prompt"); + return {}; + }), hook: vi.fn(async (name: string, callback: (event: HookEvent) => void) => { hooks.set(name, callback); return { dispose: async () => {} }; @@ -52,7 +60,7 @@ function createContext() { }), }, }; - return { ctx, tools, commands, hooks }; + return { ctx, tools, commands, hooks, calls }; } function findCommand(commands: RegisteredCommand[], name: string): RegisteredCommand { @@ -65,10 +73,22 @@ function runCommand(commands: RegisteredCommand[], name: string, text = "") { return findCommand(commands, name).execute({ sessionID: "session-web", prompt: { text }, - delivery: "steer", + delivery: "queue", }); } +function hook(hooks: Map void>, name: string) { + const found = hooks.get(name); + if (!found) throw new Error(`Hook not registered: ${name}`); + return found; +} + +const user = (text: string, metadata?: Record): Message => ({ + role: "user", + ...(metadata ? { metadata } : {}), + content: [{ type: "text", text }], +}); + describe("V2 server plugin", () => { beforeEach(() => { buildOutput.mockReset(); @@ -103,9 +123,14 @@ describe("V2 server plugin", () => { ); }); - it("shows command output as a display-only synthetic message without a model turn", async () => { - const { ctx, commands } = createContext(); - buildOutput.mockResolvedValue({ state: "output", output: "# Quota\nopenai 42%\u001b[31m" }); + it("posts the report as a user message that never starts a model turn", async () => { + const { ctx, commands, calls } = createContext(); + buildOutput.mockResolvedValue({ + state: "output", + title: "OpenCode Quota", + output: "# Quota\nopenai 42%\u001b[31m", + }); + vi.spyOn(Date, "now").mockReturnValue(1_790_000_000_000); await plugin.setup(ctx as never); await runCommand(commands, "quota"); @@ -113,20 +138,24 @@ describe("V2 server plugin", () => { expect(buildOutput).toHaveBeenCalledWith( expect.objectContaining({ command: "quota", sessionID: "session-web", arguments: undefined }), ); - expect(ctx.session.synthetic).toHaveBeenCalledTimes(1); - const input = ctx.session.synthetic.mock.calls[0][0]; - expect(input).toEqual({ + expect(ctx.session.wait).toHaveBeenCalledWith({ sessionID: "session-web" }); + expect(calls).toEqual(["wait", "prompt"]); + expect(ctx.session.prompt).toHaveBeenCalledTimes(1); + expect(ctx.session.prompt.mock.calls[0][0]).toEqual({ sessionID: "session-web", - text: expect.any(String), - description: "# Quota\nopenai 42%", + text: formatQuotaReportMessage("# Quota\nopenai 42%"), + metadata: { + opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1_790_000_000_000 }, + }, + delivery: "steer", resume: false, }); - expect(input.text).not.toContain("openai"); + vi.mocked(Date.now).mockRestore(); }); it("passes typed arguments to the output builder", async () => { const { ctx, commands } = createContext(); - buildOutput.mockResolvedValue({ state: "output", output: "report" }); + buildOutput.mockResolvedValue({ state: "output", title: "Tokens", output: "report" }); await plugin.setup(ctx as never); await runCommand(commands, "tokens_between", " 2026-09-01 2026-09-25 "); @@ -136,10 +165,10 @@ describe("V2 server plugin", () => { ); }); - it("shows the builder's usage text when /tokens_between has no arguments", async () => { + it("posts the builder's usage text when /tokens_between has no arguments", async () => { const { ctx, commands } = createContext(); const usage = "Invalid arguments for /tokens_between\n\nExpected: /tokens_between YYYY-MM-DD"; - buildOutput.mockResolvedValue({ state: "output", output: usage }); + buildOutput.mockResolvedValue({ state: "output", title: "Tokens", output: usage }); await plugin.setup(ctx as never); await runCommand(commands, "tokens_between", " "); @@ -147,51 +176,106 @@ describe("V2 server plugin", () => { expect(buildOutput).toHaveBeenCalledWith( expect.objectContaining({ command: "tokens_between", arguments: undefined }), ); - expect(ctx.session.synthetic.mock.calls[0][0].description).toBe(usage); + expect(ctx.session.prompt.mock.calls[0][0].text).toBe(formatQuotaReportMessage(usage)); }); - it("shows nothing when the plugin is disabled", async () => { + it("posts nothing when the plugin is disabled", async () => { const { ctx, commands } = createContext(); buildOutput.mockResolvedValue({ state: "noop", command: "quota", reason: "disabled" }); await plugin.setup(ctx as never); await runCommand(commands, "quota"); - expect(ctx.session.synthetic).not.toHaveBeenCalled(); + expect(ctx.session.wait).not.toHaveBeenCalled(); + expect(ctx.session.prompt).not.toHaveBeenCalled(); }); - it("removes quota command messages from model requests and keeps everything else", async () => { - const { ctx, commands, hooks } = createContext(); - buildOutput.mockResolvedValue({ state: "output", output: "report" }); - + it("removes only tagged quota reports from model requests", async () => { + const { ctx, hooks } = createContext(); await plugin.setup(ctx as never); - await runCommand(commands, "quota"); - const marker = ctx.session.synthetic.mock.calls[0][0].text as string; - - expect([...hooks.keys()]).toEqual(["context", "compaction", "generate"]); - for (const hook of hooks.values()) { - const user = { role: "user", content: [{ type: "text", text: "What is my quota?" }] }; - const assistant = { role: "assistant", content: [{ type: "text", text: marker }] }; - const quoted = { - role: "user", - content: [ - { type: "text", text: marker }, - { type: "text", text: "Please explain the line above." }, - ], + const report = formatQuotaReportMessage("openai 42%"); + + expect([...hooks.keys()]).toEqual(["context", "compaction", "generate", "title"]); + for (const name of ["context", "compaction", "generate"]) { + const tagged = user(report, { opencodeQuota: { command: "quota", title: "Quota", at: 1 } }); + const question = user("What is my quota?"); + const otherMetadata = user("Hello", { displayText: "Hello", comments: [] }); + const assistant: Message = { + role: "assistant", + metadata: { opencodeQuota: { command: "quota", title: "Quota", at: 1 } }, + content: [{ type: "text", text: "openai 42%" }], }; + const image: Message = { role: "user", content: [{ type: "media" }] }; const event: HookEvent = { - messages: [ - { role: "user", content: [{ type: "text", text: marker }] }, - user, - assistant, - quoted, - { role: "user", content: [{ type: "text", text: marker }] }, - ], + messages: [tagged, question, otherMetadata, assistant, image, { ...tagged }], }; - hook(event); + hook(hooks, name)(event); - expect(event.messages).toEqual([user, assistant, quoted]); + expect(event.messages).toEqual([question, otherMetadata, assistant, image]); + expect(event.messages[0]).toBe(question); + expect(event.messages[1]).toBe(otherMetadata); } }); + + it("cuts reports out of compaction checkpoint text and keeps the rest", async () => { + const { ctx, hooks } = createContext(); + await plugin.setup(ctx as never); + const report = formatQuotaReportMessage("openai 42%\nanthropic 7%"); + const checkpoint: Message = { + role: "user", + content: [ + { + type: "text", + text: `\n[User]: Fix the bug\n\n[User]: ${report}\n\n[Assistant]: Done\n`, + }, + { type: "text", text: "no report here" }, + ], + }; + const assistantEcho: Message = { role: "assistant", content: [{ type: "text", text: report }] }; + + for (const name of ["context", "compaction", "generate"]) { + const event: HookEvent = { messages: [checkpoint, assistantEcho] }; + + hook(hooks, name)(event); + + expect(event.messages[0].content).toEqual([ + { + type: "text", + text: "\n[User]: Fix the bug\n\n[User]: \n\n[Assistant]: Done\n", + }, + { type: "text", text: "no report here" }, + ]); + expect(event.messages[0].content[1]).toBe(checkpoint.content[1]); + expect(event.messages[1]).toBe(assistantEcho); + } + expect(checkpoint.content[0].text).toContain("openai 42%"); + }); + + it("never titles a session from a quota report", async () => { + const { ctx, hooks } = createContext(); + await plugin.setup(ctx as never); + const title = hook(hooks, "title"); + const report = formatQuotaReportMessage("openai 42%"); + + const reportOnly: HookEvent = { messages: [user(report)] }; + title(reportOnly); + expect(reportOnly.result).toBe(""); + expect(reportOnly.messages).toEqual([user("")]); + + const mixed: HookEvent = { + messages: [user(`Original request:\n${report}\n\nRecent conversation:\nUser: Fix the bug`)], + }; + title(mixed); + expect(mixed.result).toBeUndefined(); + expect(mixed.messages).toEqual([ + user("Original request:\n\n\nRecent conversation:\nUser: Fix the bug"), + ]); + + const plain = user("Fix the bug"); + const normal: HookEvent = { messages: [plain] }; + title(normal); + expect(normal.result).toBeUndefined(); + expect(normal.messages[0]).toBe(plain); + }); }); diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 7f372a45..6b18eab6 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -11,10 +11,7 @@ vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ })); function startTui() { - const commands = new Map< - string, - { run: (input?: string) => Promise; slash: { name: string } } - >(); + const commands = new Map Promise; slash?: unknown }>(); const listeners = new Map }) => void>(); const show = vi.fn((_render: () => unknown, onClose?: () => void) => onClose?.()); const set = vi.fn(); @@ -34,11 +31,7 @@ function startTui() { layer: vi.fn( ( build: () => { - commands: Array<{ - id: string; - run: (input?: string) => Promise; - slash: { name: string }; - }>; + commands: Array<{ id: string; run: () => Promise; slash?: unknown }>; }, ) => { for (const command of build().commands) commands.set(command.id, command); @@ -72,12 +65,13 @@ describe("V2 CLI command boundary", () => { }); }); - it("registers the 12 local slash commands, not V1 server command hooks", () => { + it("registers the 12 palette commands without slash entries, not V1 server command hooks", () => { const tui = startTui(); expect(tui.commands.size).toBe(12); - expect([...tui.commands.values()].map((item) => item.slash.name)).toEqual( - QUOTA_DIALOG_COMMANDS.map((item) => item.slashName), + expect([...tui.commands.keys()]).toEqual( + QUOTA_DIALOG_COMMANDS.map((item) => `quota.${item.id}`), ); + expect([...tui.commands.values()].some((item) => item.slash !== undefined)).toBe(false); expect(new Set(QUOTA_DIALOG_COMMANDS.map((item) => item.id)).size).toBe(12); expect(tui.context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "app" })); tui.dispose?.(); @@ -99,16 +93,16 @@ describe("V2 CLI command boundary", () => { tui.dispose?.(); }); - it("passes explicit /tokens_between arguments to the deterministic output builder", async () => { + it("passes the prompted /tokens_between range to the deterministic output builder", async () => { const tui = startTui(); - await tui.commands.get("quota.tokens_between")?.run("not-a-date-range"); + tui.prompt.mockResolvedValue("not-a-date-range"); + await tui.commands.get("quota.tokens_between")?.run(); expect(mocks.build).toHaveBeenCalledWith( expect.objectContaining({ command: "tokens_between", arguments: "not-a-date-range", }), ); - expect(tui.prompt).not.toHaveBeenCalled(); tui.dispose?.(); }); diff --git a/tests/plugin.tokens-session-all.test.ts b/tests/plugin.tokens-session-all.test.ts index b5025351..7a1ddbfb 100644 --- a/tests/plugin.tokens-session-all.test.ts +++ b/tests/plugin.tokens-session-all.test.ts @@ -111,13 +111,13 @@ describe("/tokens_session_all command", () => { ]); }); - it("registers /tokens_session_all as a V2 CLI slash command", async () => { + it("registers /tokens_session_all in the V2 TUI command palette", async () => { const { default: plugin } = await import("../src/tui-v2.js"); const { QUOTA_DIALOG_COMMANDS } = await import("../src/lib/quota-dialog-commands.js"); const tokensSessionAllCommand = QUOTA_DIALOG_COMMANDS.find( (command) => command.id === "tokens_session_all", ); - let commands: Array<{ slash: { name: string }; title: string }> = []; + let commands: Array<{ id: string; title: string }> = []; plugin.setup({ keymap: { layer: (build: () => { commands: typeof commands }) => { @@ -133,9 +133,9 @@ describe("/tokens_session_all command", () => { }, } as never); - expect( - commands.find((command) => command.slash.name === tokensSessionAllCommand?.slashName), - ).toEqual(expect.objectContaining({ title: tokensSessionAllCommand?.title })); + expect(commands.find((command) => command.id === "quota.tokens_session_all")).toEqual( + expect.objectContaining({ title: tokensSessionAllCommand?.title }), + ); }); it("aggregates the current session tree for /tokens_session_all", async () => { diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 07db8bf0..d66d7079 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -12,6 +12,7 @@ vi.mock("@opentui/solid", () => ({ useTerminalDimensions: () => () => ({ width: 120, height: 40 }), })); +import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; import plugin from "../src/tui-v2.tsx"; type Node = { type: string; props: Record }; @@ -32,16 +33,18 @@ function findNode(node: unknown, type: string): Node | undefined { type RegisteredCommand = { id: string; - slash: { name: string; arguments?: true }; + slash?: unknown; palette: boolean; - run: (input?: string) => Promise; + run: () => Promise; }; +type Listener = (event: { data?: Record }) => void; function startTui( location?: { directory: string }, route: { type: "home" } | { type: "session"; sessionID: string } = { type: "home" }, ) { let layer: { commands: RegisteredCommand[] } | undefined; + const listeners = new Map(); const context = { location, theme: { @@ -51,7 +54,10 @@ function startTui( })), }, data: { - on: vi.fn(() => vi.fn()), + on: vi.fn((name: string, listener: Listener) => { + listeners.set(name, listener); + return vi.fn(); + }), session: { get: vi.fn((sessionID: string) => sessionID === "ses_known" @@ -83,7 +89,8 @@ function startTui( plugin.setup(context as any); const commands = layer!.commands; const command = (id: string) => commands.find((item) => item.id === `quota.${id}`)!; - return { context, commands, command }; + const emit = (name: string, data: Record) => listeners.get(name)?.({ data }); + return { context, commands, command, emit }; } describe("V2 quota TUI commands", () => { @@ -96,24 +103,25 @@ describe("V2 quota TUI commands", () => { }); }); - it("registers every quota command as a local slash command", () => { + it("registers every quota command in the palette and leaves slash commands to the server", () => { const { context, commands } = startTui(); - expect(commands.map((command) => command.slash.name)).toEqual([ - "quota", - "quota_status", - "quota_announcements", - "pricing_refresh", - "tokens_today", - "tokens_daily", - "tokens_weekly", - "tokens_monthly", - "tokens_all", - "tokens_session", - "tokens_session_all", - "tokens_between", + expect(commands.map((command) => command.id)).toEqual([ + "quota.quota", + "quota.quota_status", + "quota.quota_announcements", + "quota.pricing_refresh", + "quota.tokens_today", + "quota.tokens_daily", + "quota.tokens_weekly", + "quota.tokens_monthly", + "quota.tokens_all", + "quota.tokens_session", + "quota.tokens_session_all", + "quota.tokens_between", ]); expect(commands.every((command) => command.palette)).toBe(true); + expect(commands.some((command) => "slash" in command)).toBe(false); expect(context.ui.slot).toHaveBeenCalledWith(expect.objectContaining({ append: "app" })); expect(context.ui.slot).toHaveBeenCalledWith( expect.objectContaining({ append: "sidebar.content" }), @@ -130,58 +138,74 @@ describe("V2 quota TUI commands", () => { "session.tool.input.started", "session.tool.success", "session.tool.failed", + "session.inbox.enqueued", ]); }); - it("asks OpenCode to keep typed slash arguments only for commands that take arguments", () => { - const { commands } = startTui(); - - expect( - commands.filter((command) => command.slash.arguments).map((command) => command.slash.name), - ).toEqual(["quota_announcements", "pricing_refresh", "tokens_between"]); - }); - - it("passes the typed /tokens_between string to the command without prompting", async () => { + it("prompts for /tokens_between dates in the palette and stops when cancelled", async () => { const { context, command } = startTui(); - await command("tokens_between").run(" 2026-09-01 2026-09-25 "); + await command("tokens_between").run(); + expect(context.ui.dialog.prompt).toHaveBeenCalledOnce(); + expect(build).not.toHaveBeenCalled(); - expect(context.ui.dialog.prompt).not.toHaveBeenCalled(); + context.ui.dialog.prompt.mockResolvedValue(" 2026-09-01 2026-09-25 "); + await command("tokens_between").run(); expect(build).toHaveBeenCalledWith( expect.objectContaining({ command: "tokens_between", arguments: "2026-09-01 2026-09-25" }), ); }); - it("prompts for /tokens_between arguments only when none were typed", async () => { + it("runs /quota_announcements and /pricing_refresh from the palette without a prompt", async () => { const { context, command } = startTui(); - await command("tokens_between").run(""); - await command("tokens_between").run(); - - expect(context.ui.dialog.prompt).toHaveBeenCalledTimes(2); - expect(build).not.toHaveBeenCalled(); - }); - - it("runs /quota_announcements and /pricing_refresh without a prompt and forwards typed arguments", async () => { - const { context, command } = startTui(); - - for (const id of ["quota_announcements", "pricing_refresh"]) { - await command(id).run(""); - await command(id).run(); - await command(id).run(" extra "); - } + await command("quota_announcements").run(); + await command("pricing_refresh").run(); expect(context.ui.dialog.prompt).not.toHaveBeenCalled(); expect(build.mock.calls.map(([params]) => [params.command, params.arguments])).toEqual([ ["quota_announcements", undefined], - ["quota_announcements", undefined], - ["quota_announcements", "extra"], - ["pricing_refresh", undefined], ["pricing_refresh", undefined], - ["pricing_refresh", "extra"], ]); }); + it("opens the dialog when a quota report is posted to the session on screen", () => { + const { context, emit } = startTui(undefined, { type: "session", sessionID: "ses_open" }); + const metadata = { opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1 } }; + const item = (text: string, meta?: Record, type = "user") => ({ + type, + delivery: "steer", + payload: { text, ...(meta ? { metadata: meta } : {}) }, + }); + const report = formatQuotaReportMessage("openai 42%"); + + emit("session.inbox.enqueued", { sessionID: "ses_open", item: item("hello") }); + emit("session.inbox.enqueued", { + sessionID: "ses_open", + item: item("hello", { displayText: "hello", comments: [] }), + }); + emit("session.inbox.enqueued", { sessionID: "ses_open", item: item(report) }); + emit("session.inbox.enqueued", { + sessionID: "ses_open", + item: item(report, metadata, "synthetic"), + }); + emit("session.inbox.enqueued", { sessionID: "ses_other", item: item(report, metadata) }); + expect(context.ui.dialog.show).not.toHaveBeenCalled(); + + vi.stubGlobal("React", { + createElement: (type: unknown, props: Record | null) => ({ type, props }), + }); + emit("session.inbox.enqueued", { sessionID: "ses_open", item: item(report, metadata) }); + + expect(context.ui.dialog.show).toHaveBeenCalledOnce(); + expect(context.ui.dialog.set).toHaveBeenCalledWith({ size: "xlarge" }); + const render = context.ui.dialog.show.mock.calls[0][0] as () => { + props: Record; + }; + expect(render().props).toMatchObject({ title: "OpenCode Quota", message: "openai 42%" }); + expect(build).not.toHaveBeenCalled(); + }); + it("reads project config from the location's Git worktree root, like the server plugin", async () => { const repo = realpathSync(mkdtempSync(join(tmpdir(), "opencode-quota-tui-roots-"))); try { diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 0dec59fa..9cff4817 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -180,7 +180,7 @@ async function setupV2Surfaces(client: ReturnType, provider const emit = (event: string, sessionID: string) => { for (const callback of events.get(event) ?? []) callback({ data: { sessionID } }); }; - let commands: Array<{ slash: { name: string }; run: (input?: unknown) => Promise }> = []; + let commands: Array<{ id: string; run: () => Promise }> = []; // Records the title and scrollbox text of each quota output dialog the TUI shows. const dialog = vi.fn((_input: { title: string; message: string }) => {}); type Node = { type: string; props: Record }; @@ -254,9 +254,9 @@ async function setupV2Surfaces(client: ReturnType, provider }, } as never); expect(slots).toEqual(["app", "sidebar.content", "prompt.footer", "home.footer.status"]); - const quota = commands.find((command) => command.slash.name === "quota"); + const quota = commands.find((command) => command.id === "quota.quota"); expect(quota).toBeDefined(); - expect(commands.some((command) => command.slash.name === "quota_status")).toBe(true); + expect(commands.some((command) => command.id === "quota.quota_status")).toBe(true); const renderSurface = async ( append: "sidebar.content" | "prompt.footer" | "home.footer.status", output: string[], From c676a33d65a9374c5af91f8f2287e9651c47e9b3 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:41:06 +0200 Subject: [PATCH 49/97] feat(tui): restore tuiCommandDisplay with the popup as default In the TUI, a typed /quota showed both the popup and the report in the chat. Restore the v4 option tuiCommandDisplay ("dialog" | "inline") with its config parsing, issue reporting, and init wizard question, now defaulting to "dialog". - "dialog": when a tagged report reaches the inbox of the session on screen, the TUI opens the popup and cancels the pending inbox item through context.client.session.inbox.cancel, the same request the TUI sends to delete a pending prompt. No chat message remains. - "inline": the report stays in the chat and no popup opens. Web and Desktop keep showing the report in the chat. The TUI cannot tell which client typed the command, so in dialog mode it also removes a Web report for a session it shows at the same time (documented). --- README.md | 4 +- docs/readme/configuration.md | 6 + docs/readme/manual-install.md | 2 +- src/lib/config.ts | 20 +++ src/lib/init-installer.ts | 49 ++++++- src/lib/types.ts | 9 ++ src/plugin.ts | 5 +- src/tui-v2.tsx | 34 ++++- tests/lib.config.test.ts | 29 ++++ tests/lib.init-installer.test.ts | 55 +++++++- tests/provider-surfaces.opencode-go.test.ts | 1 + tests/tui-v2-commands.test.ts | 139 ++++++++++++++++---- tests/v4-phase5-cross-surface.test.ts | 2 + 13 files changed, 314 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3b6ab2f7..1a9caba9 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ The updater prints the complete preview before its own config or cache changes. - OpenCode 1 is no longer supported. Stay on `4.10.3` if you still use OpenCode 1, or follow [Moving to OpenCode 2](docs/readme/updating.md#moving-to-opencode-2). - Logins are read only from OpenCode 2's database (`opencode.db`), never from `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. - Web and Desktop have the quota slash commands but no toasts or panels, because OpenCode 2 gives plugins no Web UI hooks. On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. -- One `"plugin"` entry in `opencode.json` now loads both the server and the TUI; no `tui.json` entry is needed. TUI slash commands post the report in the chat and open a dialog; the command palette opens only the dialog. -- `tuiCommandDisplay` and `tuiCompactStatus.suppressWhenNativeProviderQuota` were removed. Old keys are ignored. +- One `"plugin"` entry in `opencode.json` now loads both the server and the TUI; no `tui.json` entry is needed. TUI slash commands and the command palette open the report in a popup and leave no chat message. +- `tuiCommandDisplay` now defaults to `"dialog"`; set `"inline"` to keep TUI slash reports in the chat instead. `tuiCompactStatus.suppressWhenNativeProviderQuota` was removed; the old key is ignored. ## Choose your setup diff --git a/docs/readme/configuration.md b/docs/readme/configuration.md index 2dbccfbc..174e709d 100644 --- a/docs/readme/configuration.md +++ b/docs/readme/configuration.md @@ -28,6 +28,8 @@ Strict `.json` files also work. Run `/quota_status` if you are unsure which file | Show percentages without `left` or `used` | `percentLabelStyle: "bare"` | | Use dense reset countdown units | `resetTimeSpaced: false` | | Show supplementary accounting facts | `accountingDetail: "detailed"` | +| Keep TUI slash results in the chat | `tuiCommandDisplay: "inline"` | +| Show TUI slash results only in a popup | `tuiCommandDisplay: "dialog"` | | Turn the TUI sidebar on or off | `tuiSidebarPanel.enabled` | | Prefer an OpenCode Go collapsed row | `tuiSidebarPanel.opencodeGoPreferredWindow` | | Turn popup quota notifications on or off | `enableToast` | @@ -56,6 +58,9 @@ The installer chooses `allWindows` by default. If the setting is absent, the bui "resetTimeSpaced": false, "accountingDetail": "summary", + // Keep TUI slash-command results in the chat instead of the default popup. + "tuiCommandDisplay": "inline", + // Show the sidebar and prefer OpenCode Go's Five-hour row while collapsed. "tuiSidebarPanel": { "enabled": true, @@ -421,6 +426,7 @@ Existing `experimental.quotaToast` settings remain supported. | Option | Default | Meaning | | -------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tuiCommandDisplay` | `"dialog"` | Choose where TUI slash command reports (`/quota`, `/tokens_*`, ...) appear. `dialog` opens a scrollable popup and leaves no message in the chat; `inline` keeps the report in the chat as your message, with no popup. Web and Desktop always show the report in the chat. If a session is open in the TUI and in Web at the same time, `dialog` also removes a Web `/quota` report from the chat while the TUI shows that session. | | `tuiSidebarPanel.enabled` | `true` | Show the Sidebar `Quota` panel when the TUI plugin is installed. Click the panel header to toggle its collapsed/expanded window layout; OpenCode remembers the last state. This is not an `accountingDetail` override. | | `tuiSidebarPanel.formatStyle` | (root `formatStyle`) | Override `formatStyle` for the Sidebar panel only. Useful when you want `allWindows` detail in the sidebar but a different style elsewhere. | | `tuiSidebarPanel.opencodeGoPreferredWindow` | unset | Prefer `rolling` (Five-hour), `weekly`, or `monthly` for OpenCode Go only while the sidebar is collapsed. If unset or unavailable, the lowest-remaining window is used. Expanded rows still follow `opencodeGoWindows`. | diff --git a/docs/readme/manual-install.md b/docs/readme/manual-install.md index a7f757d2..e90a7e71 100644 --- a/docs/readme/manual-install.md +++ b/docs/readme/manual-install.md @@ -82,7 +82,7 @@ Put these settings in `quota-toast.jsonc`. | Quota bar under the prompt | `tuiPromptBar.enabled: true` | | Manual slash commands only | Disable sidebar, toast, and compact line | -TUI slash commands post the report in the chat and open a dialog. Web and Desktop show none of the settings above. +TUI slash commands open the report in a popup; set `tuiCommandDisplay: "inline"` to keep it in the chat instead. Web and Desktop show none of the settings above. See [Configuration](configuration.md) for more examples and every setting. diff --git a/src/lib/config.ts b/src/lib/config.ts index 85bc2c2d..1c40285e 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -26,6 +26,7 @@ import type { QuotaResetWindow, QuotaToastConfig, SessionTokenScope, + TuiCommandDisplay, } from "./types.js"; import { DEFAULT_CONFIG } from "./types.js"; @@ -40,6 +41,7 @@ export const QUOTA_TOAST_SETTING_SOURCE_KEYS = [ "enableToast", "resetNotifications.enabled", "resetNotifications.windows", + "tuiCommandDisplay", "formatStyle", "percentDisplayMode", "quotaProjection", @@ -152,6 +154,7 @@ type ValidatedQuotaToastPatch = { enabled?: boolean; enableToast?: boolean; resetNotifications?: QuotaResetNotificationsPatch; + tuiCommandDisplay?: TuiCommandDisplay; formatStyle?: QuotaToastConfig["formatStyle"]; percentDisplayMode?: PercentDisplayMode; quotaProjection?: "runway"; @@ -245,6 +248,10 @@ function isValidAccountingDetail(value: unknown): value is QuotaToastConfig["acc return value === "summary" || value === "detailed"; } +function isValidTuiCommandDisplay(value: unknown): value is TuiCommandDisplay { + return value === "inline" || value === "dialog"; +} + function isValidSessionTokenScope(value: unknown): value is SessionTokenScope { return value === "current" || value === "tree"; } @@ -632,6 +639,14 @@ function extractValidatedQuotaToastPatch( if (resetNotifications) patch.resetNotifications = resetNotifications; } + if (hasOwnKey(quotaToastConfig, "tuiCommandDisplay")) { + if (isValidTuiCommandDisplay(quotaToastConfig.tuiCommandDisplay)) { + patch.tuiCommandDisplay = quotaToastConfig.tuiCommandDisplay; + } else { + reportIssue?.("tuiCommandDisplay", 'expected "inline" or "dialog"'); + } + } + const formatStyle = getConfiguredFormatStyle(quotaToastConfig as Partial); if (formatStyle) { patch.formatStyle = formatStyle; @@ -914,6 +929,11 @@ function applyValidatedQuotaToastPatch( } } + if (hasOwnKey(patch, "tuiCommandDisplay")) { + config.tuiCommandDisplay = patch.tuiCommandDisplay!; + applySettingSource(settingSources, "tuiCommandDisplay", sourcePath); + } + if (hasOwnKey(patch, "formatStyle")) { config.formatStyle = patch.formatStyle!; applySettingSource(settingSources, "formatStyle", sourcePath); diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index ffd44a84..aadbf9c0 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -37,12 +37,14 @@ import { resolveQuotaFormatStyle, } from "./quota-format-style.js"; import { QUOTA_PROVIDERS_AGGREGATE_ID } from "./quota-providers.js"; -import type { QuotaToastConfig, SessionTokenScope } from "./types.js"; +import type { QuotaToastConfig, SessionTokenScope, TuiCommandDisplay } from "./types.js"; const QUOTA_PLUGIN_SPEC = "@slkiser/opencode-quota@latest"; const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json"; const GITHUB_REPO_URL = "https://github.com/slkiser/opencode-quota"; const GITHUB_STAR_NOTE = `if this helps, stars are appreciated: ${GITHUB_REPO_URL}`; +const TUI_COMMAND_DISPLAY_COMMENT = + "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog."; export type InitInstallerInterface = "tui" | "web" | "both"; export type InitInstallerScope = "project" | "global"; @@ -60,6 +62,7 @@ export interface InitInstallerSelections { percentDisplayMode: QuotaToastConfig["percentDisplayMode"]; showSessionTokens: boolean; sessionTokenScope?: SessionTokenScope; + tuiCommandDisplay?: TuiCommandDisplay; maintainerAnnouncements?: boolean; configFormat?: ConfigFileFormat; } @@ -661,6 +664,10 @@ async function planOpencodeEdit(params: { target, desiredData: root, managedComments: [ + { + path: ["plugin"], + text: TUI_COMMAND_DISPLAY_COMMENT, + }, { path: ["plugin"], text: "// OpenCode Quota: loads the server plugin for slash commands and quota checks.", @@ -773,6 +780,15 @@ async function planQuotaConfigEdit(params: { "quotaToast.enableToast", edit, ); + if (params.selections.tuiCommandDisplay !== undefined) { + setInstallerOwnedSetting( + quotaToast, + "tuiCommandDisplay", + params.selections.tuiCommandDisplay, + "quotaToast.tuiCommandDisplay", + edit, + ); + } setInstallerOwnedSetting( quotaToast, "showSessionTokens", @@ -886,6 +902,9 @@ function buildPlanSummary(plan: InitInstallerPlan): string[] { if (plan.selections.interfaces !== "web") { lines.push(`TUI surfaces: ${getUiLabel(quotaUiIntent.choices)}`); + lines.push( + `Command display: ${plan.selections.tuiCommandDisplay === "inline" ? "Inline with messages" : "Popup dialog"}`, + ); } lines.push( @@ -1003,6 +1022,10 @@ export async function planInitInstaller(params: { ...requestedSelections, configFormat: params.selections.configFormat ?? "jsonc", quotaUi: quotaUiIntent.choices, + tuiCommandDisplay: + params.selections.interfaces === "web" + ? undefined + : (params.selections.tuiCommandDisplay ?? "dialog"), maintainerAnnouncements: params.selections.maintainerAnnouncements, manualProviders: params.selections.providerMode === "manual" @@ -1099,6 +1122,7 @@ type ExistingInstallerAnswers = { percentDisplayMode?: QuotaToastConfig["percentDisplayMode"]; showSessionTokens?: boolean; sessionTokenScope?: SessionTokenScope; + tuiCommandDisplay?: TuiCommandDisplay; maintainerAnnouncements?: boolean; }; @@ -1156,6 +1180,9 @@ async function readExistingInstallerAnswers(baseDir: string): Promise 1)) break; prompts.log.error("Manual commands only cannot be combined with automatic surfaces."); } + + tuiCommandDisplay = await prompts.select({ + message: "Where should slash commands (e.g. /quota) appear?", + initialValue: existing.tuiCommandDisplay ?? "dialog", + options: [ + { + label: "Inline with messages", + value: "inline", + hint: "persist output in the message transcript", + }, + { + label: "Popup dialog", + value: "dialog", + hint: "show output in a temporary TUI popup", + }, + ], + }); + if (prompts.isCancel(tuiCommandDisplay)) return null; } const providerMode = await prompts.select({ @@ -1376,6 +1422,7 @@ async function promptForSelections( percentDisplayMode: percentDisplayMode as QuotaToastConfig["percentDisplayMode"], showSessionTokens: showSessionTokens === "yes", sessionTokenScope: sessionTokenScope as SessionTokenScope, + tuiCommandDisplay: interfaces === "web" ? undefined : (tuiCommandDisplay as TuiCommandDisplay), maintainerAnnouncements: interfaces === "web" ? undefined : maintainerAnnouncements !== false, configFormat: configFormat as ConfigFileFormat, }; diff --git a/src/lib/types.ts b/src/lib/types.ts index e24400ae..369719c2 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -80,6 +80,8 @@ export interface MaintainerAnnouncementsConfig { home: boolean; } +export type TuiCommandDisplay = "inline" | "dialog"; + /** Request timeout in milliseconds */ export const REQUEST_TIMEOUT_MS = 5000; @@ -93,6 +95,12 @@ export interface QuotaToastConfig { /** Opt-in, persisted notifications when selected quota windows reset. */ resetNotifications: QuotaResetNotificationsConfig; + /** + * Where TUI quota slash command reports appear: "dialog" shows only the popup, + * "inline" keeps the report in the chat. Web and Desktop always show it in the chat. + */ + tuiCommandDisplay: TuiCommandDisplay; + /** * Shared quota-row formatting style for popup toasts and the TUI sidebar. * @@ -222,6 +230,7 @@ export const DEFAULT_CONFIG: QuotaToastConfig = { enabled: false, windows: ["weekly"], }, + tuiCommandDisplay: "dialog", formatStyle: DEFAULT_QUOTA_FORMAT_STYLE, percentDisplayMode: "remaining", accountingDetail: "summary", diff --git a/src/plugin.ts b/src/plugin.ts index cf619b56..b7c89f28 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -107,8 +107,9 @@ export const QuotaToastPlugin = Plugin.define({ }); }); - // Web, Desktop, and the TUI list these in their "/" menu. The TUI opens its dialog when - // a report arrives; its command palette runs the same reports without posting them. + // Web, Desktop, and the TUI list these in their "/" menu. When a report arrives, the TUI + // follows tuiCommandDisplay: "dialog" opens its dialog and cancels the posted report, + // "inline" leaves it in the chat. Its command palette runs the same reports without posting them. await ctx.command.transform((editor) => { for (const spec of QUOTA_DIALOG_COMMANDS) { editor.add({ diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 75c6d768..4ad3dd37 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -5,6 +5,7 @@ import { RGBA, type ScrollBoxRenderable, TextAttributes } from "@opentui/core"; import { type JSX, useTerminalDimensions } from "@opentui/solid"; import { createSignal, onCleanup, Show } from "solid-js"; +import { loadConfig } from "./lib/config.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { formatQuotaRows } from "./lib/format.js"; @@ -77,6 +78,11 @@ type DialogTheme = { }; type TuiContext = { location?: { directory: string }; + client: { + session: { + inbox: { cancel: (input: { sessionID: string; inboxID: string }) => Promise }; + }; + }; theme: { surface: (name: "dialog") => DialogTheme }; data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void; @@ -691,20 +697,36 @@ async function runQuotaCommand( } /** - * Typed quota slash commands run on the server, which posts the report into the chat. - * When a report arrives for the session on screen, open it in the dialog too. + * Typed quota slash commands run on the server, which posts the report into the chat as a + * pending inbox item. When a report arrives for the session on screen, tuiCommandDisplay + * decides what the TUI shows: "dialog" opens the report in the dialog and cancels the pending + * item, so no chat message remains; "inline" leaves the report in the chat. + * The TUI cannot tell which client typed the command, so in "dialog" mode it also removes a + * report that Web or Desktop requested for the same session while the TUI shows it. */ -function showPostedQuotaReport(context: TuiContext, event: TuiEvent): void { +async function showPostedQuotaReport(context: TuiContext, event: TuiEvent): Promise { const sessionID = getSessionID(event); if (!sessionID || sessionID !== getRouteSessionID(context)) return; + const inboxID = event.data?.inboxID; const item = event.data?.item as | { type?: string; payload?: { text?: unknown; metadata?: Record } } | undefined; + if (typeof inboxID !== "string") return; if (item?.type !== "user" || typeof item.payload?.text !== "string") return; const report = readQuotaReportMetadata(item.payload.metadata); if (!report) return; const spec = QUOTA_DIALOG_COMMANDS.find((candidate) => candidate.id === report.command); if (!spec) return; + const config = await loadConfig(quotaClient(context), undefined, { + configRootDir: quotaRoots(context).configRoot, + }); + if (config.tuiCommandDisplay !== "dialog") return; + // Cancelling deletes the pending item before it becomes a stored message, the same + // request the TUI sends when the user deletes a pending prompt. + void context.client.session.inbox.cancel({ sessionID, inboxID }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + console.warn(`[opencode-quota] failed to remove the quota report from the chat: ${message}`); + }); void showQuotaOutputDialog(context, { title: report.title, message: readQuotaReport(item.payload.text), @@ -839,9 +861,9 @@ const plugin = Plugin.define({ const id = event.data?.id; if (typeof id === "string") questionToolCalls.delete(id); }); - const onInboxEnqueued = api.data.on("session.inbox.enqueued", (event) => - showPostedQuotaReport(api, event), - ); + const onInboxEnqueued = api.data.on("session.inbox.enqueued", (event) => { + void showPostedQuotaReport(api, event).catch(reportFailure); + }); disposeEvents = () => { onExecutionSucceeded(); onCompacted(); diff --git a/tests/lib.config.test.ts b/tests/lib.config.test.ts index 4fe9fbe7..984bdadf 100644 --- a/tests/lib.config.test.ts +++ b/tests/lib.config.test.ts @@ -63,6 +63,35 @@ describe("loadConfig", () => { return { config, meta }; } + it("defaults and validates native TUI command display with provenance", async () => { + const defaults = await loadSdkConfig({}); + expect(defaults.config.tuiCommandDisplay).toBe("dialog"); + expect(defaults.meta.settingSources).toEqual({}); + + const inline = await loadSdkConfig({ tuiCommandDisplay: "inline" }); + expect(inline.config.tuiCommandDisplay).toBe("inline"); + expect(inline.meta.settingSources).toEqual({ + tuiCommandDisplay: "client.config.get", + }); + expect(inline.meta.configIssues).toEqual([]); + + const invalid = await loadSdkConfig({ tuiCommandDisplay: "both" }); + expect(invalid.config.tuiCommandDisplay).toBe("dialog"); + expect(invalid.meta.settingSources).toEqual({}); + expect(invalid.meta.configIssues).toEqual([ + { + path: "client.config.get", + key: "tuiCommandDisplay", + message: 'expected "inline" or "dialog"', + }, + ]); + + const removedKey = await loadSdkConfig({ tuiQuotaCommandDisplay: "inline" }); + expect(removedKey.config.tuiCommandDisplay).toBe("dialog"); + expect(removedKey.meta.settingSources).toEqual({}); + expect(removedKey.meta.configIssues).toEqual([]); + }); + it("loads accounting detail with provenance and diagnoses the removed Zen key without translating it", async () => { const defaults = await loadSdkConfig({}); expect(defaults.config.accountingDetail).toBe("summary"); diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index e04970a7..d0788301 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -36,6 +36,7 @@ const DEFAULT_PROMPT_SELECT_VALUES = [ "tui", "project", "jsonc", + "inline", "auto", "singleWindow", "remaining", @@ -163,10 +164,11 @@ describe("init installer planning and merge behavior", () => { percentDisplayMode: "used", showSessionTokens: false, sessionTokenScope: "tree", + tuiCommandDisplay: "dialog", }); expect(quotaConfig).not.toHaveProperty("tuiQuotaCommandDisplay"); expect(readFileSync(join(projectDir, "opencode.jsonc"), "utf8")).toContain( - "// OpenCode Quota: loads the server plugin for slash commands and quota checks.", + "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog.", ); }); @@ -186,13 +188,14 @@ describe("init installer planning and merge behavior", () => { expect(() => JSON.parse(raw)).not.toThrow(); expect(raw).not.toMatch(/^\s*\/\//m); const quotaRaw = readFileSync(join(projectDir, "opencode-quota", "quota-toast.json"), "utf8"); + expect(JSON.parse(quotaRaw).tuiCommandDisplay).toBe("dialog"); expect(JSON.parse(quotaRaw).sessionTokenScope).toBe("current"); expect(plan.summaryLines).toContain("Session token scope: Current session"); expect(quotaRaw).not.toContain("//"); }); - it("writes explanatory comments to generated JSONC configs", async () => { - const projectDir = join(tempDir, "comments-jsonc"); + it("writes Inline selection and explanatory comments only to generated JSONC host configs", async () => { + const projectDir = join(tempDir, "inline-jsonc"); mkdirSync(projectDir, { recursive: true }); const plan = await planInitInstaller({ @@ -200,18 +203,21 @@ describe("init installer planning and merge behavior", () => { selections: installerSelections({ configFormat: "jsonc", quotaUi: ["sidebar"], + tuiCommandDisplay: "inline", }), }); await applyInitInstallerPlan(plan); + expect(plan.summaryLines).toContain("Command display: Inline with messages"); const quotaPath = join(projectDir, "opencode-quota", "quota-toast.jsonc"); + expect(readJson(quotaPath).tuiCommandDisplay).toBe("inline"); expect(readFileSync(quotaPath, "utf8")).toContain( "// Quota presentation and reset-period choices.", ); const hostRaw = readFileSync(join(projectDir, "opencode.jsonc"), "utf8"); expect(hostRaw).toContain( - "// OpenCode Quota: loads the server plugin for slash commands and quota checks.", + "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog.", ); expect(() => parseJsonOrJsonc(hostRaw, true)).not.toThrow(); expect(existsSync(join(projectDir, "tui.jsonc"))).toBe(false); @@ -812,6 +818,25 @@ describe("init installer planning and merge behavior", () => { expect.objectContaining({ label: "Compact status line (TUI)", value: "compact_status" }), expect.objectContaining({ label: "Manual commands only", value: "none" }), ]); + expect( + prompts.selectCalls.find( + (call) => call.message === "Where should slash commands (e.g. /quota) appear?", + ), + ).toMatchObject({ + initialValue: "dialog", + options: [ + { + label: "Inline with messages", + value: "inline", + hint: "persist output in the message transcript", + }, + { + label: "Popup dialog", + value: "dialog", + hint: "show output in a temporary TUI popup", + }, + ], + }); expect( prompts.selectCalls.find( (call) => call.message === "How should pre-configured providers be selected?", @@ -892,13 +917,24 @@ describe("init installer planning and merge behavior", () => { "percentDisplayMode": "remaining", "showSessionTokens": false, "sessionTokenScope": "tree", + "tuiCommandDisplay": "inline", "tuiSidebarPanel": { "enabled": true }, "maintainerAnnouncements": { "enabled": true } }`, "utf8", ); const prompts = createPromptStub({ - selectValues: ["tui", "project", "jsonc", "manual", "allWindows", "used", "yes", "tree"], + selectValues: [ + "tui", + "project", + "jsonc", + "dialog", + "manual", + "allWindows", + "used", + "yes", + "tree", + ], multiselectValues: [["toast"], ["anthropic"]], confirmValues: [false, false], }); @@ -908,6 +944,11 @@ describe("init installer planning and merge behavior", () => { expect(code).toBe(0); expect(prompts.multiselectCalls[0]).toMatchObject({ initialValues: ["sidebar"] }); expect(prompts.multiselectCalls[1]).toMatchObject({ initialValues: ["openai"] }); + expect( + prompts.selectCalls.find( + (call) => call.message === "Where should slash commands (e.g. /quota) appear?", + ), + ).toMatchObject({ initialValue: "inline" }); expect( prompts.selectCalls.find( (call) => call.message === "How should pre-configured providers be selected?", @@ -1127,6 +1168,7 @@ describe("init installer planning and merge behavior", () => { expect(plan.summaryLines).toContain("Interface: Web"); expect(plan.summaryLines.some((line) => line.startsWith("TUI surfaces:"))).toBe(false); + expect(plan.summaryLines.some((line) => line.startsWith("Command display:"))).toBe(false); expect(plan.edits.map((edit) => edit.kind)).toEqual(["opencode", "quota"]); await applyInitInstallerPlan(plan); @@ -1137,6 +1179,7 @@ describe("init installer planning and merge behavior", () => { ]); const quotaConfig = readJson(join(projectDir, "opencode-quota", "quota-toast.jsonc")); expect(quotaConfig.enableToast).toBe(false); + expect(quotaConfig.tuiCommandDisplay).toBeUndefined(); }); it("preserves unrelated JSONC comments and trailing commas during an in-place rerun edit", async () => { @@ -1152,6 +1195,7 @@ describe("init installer planning and merge behavior", () => { "formatStyle": "singleWindow", "percentDisplayMode": "remaining", "showSessionTokens": false, + "tuiCommandDisplay": "inline", "tuiSidebarPanel": { "enabled": true }, // keep unrelated section comment "unrelated": { @@ -1169,6 +1213,7 @@ describe("init installer planning and merge behavior", () => { manualProviders: ["anthropic"], formatStyle: "allWindows", percentDisplayMode: "used", + tuiCommandDisplay: "dialog", maintainerAnnouncements: false, }), }); diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 3ec13722..b5f41097 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -68,6 +68,7 @@ function createConfig() { showSessionTokens: false, telemetry: { enabled: false }, maintainerAnnouncements: { enabled: false, home: false }, + tuiCommandDisplay: "dialog", tuiSidebarPanel: { enabled: true, defaultExpanded: true, diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index d66d7079..98115776 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs"; +import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -11,6 +11,12 @@ vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ vi.mock("@opentui/solid", () => ({ useTerminalDimensions: () => () => ({ width: 120, height: 40 }), })); +const loadConfig = vi.hoisted(() => vi.fn()); +vi.mock("../src/lib/config.js", async (importOriginal) => { + const original = await importOriginal(); + loadConfig.mockImplementation(original.loadConfig); + return { ...original, loadConfig }; +}); import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; import plugin from "../src/tui-v2.tsx"; @@ -47,6 +53,7 @@ function startTui( const listeners = new Map(); const context = { location, + client: { session: { inbox: { cancel: vi.fn().mockResolvedValue(undefined) } } }, theme: { surface: vi.fn(() => ({ text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, @@ -169,41 +176,125 @@ describe("V2 quota TUI commands", () => { ]); }); - it("opens the dialog when a quota report is posted to the session on screen", () => { - const { context, emit } = startTui(undefined, { type: "session", sessionID: "ses_open" }); + describe("quota reports posted by slash commands", () => { const metadata = { opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1 } }; + const report = formatQuotaReportMessage("openai 42%"); const item = (text: string, meta?: Record, type = "user") => ({ type, delivery: "steer", payload: { text, ...(meta ? { metadata: meta } : {}) }, }); - const report = formatQuotaReportMessage("openai 42%"); + let projectDir: string; - emit("session.inbox.enqueued", { sessionID: "ses_open", item: item("hello") }); - emit("session.inbox.enqueued", { - sessionID: "ses_open", - item: item("hello", { displayText: "hello", comments: [] }), + beforeEach(() => { + projectDir = realpathSync(mkdtempSync(join(tmpdir(), "opencode-quota-tui-report-"))); + mkdirSync(join(projectDir, "global")); + mkdirSync(join(projectDir, "opencode-quota")); + vi.stubEnv("OPENCODE_CONFIG_DIR", join(projectDir, "global")); + vi.stubGlobal("React", { + createElement: (type: unknown, props: Record | null) => ({ type, props }), + }); + return () => rmSync(projectDir, { recursive: true, force: true }); }); - emit("session.inbox.enqueued", { sessionID: "ses_open", item: item(report) }); - emit("session.inbox.enqueued", { - sessionID: "ses_open", - item: item(report, metadata, "synthetic"), + + function writeCommandDisplay(value: string) { + writeFileSync( + join(projectDir, "opencode-quota", "quota-toast.json"), + JSON.stringify({ tuiCommandDisplay: value }), + ); + } + + /** Waits for the listener to read tuiCommandDisplay and act on it. */ + async function settle() { + await loadConfig.mock.results.at(-1)?.value; + await new Promise((resolve) => setImmediate(resolve)); + } + + it("ignores untagged items, other item types, and sessions not on screen", async () => { + const { context, emit } = startTui( + { directory: projectDir }, + { type: "session", sessionID: "ses_open" }, + ); + + emit("session.inbox.enqueued", { + sessionID: "ses_open", + inboxID: "msg_1", + item: item("hello"), + }); + emit("session.inbox.enqueued", { + sessionID: "ses_open", + inboxID: "msg_2", + item: item("hello", { displayText: "hello", comments: [] }), + }); + emit("session.inbox.enqueued", { + sessionID: "ses_open", + inboxID: "msg_3", + item: item(report), + }); + emit("session.inbox.enqueued", { + sessionID: "ses_open", + inboxID: "msg_4", + item: item(report, metadata, "synthetic"), + }); + emit("session.inbox.enqueued", { + sessionID: "ses_other", + inboxID: "msg_5", + item: item(report, metadata), + }); + await settle(); + + expect(loadConfig).not.toHaveBeenCalled(); + expect(context.ui.dialog.show).not.toHaveBeenCalled(); + expect(context.client.session.inbox.cancel).not.toHaveBeenCalled(); }); - emit("session.inbox.enqueued", { sessionID: "ses_other", item: item(report, metadata) }); - expect(context.ui.dialog.show).not.toHaveBeenCalled(); - vi.stubGlobal("React", { - createElement: (type: unknown, props: Record | null) => ({ type, props }), + it("dialog mode (the default) opens the report and removes it from the chat", async () => { + const { context, emit } = startTui( + { directory: projectDir }, + { type: "session", sessionID: "ses_open" }, + ); + + emit("session.inbox.enqueued", { + sessionID: "ses_open", + inboxID: "msg_report", + item: item(report, metadata), + }); + await settle(); + + expect(context.client.session.inbox.cancel).toHaveBeenCalledExactlyOnceWith({ + sessionID: "ses_open", + inboxID: "msg_report", + }); + expect(context.ui.dialog.show).toHaveBeenCalledOnce(); + expect(context.ui.dialog.set).toHaveBeenCalledWith({ size: "xlarge" }); + const render = context.ui.dialog.show.mock.calls[0][0] as () => { + props: Record; + }; + expect(render().props).toMatchObject({ title: "OpenCode Quota", message: "openai 42%" }); + expect(build).not.toHaveBeenCalled(); }); - emit("session.inbox.enqueued", { sessionID: "ses_open", item: item(report, metadata) }); - expect(context.ui.dialog.show).toHaveBeenCalledOnce(); - expect(context.ui.dialog.set).toHaveBeenCalledWith({ size: "xlarge" }); - const render = context.ui.dialog.show.mock.calls[0][0] as () => { - props: Record; - }; - expect(render().props).toMatchObject({ title: "OpenCode Quota", message: "openai 42%" }); - expect(build).not.toHaveBeenCalled(); + it("inline mode leaves the report in the chat and opens no dialog", async () => { + writeCommandDisplay("inline"); + const { context, emit } = startTui( + { directory: projectDir }, + { type: "session", sessionID: "ses_open" }, + ); + + emit("session.inbox.enqueued", { + sessionID: "ses_open", + inboxID: "msg_report", + item: item(report, metadata), + }); + await settle(); + + expect(loadConfig).toHaveBeenCalledOnce(); + await expect(loadConfig.mock.results[0].value).resolves.toMatchObject({ + tuiCommandDisplay: "inline", + }); + expect(context.ui.dialog.show).not.toHaveBeenCalled(); + expect(context.client.session.inbox.cancel).not.toHaveBeenCalled(); + }); }); it("reads project config from the location's Git worktree root, like the server plugin", async () => { diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 9cff4817..e1bf3d65 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -310,6 +310,7 @@ function configFor(formatStyle: "allWindows" | "singleWindow") { telemetry: { enabled: true, }, + tuiCommandDisplay: "dialog", tuiSidebarPanel: { enabled: true, defaultExpanded: false, @@ -342,6 +343,7 @@ function configForSingleProvider(providerId = "minimax-coding-plan") { telemetry: { enabled: false, }, + tuiCommandDisplay: "dialog", tuiSidebarPanel: { enabled: true, defaultExpanded: false, From 5c2e47ce1f981d94935108b8b71310e7b02f87a9 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:06:42 +0200 Subject: [PATCH 50/97] feat: ask users to upvote the OpenCode ecosystem listing --- README.md | 2 + src/lib/maintainer-announcements.ts | 7 ++++ tests/lib.maintainer-announcements.test.ts | 48 +++++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a9caba9..abc65b14 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,8 @@ Thanks to everyone who has contributed to OpenCode Quota. +Help upstream: a 👍 on [opencode#38281](https://github.com/anomalyco/opencode/issues/38281) (ecosystem listing) and [#43132](https://github.com/anomalyco/opencode/issues/43132) (Web/Desktop plugin panels) helps. Please react; don't comment "+1". + ## License MIT diff --git a/src/lib/maintainer-announcements.ts b/src/lib/maintainer-announcements.ts index d544bbcd..d75a2edd 100644 --- a/src/lib/maintainer-announcements.ts +++ b/src/lib/maintainer-announcements.ts @@ -56,6 +56,13 @@ export const BUNDLED_MAINTAINER_ANNOUNCEMENTS: readonly MaintainerAnnouncement[] startsAt: "2026-09-25T00:00:00.000Z", endsAt: "2026-11-25T00:00:00.000Z", }, + { + id: "opencode-ecosystem-listing-thumbs-up", + message: "Help OpenCode list this plugin: add a thumbs-up to issue #38281.", + url: "https://github.com/anomalyco/opencode/issues/38281", + startsAt: "2026-09-26T00:00:00.000Z", + endsAt: "2026-10-26T00:00:00.000Z", + }, { id: "google-gemini-cli-org-only", message: diff --git a/tests/lib.maintainer-announcements.test.ts b/tests/lib.maintainer-announcements.test.ts index fb42b7c9..2e0144df 100644 --- a/tests/lib.maintainer-announcements.test.ts +++ b/tests/lib.maintainer-announcements.test.ts @@ -33,6 +33,13 @@ const openCode2FeedbackAnnouncement = { startsAt: "2026-09-25T00:00:00.000Z", endsAt: "2026-11-25T00:00:00.000Z", } satisfies MaintainerAnnouncement; +const ecosystemThumbsUpAnnouncement = { + id: "opencode-ecosystem-listing-thumbs-up", + message: "Help OpenCode list this plugin: add a thumbs-up to issue #38281.", + url: "https://github.com/anomalyco/opencode/issues/38281", + startsAt: "2026-09-26T00:00:00.000Z", + endsAt: "2026-10-26T00:00:00.000Z", +} satisfies MaintainerAnnouncement; const geminiAnnouncement = { id: "google-gemini-cli-org-only", message: @@ -175,6 +182,7 @@ describe("maintainer announcements", () => { expect(BUNDLED_MAINTAINER_ANNOUNCEMENTS).toEqual([ ecosystemAnnouncement, openCode2FeedbackAnnouncement, + ecosystemThumbsUpAnnouncement, geminiAnnouncement, ]); expect(active).toEqual([ @@ -223,14 +231,50 @@ describe("maintainer announcements", () => { expect(evaluateAt("2026-11-24T23:59:59.999Z", ["google-agy"])?.active).toBe(true); expect(evaluateAt("2026-11-25T00:00:00.000Z")?.reasons).toEqual(["ended"]); - expect(getActiveIds("2026-10-15T12:00:00.000Z", "auto")).toEqual(["opencode-2-feedback"]); - expect(getActiveIds("2026-10-15T12:00:00.000Z", ["google-gemini-cli"])).toEqual([ + expect(getActiveIds("2026-10-26T12:00:00.000Z", "auto")).toEqual(["opencode-2-feedback"]); + expect(getActiveIds("2026-10-26T12:00:00.000Z", ["google-gemini-cli"])).toEqual([ "opencode-2-feedback", "google-gemini-cli-org-only", ]); expect(getActiveIds("2026-11-25T00:00:00.000Z", "auto")).toEqual([]); }); + it("shows the ecosystem listing thumbs-up notice to everyone for one month beside the feedback notice", () => { + const evaluateAt = (iso: string, enabledProviders: string[] | "auto" = "auto") => + evaluateMaintainerAnnouncements({ nowMs: Date.parse(iso), enabledProviders }).find( + (item) => item.announcement.id === "opencode-ecosystem-listing-thumbs-up", + ); + const getActiveIds = (iso: string, enabledProviders: string[] | "auto") => + getActiveMaintainerAnnouncements({ nowMs: Date.parse(iso), enabledProviders }).map( + (item) => item.announcement.id, + ); + + expect(evaluateAt("2026-09-25T23:59:59.999Z")?.reasons).toEqual(["not_started"]); + expect(evaluateAt("2026-09-26T00:00:00.000Z")).toEqual({ + announcement: ecosystemThumbsUpAnnouncement, + active: true, + reasons: [], + }); + expect(evaluateAt("2026-10-25T23:59:59.999Z", ["google-agy"])?.active).toBe(true); + expect(evaluateAt("2026-10-26T00:00:00.000Z")?.reasons).toEqual(["ended"]); + + expect(getActiveIds("2026-10-15T12:00:00.000Z", "auto")).toEqual([ + "opencode-ecosystem-listing-thumbs-up", + "opencode-2-feedback", + ]); + expect(getActiveIds("2026-10-15T12:00:00.000Z", ["google-gemini-cli"])).toEqual([ + "opencode-ecosystem-listing-thumbs-up", + "opencode-2-feedback", + "google-gemini-cli-org-only", + ]); + expect( + getMaintainerAnnouncementsSummary({ + nowMs: Date.parse("2026-10-15T12:00:00.000Z"), + enabledProviders: "auto", + }).activeCount, + ).toBe(2); + }); + it("sorts active announcements before inactive, then by end date and id", () => { const evaluations = evaluateMaintainerAnnouncements({ nowMs: NOW_MS, From a766117b31ff68bf368647d04455650dd21f6547 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:20:37 +0200 Subject: [PATCH 51/97] docs(readme): tighten the v5 README --- README.md | 42 ++++++++++++++++++++--------------- docs/readme/configuration.md | 1 + docs/readme/manual-install.md | 2 ++ docs/readme/updating.md | 9 +++++++- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index abc65b14..f79347f1 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ License: MIT

+> [!TIP] +> Help upstream: a 👍 on [opencode#38281](https://github.com/anomalyco/opencode/issues/38281) (ecosystem listing) and [#43132](https://github.com/anomalyco/opencode/issues/43132) (Web/Desktop plugin panels) helps. Please react; don't comment "+1". + [![OpenCode Quota sidebar](https://shawnkiser.com/opencode-quota/opencode-quota-sidebar.webp)](https://github.com/slkiser/opencode-quota) --- @@ -28,7 +31,7 @@ npx @slkiser/opencode-quota init > [!IMPORTANT] > Requires OpenCode `2.0.16` or newer. Node.js `22.13+` is required for `npx @slkiser/opencode-quota ...` (on Node 23, `23.4+`). -Upgrading from v3? Read the [v4 migration guide](docs/readme/v4-migration.md). +Upgrading from 4.x? Read [what changed in 5.0](#breaking-changes-in-500). After installation: @@ -54,15 +57,18 @@ After installation: 4. Restart OpenCode. -The updater prints the complete preview before its own config or cache changes. `--yes` authorizes only the previewed safe config edits and manifest-verified cache cleanup; it never moves or deletes secrets. See [Updating safely](docs/readme/updating.md) for detailed behavior and manual credential steps. +The updater always shows a preview first. `--yes` applies only the safe edits it previewed, and it never moves or deletes secrets. See [Updating safely](docs/readme/updating.md). -**Breaking changes in 5.0.0:** +### Breaking changes in 5.0.0 -- OpenCode 1 is no longer supported. Stay on `4.10.3` if you still use OpenCode 1, or follow [Moving to OpenCode 2](docs/readme/updating.md#moving-to-opencode-2). -- Logins are read only from OpenCode 2's database (`opencode.db`), never from `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. -- Web and Desktop have the quota slash commands but no toasts or panels, because OpenCode 2 gives plugins no Web UI hooks. On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. -- One `"plugin"` entry in `opencode.json` now loads both the server and the TUI; no `tui.json` entry is needed. TUI slash commands and the command palette open the report in a popup and leave no chat message. -- `tuiCommandDisplay` now defaults to `"dialog"`; set `"inline"` to keep TUI slash reports in the chat instead. `tuiCompactStatus.suppressWhenNativeProviderQuota` was removed; the old key is ignored. +> [!WARNING] +> - OpenCode 1 is no longer supported. Stay on `4.10.3` if you still use it. +> - Logins come only from OpenCode 2's `opencode.db`, never `auth.json`. If a provider is missing, log in again. +> - Web and Desktop get the slash commands (report posts in the chat) but no toasts or panels. +> - One `"plugin"` entry in `opencode.json` loads the server and the TUI; no `tui.json` entry is needed. +> - TUI slash reports now open in a popup. Set `tuiCommandDisplay: "inline"` to keep them in the chat. +> +> Details: [Moving to OpenCode 2](docs/readme/updating.md#moving-to-opencode-2). ## Choose your setup @@ -95,13 +101,13 @@ The updater prints the complete preview before its own config or cache changes. More ways to use it: -- Check quota outside the TUI: run `npx @slkiser/opencode-quota show` in a terminal. In Web and Desktop, slash commands post the report in the chat, and the assistant can call the `quota_status` diagnostics tool. -- See every OpenCode login: each connection for a provider gets its own rows, and `*` marks the active one. -- Automate quota checks with JSON output for scripts, status bars, and CI. Optional OpenTelemetry metrics support monitoring tools. -- Customize the display with [`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings), OpenCode Go's preferred collapsed-sidebar window, spaced reset countdowns by default with a `resetTimeSpaced: false` dense opt-out, decimal reset precision, bare percent labels, and [`accountingDetail`](docs/readme/configuration.md#show-accounting-detail). -- Optionally estimate **Runs out ≈ 1h 50m** for supported fixed windows with [`quotaProjection: "runway"`](docs/readme/configuration.md#estimate-when-fixed-quota-runs-out). It is off by default and leaves JSON output unchanged. -- Choose current-session or descendant-tree token totals. Get reset popups for selected windows with [`resetNotifications`](docs/readme/configuration.md#notify-when-quota-becomes-available-again). -- Troubleshoot authentication, quota sources, pricing, and maintainer notices. +- **Terminal:** run `npx @slkiser/opencode-quota show`. In Web and Desktop, slash commands post the report in the chat, and the AI can call the `quota_status` tool. +- **Several logins:** each login for a provider gets its own rows; `*` marks the active one. +- **Scripts and CI:** JSON output and optional OpenTelemetry metrics. See [External integration](docs/readme/external-integration.md). +- **Display:** a quota bar under the prompt ([`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings)), OpenCode Go's collapsed-sidebar row ([`tuiSidebarPanel.opencodeGoPreferredWindow`](docs/readme/configuration.md#tui-settings)), reset countdown style ([`resetTimeSpaced`](docs/readme/configuration.md#common-changes), [`resetTimeDecimals`](docs/readme/configuration.md#common-changes)), bare `81%` labels ([`percentLabelStyle`](docs/readme/configuration.md#common-changes)), and extra accounting rows ([`accountingDetail`](docs/readme/configuration.md#show-accounting-detail)). +- **Runs-out estimate:** [`quotaProjection: "runway"`](docs/readme/configuration.md#estimate-when-fixed-quota-runs-out) shows **Runs out ≈ 1h 50m** for supported fixed windows. Off by default; JSON is unchanged. +- **Tokens and resets:** include subagent sessions with [`sessionTokenScope: "tree"`](docs/readme/configuration.md#include-subagent-session-tokens); get a popup when quota comes back with [`resetNotifications`](docs/readme/configuration.md#notify-when-quota-becomes-available-again). +- **Troubleshooting:** `/quota_status` checks logins, quota sources, pricing, and maintainer notices. See [Configuration](docs/readme/configuration.md) for UI options and [Manual install](docs/readme/manual-install.md) for setup details. @@ -109,7 +115,7 @@ See [Configuration](docs/readme/configuration.md) for UI options and [Manual ins ### Core slash commands -These run in the OpenCode TUI, Web, and Desktop. Each posts its report in the chat as your message, and the AI never answers it; the TUI also opens the report in a dialog. Type arguments after the command, such as `/tokens_between 2026-09-01 2026-09-25`. In the TUI command palette, the same commands open only the dialog and ask for missing dates. See [Web and Desktop notes](docs/readme/manual-install.md#web-and-desktop-notes). +Type these in OpenCode. Add arguments after the command, like `/tokens_between 2026-09-01 2026-09-25`. See [Web and Desktop notes](docs/readme/manual-install.md#web-and-desktop-notes). | Command | Use when | | --------------------------------------- | --------------------------------------------------------------- | @@ -252,6 +258,8 @@ Project guides: - [Providers](docs/readme/providers.md) - [Troubleshooting](docs/readme/troubleshooting.md) - [External integration](docs/readme/external-integration.md) +- [Updating safely](docs/readme/updating.md) +- [v4 migration guide](docs/readme/v4-migration.md) (upgrading from v3) External references: @@ -270,8 +278,6 @@ Thanks to everyone who has contributed to OpenCode Quota. -Help upstream: a 👍 on [opencode#38281](https://github.com/anomalyco/opencode/issues/38281) (ecosystem listing) and [#43132](https://github.com/anomalyco/opencode/issues/43132) (Web/Desktop plugin panels) helps. Please react; don't comment "+1". - ## License MIT diff --git a/docs/readme/configuration.md b/docs/readme/configuration.md index 174e709d..804a1af4 100644 --- a/docs/readme/configuration.md +++ b/docs/readme/configuration.md @@ -27,6 +27,7 @@ Strict `.json` files also work. Run `/quota_status` if you are unsure which file | Estimate when eligible fixed quota runs out | `quotaProjection: "runway"` | | Show percentages without `left` or `used` | `percentLabelStyle: "bare"` | | Use dense reset countdown units | `resetTimeSpaced: false` | +| Show reset countdowns as decimals (`5.7d`) | `resetTimeDecimals: 1` | | Show supplementary accounting facts | `accountingDetail: "detailed"` | | Keep TUI slash results in the chat | `tuiCommandDisplay: "inline"` | | Show TUI slash results only in a popup | `tuiCommandDisplay: "dialog"` | diff --git a/docs/readme/manual-install.md b/docs/readme/manual-install.md index e90a7e71..391d1c9e 100644 --- a/docs/readme/manual-install.md +++ b/docs/readme/manual-install.md @@ -88,6 +88,8 @@ See [Configuration](configuration.md) for more examples and every setting. ## Web and Desktop notes +- Slash commands work in the TUI, Web, and Desktop. Add arguments after the command, like `/tokens_between 2026-09-01 2026-09-25`. +- In the TUI, a slash command opens the report in a popup and leaves no chat message (the default `tuiCommandDisplay: "dialog"`). With `tuiCommandDisplay: "inline"`, the report stays in the chat instead. The TUI command palette runs the same commands, opens only the popup, and asks for missing dates. - On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. - Each report starts with `[OpenCode Quota report]` and ends with `[End of OpenCode Quota report]`. These lines keep the report out of compaction summaries too. - Web shows chat messages in a proportional font, so report columns may not line up. For aligned columns, use the TUI or run `npx @slkiser/opencode-quota show` in a terminal. diff --git a/docs/readme/updating.md b/docs/readme/updating.md index 3b9f575e..8f20485b 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -46,7 +46,14 @@ The updater builds one plan, prints it in full, and then either stops or applies 2. Run the update steps above. 3. Run `/quota_status` in the OpenCode TUI. If a provider you use is missing, log in to it again in OpenCode 2 (`/connect`, or `opencode auth login `). OpenCode Quota never reads `auth.json`. -The `opencode.json` plugin entry loads both the server and the TUI; no `tui.json` entry is needed. Web and Desktop have the quota slash commands, which post the report in the chat, but no toasts or panels on OpenCode 2; see [Web and Desktop notes](manual-install.md#web-and-desktop-notes). +What changed in `5.0.0`: + +- **OpenCode 1 is no longer supported.** Stay on `4.10.3` if you still use OpenCode 1. +- **Logins come only from `opencode.db`.** OpenCode Quota reads logins only from OpenCode 2's database, never from `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. +- **Web and Desktop have slash commands but no toasts or panels,** because OpenCode 2 gives plugins no Web UI hooks. On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. See [Web and Desktop notes](manual-install.md#web-and-desktop-notes). +- **One plugin entry.** One `"plugin"` entry in `opencode.json` loads both the server and the TUI; no `tui.json` entry is needed. +- **TUI reports open in a popup.** TUI slash commands and the command palette open the report in a popup and leave no chat message. `tuiCommandDisplay` now defaults to `"dialog"`; set `"inline"` to keep TUI slash reports in the chat instead. +- **Removed setting.** `tuiCompactStatus.suppressWhenNativeProviderQuota` was removed; the old key is ignored. ## Read the preview From 644efd56d4645505f02134837e389b03f37df4b8 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:28:03 +0200 Subject: [PATCH 52/97] fix(google): find Gemini CLI and AGY login plugins installed by OpenCode 2 OpenCode 2 installs npm plugins under /opencode/npm/@//node_modules/ (packages/util/src/npm.ts: directory = join(global.cache, "npm", "@"); `current` loads the highest numeric generation; generation numbers are install timestamps). The companion lookup only searched OpenCode 1's node_modules/ and packages/ folders, so Gemini CLI and Google AGY reported "companion missing" on OpenCode 2. The runtime-files stage now searches the OpenCode 2 layout first: the newest generation of each @ folder, most recent install first across specs (for example @latest and @alpha). The OpenCode 1 layouts stay. OpenCode 2 uses the same ~/.cache/opencode directory and never removes node_modules/ or packages/ there (only `opencode uninstall` deletes the cache). OpenCode 2 imports OpenCode 1 google-agy and google logins from auth.json once, and the companion OAuth client constants are identical across versions (AGY 1.2.11 and 1.2.11-alpha.0 ship the same AGY_CLIENT_ID), so upgraded users keep working quota from the companion files left by OpenCode 1 until they install the OpenCode 2 build. Checked the published tarballs: - opencode-gemini-auth 2.0.1: `gemini-cli` OAuth method on the `google` integration; value {type, methodID, refresh "token|project|managed", access, expires, metadata.email}; constants in dist/index.js. - @anthonyhaussman/opencode-agy-auth 1.2.11-alpha.0: `oauth` method on `google-agy`; value {type, methodID, refresh "token|project|managed", access, expires}; constants in dist/index.js. Both match the existing row matching and constant extraction, so neither changed. Tests cover the OpenCode 2 layout, the install pick, a missing package, and both credential rows read from the credential table. Docs: Gemini CLI needs opencode-gemini-auth 2.x and the Gemini CLI login method; Google AGY needs the @alpha OpenCode 2 build, and logins copied from OpenCode 1 keep working. --- docs/readme/providers.md | 6 +- src/lib/google-companion-credentials.ts | 60 +++++++++++++- tests/lib.google-companion.test.ts | 100 ++++++++++++++++++++++++ tests/lib.opencode-auth.test.ts | 62 +++++++++++++++ 4 files changed, 224 insertions(+), 4 deletions(-) diff --git a/docs/readme/providers.md b/docs/readme/providers.md index 63c39a89..c79fb0a8 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -443,7 +443,7 @@ OpenCode Quota's Google integrations use independent community companion plugins ### Google AGY -Use companion plugin [`@anthonyhaussman/opencode-agy-auth`](https://github.com/anthonyhaussman/opencode-agy-auth). Add it before `@slkiser/opencode-quota` in `opencode.json`, then authenticate Google once: +Use companion plugin [`@anthonyhaussman/opencode-agy-auth`](https://github.com/anthonyhaussman/opencode-agy-auth). OpenCode 2 needs its OpenCode 2 build, currently the alpha: `@anthonyhaussman/opencode-agy-auth@alpha`. Add it before `@slkiser/opencode-quota` in `opencode.json`, then authenticate Google once and choose **Antigravity CLI (OAuth)**: Google AGY reports the companion's grouped weekly and five-hour quota windows for each account. @@ -451,6 +451,8 @@ Google AGY reports the companion's grouped weekly and five-hour quota windows fo opencode auth login google-agy ``` +AGY logins copied from OpenCode 1 keep working. + If you use manual provider selection, include `google-agy` in `enabledProviders`. ```jsonc @@ -479,7 +481,7 @@ If the AGY auth entry does not include a project id, set `OPENCODE_AGY_PROJECT_I Gemini CLI works only with Gemini Code Assist Standard or Enterprise (organization) accounts. Google ended personal Gemini Code Assist accounts (individual, AI Pro, and AI Ultra) on 2026-06-18; see [Google's notice](https://developers.google.com/gemini-code-assist/docs/deprecations/code-assist-individuals). Personal Google users should use [Google AGY](#google-agy-quick-setup). -Use companion plugin [`opencode-gemini-auth`](https://github.com/jenslys/opencode-gemini-auth#readme). Add it before `@slkiser/opencode-quota` in `opencode.json`, then authenticate Google once: +Use companion plugin [`opencode-gemini-auth`](https://github.com/jenslys/opencode-gemini-auth#readme) 2.x, which supports OpenCode 2. Add it before `@slkiser/opencode-quota` in `opencode.json`, then authenticate Google once and choose **OAuth with Google (Gemini CLI)**: ```bash opencode auth login google diff --git a/src/lib/google-companion-credentials.ts b/src/lib/google-companion-credentials.ts index ee26f639..cd0130c8 100644 --- a/src/lib/google-companion-credentials.ts +++ b/src/lib/google-companion-credentials.ts @@ -1,4 +1,4 @@ -import { readdirSync } from "node:fs"; +import { type Dirent, readdirSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -133,9 +133,65 @@ function resolveSpecifier( } } +/** + * Package roots in OpenCode 2's plugin install layout: + * `/npm/@//node_modules/`. + * OpenCode 2 loads only the highest-numbered generation of a spec directory. + * Generation numbers are install timestamps, so across specs (for example + * `@latest` and `@alpha`) the most recent install comes first. + */ +function getOpencode2NpmPackageRoots( + cacheDir: string, + descriptor: GoogleCompanionDescriptor, +): string[] { + const [scope, name] = + descriptor.packageScan === "scoped" + ? descriptor.packageName.split("/") + : [undefined, descriptor.packageName]; + const specsDir = scope ? join(cacheDir, "npm", scope) : join(cacheDir, "npm"); + const installs: { generation: number; packageRoot: string }[] = []; + + let specEntries: Dirent[]; + try { + specEntries = readdirSync(specsDir, { withFileTypes: true }); + } catch { + // A missing OpenCode 2 npm directory is not an installed companion. + return []; + } + + for (const entry of specEntries) { + if (!entry.isDirectory() || !entry.name.startsWith(`${name}@`)) continue; + const specDir = join(specsDir, entry.name); + let generation: string | undefined; + try { + generation = readdirSync(specDir) + .filter((candidate) => /^\d+$/.test(candidate)) + .sort((left, right) => Number(left) - Number(right)) + .at(-1); + } catch { + continue; + } + if (!generation) continue; + installs.push({ + generation: Number(generation), + packageRoot: join(specDir, generation, "node_modules", descriptor.packageName), + }); + } + + return installs + .sort((left, right) => right.generation - left.generation) + .map((install) => install.packageRoot); +} + function getRuntimePackageRoots(descriptor: GoogleCompanionDescriptor): string[] { const { cacheDir } = getOpencodeRuntimeDirs(); - const packageRoots = [join(cacheDir, "node_modules", descriptor.packageName)]; + const packageRoots = [ + ...getOpencode2NpmPackageRoots(cacheDir, descriptor), + // OpenCode 1 layouts. OpenCode 2 shares this cache directory and never + // removes them, so companions installed before the upgrade still provide + // the OAuth client for logins that OpenCode 2 imported from auth.json. + join(cacheDir, "node_modules", descriptor.packageName), + ]; try { const packagesDir = join(cacheDir, "packages"); diff --git a/tests/lib.google-companion.test.ts b/tests/lib.google-companion.test.ts index 902054f2..75799c5c 100644 --- a/tests/lib.google-companion.test.ts +++ b/tests/lib.google-companion.test.ts @@ -355,6 +355,106 @@ describe("google companion credential resolution", () => { } }); + it("finds each companion in OpenCode 2's npm plugin install layout", async () => { + for (const companion of companions) { + const cacheDir = join(tempDir, `${companion.id}-opencode2-cache`); + const bundlePath = join( + cacheDir, + "npm", + `${companion.packageName}@latest`, + "1790000000000", + "node_modules", + companion.packageName, + "dist", + "index.js", + ); + writeCredentials(bundlePath, companion, "var"); + const loaded = await loadWith( + companion, + () => { + throw moduleNotFound(); + }, + cacheDir, + ); + await expect(loaded.inspect()).resolves.toEqual({ + state: "present", + importSpecifier: companion.sourceSpecifier, + resolvedPath: bundlePath, + }); + await expectConfigured(loaded, bundlePath); + } + }); + + it("picks the OpenCode 2 install that OpenCode 2 loads", async () => { + for (const companion of companions) { + const cacheDir = join(tempDir, `${companion.id}-opencode2-pick`); + const bundle = (spec: string, generation: string) => + join( + cacheDir, + "npm", + `${companion.packageName}@${spec}`, + generation, + "node_modules", + companion.packageName, + "dist", + "index.js", + ); + writeCredentials(bundle("latest", "999"), companion, "var", "-older-generation"); + writeCredentials(bundle("latest", "1000"), companion, "var", "-newest"); + writeCredentials(bundle("alpha", "998"), companion, "var", "-older-spec"); + mkdirSync(join(cacheDir, "npm", `${companion.packageName}@latest`, ".staging-2000-x"), { + recursive: true, + }); + writeCredentials( + join(cacheDir, "node_modules", companion.packageName, "dist", "index.js"), + companion, + "var", + "-opencode1", + ); + const loaded = await loadWith( + companion, + () => { + throw moduleNotFound(); + }, + cacheDir, + ); + await expectConfigured(loaded, bundle("latest", "1000"), "-newest"); + } + }); + + it("reports a missing companion when OpenCode 2 has not installed it", async () => { + for (const companion of companions) { + const cacheDir = join(tempDir, `${companion.id}-opencode2-missing`); + mkdirSync(join(cacheDir, "npm", `${companion.packageName}@latest`), { recursive: true }); + writeCredentials( + join( + cacheDir, + "npm", + "other-plugin@latest", + "1", + "node_modules", + "other-plugin", + "dist", + "index.js", + ), + companion, + "var", + ); + const loaded = await loadWith( + companion, + () => { + throw moduleNotFound(); + }, + cacheDir, + ); + await expect(loaded.inspect()).resolves.toEqual({ + state: "missing", + importSpecifier: companion.sourceSpecifier, + error: companion.missingError, + }); + } + }); + it("supports Gemini root-only bundles and runtime-path package resolution", async () => { const companion = companions[1]!; const cacheDir = join(tempDir, "gemini-runtime-cache"); diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 29277efa..b009c464 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -4,6 +4,8 @@ import { join } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { resolveAgyAccounts } from "../src/lib/google-agy.js"; +import { resolveGeminiCliAccounts } from "../src/lib/google-gemini-cli.js"; import { clearReadAuthFileCacheForTests, formatCredentialDisplayNames, @@ -264,3 +266,63 @@ describe("selectConnectionCredentialRows", () => { ]); }); }); + +describe("Google companion credentials written by OpenCode 2", () => { + it("parses the opencode-gemini-auth 2.x and AGY alpha OAuth rows", async () => { + const { databasePath } = await createCredentialDatabase(); + const database = new DatabaseSync(databasePath); + const insert = database.prepare( + "INSERT INTO credential VALUES (?, ?, ?, ?, NULL, NULL, 1, 5, 5)", + ); + // opencode-gemini-auth 2.0.1: `gemini-cli` method on OpenCode 2's `google` integration. + insert.run( + "gemini", + "google", + "user@example.com", + JSON.stringify({ + type: "oauth", + methodID: "gemini-cli", + refresh: "gemini-refresh|gemini-project|", + access: "gemini-access", + expires: 50, + metadata: { email: "user@example.com" }, + }), + ); + // @anthonyhaussman/opencode-agy-auth 1.2.11-alpha.0: `oauth` method on `google-agy`. + insert.run( + "agy", + "google-agy", + "OAuth", + JSON.stringify({ + type: "oauth", + methodID: "oauth", + refresh: "agy-refresh|agy-project|agy-managed-project", + access: "agy-access", + expires: 60, + }), + ); + database.close(); + vi.stubEnv("OPENCODE_DB", databasePath); + const auth = await readAuthFile(); + + expect(resolveGeminiCliAccounts(auth)).toEqual([ + { + sourceKey: "google", + refreshToken: "gemini-refresh", + projectId: "gemini-project", + email: "user@example.com", + accessToken: "gemini-access", + expiresAt: 50, + }, + ]); + expect(resolveAgyAccounts(auth)).toEqual([ + { + sourceKey: "google-agy", + refreshToken: "agy-refresh", + projectId: "agy-managed-project", + accessToken: "agy-access", + expiresAt: 60, + }, + ]); + }); +}); From de210c8b99b5a4e5dc0a44ff58d971b64004cd16 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:34:27 +0200 Subject: [PATCH 53/97] feat(cursor): support the cursor-opencode-provider plugin on OpenCode 2 Adopt cursor-opencode-provider (oakimov, npm 0.7.3) as the documented Cursor companion plugin for OpenCode 2. Detection: /quota_status now reports plugin_enabled for cursor-opencode-provider, its OpenCode 2 entries /plugin/opencode2 and /server, and version-pinned forms of each. OpenCode 2 resolves a bare package's ./server export, which is the same OpenCode 2 entry. The OpenCode 1 entries /plugin and /plugin/v2 are not matched. The configured-provider inference now matches the same package. Old companions removed from plugin matching: @playwo/opencode-cursor-oauth, opencode-cursor-oauth, opencode-cursor, cursor-acp, open-cursor and @rama_nigg/open-cursor. None has a build that loads on stable OpenCode 2 (2.0.4+): @playwo 0.4.3 targets @opencode-ai/plugin 1.x only; the OpenCode 2 setup in opencode-cursor-oauth 0.2.0 (built on beta-17793) and @rama_nigg/open-cursor 2.5.8 calls ctx.catalog.transform, and stable @opencode/plugin 2.0.4 and 2.0.18 no longer have ctx.catalog. The cursor-acp and open-cursor provider ids stay in usage accounting because migrated message history can still carry them. Auth: a Cursor API key credential is valid. OpenCode 2 stores it as { type: "key", key } under integration cursor, which the credential reader maps to { type: "api", key }. The CURSOR_API_KEY environment variable, which the plugin also accepts, is reported as present with source env:CURSOR_API_KEY. Docs: the Cursor setup and troubleshooting notes now give the cursor-opencode-provider/plugin/opencode2 plugin entry and the /connect -> Cursor login (browser login or API key). The upstream reference tracking still follows @playwo/opencode-cursor-oauth for Cursor model pricing coverage, so the test tying it to the runtime package name is removed. --- docs/readme/providers.md | 10 ++- docs/readme/troubleshooting.md | 4 +- src/lib/cursor-detection.ts | 55 ++++++------ src/lib/opencode-config-providers.ts | 11 +-- src/lib/types.ts | 5 +- tests/lib.cursor-detection.test.ts | 94 +++++++++++++++++---- tests/lib.opencode-config-providers.test.ts | 26 +++++- tests/providers.cursor.test.ts | 2 +- tests/upstream-plugin-specs.test.ts | 7 -- 9 files changed, 140 insertions(+), 74 deletions(-) diff --git a/docs/readme/providers.md b/docs/readme/providers.md index c79fb0a8..7ac63509 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -410,12 +410,16 @@ If that response includes Anthropic's model-scoped Fable weekly window, OpenCode ### Cursor -Use companion plugin [`@playwo/opencode-cursor-oauth`](https://github.com/PoolPirate/opencode-cursor#readme). Add it before `@slkiser/opencode-quota` in `opencode.json`, then authenticate once: +Use companion plugin [`cursor-opencode-provider`](https://github.com/oakimov/cursor-opencode-provider#readme). Add its OpenCode 2 entry before `@slkiser/opencode-quota` in `opencode.json`: -```bash -opencode auth login cursor +```jsonc +{ + "plugin": ["cursor-opencode-provider/plugin/opencode2", "@slkiser/opencode-quota"], +} ``` +Restart OpenCode, run `/connect`, choose **Cursor**, then sign in with **Cursor account (browser login)** or paste an **API key** from [cursor.com/settings](https://cursor.com/settings). A `CURSOR_API_KEY` environment variable also works. OpenCode 1 Cursor plugins such as `@playwo/opencode-cursor-oauth` and `@rama_nigg/open-cursor` do not run on OpenCode 2; replace them with this entry. + Cursor estimates the current local billing cycle from OpenCode history. With complete model coverage and a positive configured/preset allowance, it shows an **API budget** percentage with used, limit, and remaining USD facts. If any Cursor model is unknown, it shows only **Known API spend** plus a partial-data issue; it never presents that partial spend as total account spend or a percentage. Without an allowance it shows **API spend**. **Auto+Composer spend** is supplementary and appears in detailed output when space allows. Runs-out projection is available only when `cursorBillingCycleStartDay` explicitly anchors that fixed cycle. The ordinary local calendar-month fallback is not projection evidence. diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 1a933af2..a8126932 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -130,8 +130,8 @@ Run `/quota_status` and check the Cursor section. | Symptom | Fix | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| Cursor not detected | Put `@playwo/opencode-cursor-oauth` before `@slkiser/opencode-quota` in `opencode.json`. | -| Cursor auth missing | Run `opencode auth login cursor`. | +| Cursor not detected | Put `cursor-opencode-provider/plugin/opencode2` before `@slkiser/opencode-quota` in `opencode.json`. | +| Cursor auth missing | Run `/connect` → **Cursor** in OpenCode, or set `CURSOR_API_KEY`. | | Quota appears but no remaining percentage | Set `cursorPlan` or `cursorIncludedApiUsd` in `opencode-quota/quota-toast.json`. | | Billing cycle looks wrong | Set `cursorBillingCycleStartDay` in `opencode-quota/quota-toast.json` to your local billing anchor day. | | Unknown Cursor pricing | Run `/pricing_refresh`; if still unknown, check `/quota_status` for unknown model ids. | diff --git a/src/lib/cursor-detection.ts b/src/lib/cursor-detection.ts index 6083e4c6..2291bfd7 100644 --- a/src/lib/cursor-detection.ts +++ b/src/lib/cursor-detection.ts @@ -3,12 +3,11 @@ import { readFile } from "fs/promises"; import { homedir, platform } from "os"; import { join } from "path"; import { getPluginSpecFromEntry } from "./config-file-utils.js"; -import { CURSOR_LEGACY_PROVIDER_ID } from "./cursor-pricing.js"; import { parseJsonOrJsonc } from "./jsonc.js"; import { getCredentialDatabasePaths, readAuthFile } from "./opencode-auth.js"; import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { getQuotaProviderRuntimeIds } from "./provider-metadata.js"; -import type { CursorOAuthAuthData } from "./types.js"; +import type { CursorAuthData } from "./types.js"; export interface CursorAuthPresence { state: "missing" | "present" | "invalid"; @@ -25,20 +24,15 @@ export interface CursorOpenCodeIntegration { checkedPaths: string[]; } -export const CURSOR_CANONICAL_PLUGIN_PACKAGE = "@playwo/opencode-cursor-oauth"; -const CURSOR_LEGACY_PLUGIN_PACKAGES = ["opencode-cursor", "opencode-cursor-oauth"]; -const CURSOR_COMPAT_PLUGIN_PACKAGES = new Set([ - CURSOR_CANONICAL_PLUGIN_PACKAGE, - ...CURSOR_LEGACY_PLUGIN_PACKAGES, - CURSOR_LEGACY_PROVIDER_ID, - "open-cursor", - "@rama_nigg/open-cursor", -]); -const CURSOR_COMPAT_PLUGIN_SUFFIXES = [ - `/${CURSOR_CANONICAL_PLUGIN_PACKAGE}`, - ...CURSOR_LEGACY_PLUGIN_PACKAGES.map((pkg) => `/${pkg}`), - "/open-cursor", -]; +export const CURSOR_CANONICAL_PLUGIN_PACKAGE = "cursor-opencode-provider"; +/** + * `cursor-opencode-provider` specs: the bare package (OpenCode 2 resolves its `./server` + * export), its OpenCode 2 entry `/plugin/opencode2`, or `/server`, each optionally + * version-pinned on the package or the entry, e.g. `cursor-opencode-provider@0.7.3/plugin/opencode2`. + */ +const CURSOR_PLUGIN_SPEC_PATTERN = + /^cursor-opencode-provider(@[^/]+)?(\/plugin\/opencode2|\/server)?(@[^/]+)?$/; +const CURSOR_API_KEY_ENV = "CURSOR_API_KEY"; function dedupe(list: string[]): string[] { return [...new Set(list.filter(Boolean))]; @@ -78,12 +72,14 @@ function hasNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } -function isValidCursorOAuthEntry(value: unknown): value is CursorOAuthAuthData { +/** OpenCode 2 stores Cursor OAuth tokens as `oauth`, and an API key `key` credential that our reader maps to `api`. */ +function isValidCursorCredential(value: unknown): value is CursorAuthData { if (!value || typeof value !== "object") return false; const entry = value as Record; - return ( - entry.type === "oauth" && (hasNonEmptyString(entry.refresh) || hasNonEmptyString(entry.access)) - ); + if (entry.type === "oauth") { + return hasNonEmptyString(entry.refresh) || hasNonEmptyString(entry.access); + } + return entry.type === "api" && hasNonEmptyString(entry.key); } export async function inspectCursorAuthPresence(): Promise { @@ -97,7 +93,7 @@ export async function inspectCursorAuthPresence(): Promise { const cursorAuth = (await readAuthFile())?.cursor; if (cursorAuth) { - if (isValidCursorOAuthEntry(cursorAuth)) { + if (isValidCursorCredential(cursorAuth)) { return { state: "present", selectedPath: credentialDatabasePath, @@ -108,7 +104,16 @@ export async function inspectCursorAuthPresence(): Promise { invalidPath = credentialDatabasePath; invalidError = - "Cursor credential in the OpenCode database is missing a valid oauth token payload"; + "Cursor credential in the OpenCode database is missing a valid OAuth token or API key"; + } + + if (hasNonEmptyString(process.env[CURSOR_API_KEY_ENV])) { + return { + state: "present", + selectedPath: `env:${CURSOR_API_KEY_ENV}`, + presentPaths, + candidatePaths, + }; } for (const path of legacyCandidatePaths) { @@ -150,11 +155,7 @@ export async function inspectCursorAuthPresence(): Promise { function pluginIncludesCursor(value: unknown): boolean { if (typeof value !== "string") return false; - const normalized = value.trim().toLowerCase(); - return ( - CURSOR_COMPAT_PLUGIN_PACKAGES.has(normalized) || - CURSOR_COMPAT_PLUGIN_SUFFIXES.some((suffix) => normalized.endsWith(suffix)) - ); + return CURSOR_PLUGIN_SPEC_PATTERN.test(value.trim().toLowerCase()); } function providerConfigIncludesCursor(value: unknown): boolean { diff --git a/src/lib/opencode-config-providers.ts b/src/lib/opencode-config-providers.ts index 092cf878..e747eec8 100644 --- a/src/lib/opencode-config-providers.ts +++ b/src/lib/opencode-config-providers.ts @@ -75,16 +75,7 @@ const COMPANION_PLUGIN_PROVIDER_IDS: ReadonlyArray<{ matches: readonly string[]; }> = [ { providerId: "google-gemini-cli", matches: ["opencode-gemini-auth"] }, - { - providerId: "cursor", - matches: [ - "@playwo/opencode-cursor-oauth", - "opencode-cursor-oauth", - "opencode-cursor", - "open-cursor", - "@rama_nigg/open-cursor", - ], - }, + { providerId: "cursor", matches: ["cursor-opencode-provider"] }, ]; function mergeOpenCodeConfig( diff --git a/src/lib/types.ts b/src/lib/types.ts index 369719c2..508de307 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -308,11 +308,12 @@ export interface CopilotAuthData { export type AlibabaCodingPlanTier = "lite" | "pro"; -export interface CursorOAuthAuthData { +export interface CursorAuthData { type: string; access?: string; refresh?: string; expires?: number; + key?: string; [key: string]: unknown; } @@ -473,7 +474,7 @@ export interface AuthData { nanogpt?: NanoGptAuthData; "nano-gpt"?: NanoGptAuthData; deepseek?: DeepSeekAuthData; - cursor?: CursorOAuthAuthData; + cursor?: CursorAuthData; alibaba?: AlibabaAuthData; "alibaba-coding-plan"?: AlibabaAuthData; "zai-coding-plan"?: { diff --git a/tests/lib.cursor-detection.test.ts b/tests/lib.cursor-detection.test.ts index c63cc0ac..4a448ff4 100644 --- a/tests/lib.cursor-detection.test.ts +++ b/tests/lib.cursor-detection.test.ts @@ -64,6 +64,7 @@ describe("cursor detection", () => { mockAuth.value = null; vi.resetModules(); delete process.env.CURSOR_ACP_HOME_DIR; + delete process.env.CURSOR_API_KEY; }); it("prefers the Cursor OAuth credential in the OpenCode database", async () => { @@ -107,15 +108,48 @@ describe("cursor detection", () => { expect(result.state).toBe("invalid"); expect(result.selectedPath).toBe(testPaths.credentialDatabase); expect(result.error).toBe( - "Cursor credential in the OpenCode database is missing a valid oauth token payload", + "Cursor credential in the OpenCode database is missing a valid OAuth token or API key", ); }); - it("detects the canonical Cursor companion package and provider.cursor config", async () => { + it("accepts a Cursor API key credential in the OpenCode database", async () => { + mockFiles.set(testPaths.credentialDatabase, ""); + // The credential reader maps OpenCode 2's stored `{ type: "key", key }` to `type: "api"`. + mockAuth.value = { cursor: { type: "api", key: "crsr_test-key" } }; + + const { inspectCursorAuthPresence } = await import("../src/lib/cursor-detection.js"); + const result = await inspectCursorAuthPresence(); + + expect(result.state).toBe("present"); + expect(result.selectedPath).toBe(testPaths.credentialDatabase); + }); + + it("reports a Cursor API key credential without a key as invalid", async () => { + mockFiles.set(testPaths.credentialDatabase, ""); + mockAuth.value = { cursor: { type: "api", key: " " } }; + + const { inspectCursorAuthPresence } = await import("../src/lib/cursor-detection.js"); + const result = await inspectCursorAuthPresence(); + + expect(result.state).toBe("invalid"); + expect(result.selectedPath).toBe(testPaths.credentialDatabase); + }); + + it("accepts the CURSOR_API_KEY environment variable", async () => { + process.env.CURSOR_API_KEY = "crsr_env-key"; + + const { inspectCursorAuthPresence } = await import("../src/lib/cursor-detection.js"); + const result = await inspectCursorAuthPresence(); + + expect(result.state).toBe("present"); + expect(result.selectedPath).toBe("env:CURSOR_API_KEY"); + }); + + it("detects the cursor-opencode-provider plugin and provider.cursor config", async () => { mockFiles.set( testPaths.opencodeConfig, JSON.stringify({ - plugin: ["@playwo/opencode-cursor-oauth"], + plugin: ["cursor-opencode-provider/plugin/opencode2"], provider: { cursor: { name: "Cursor", @@ -129,17 +163,17 @@ describe("cursor detection", () => { ); const result = await inspectCursorOpenCodeIntegration(); - expect(CURSOR_CANONICAL_PLUGIN_PACKAGE).toBe("@playwo/opencode-cursor-oauth"); + expect(CURSOR_CANONICAL_PLUGIN_PACKAGE).toBe("cursor-opencode-provider"); expect(result.pluginEnabled).toBe(true); expect(result.providerConfigured).toBe(true); expect(result.matchedPaths).toEqual([testPaths.opencodeConfig]); }); - it("detects the Cursor companion and provider in OpenCode 2 native plugins and providers", async () => { + it("detects the Cursor plugin and provider in OpenCode 2 native plugins and providers", async () => { mockFiles.set( testPaths.opencodeConfig, JSON.stringify({ - plugins: [{ package: "@playwo/opencode-cursor-oauth", options: {} }], + plugins: [{ package: "cursor-opencode-provider/plugin/opencode2", options: {} }], providers: { cursor: { name: "Cursor" } }, }), ); @@ -152,32 +186,56 @@ describe("cursor detection", () => { expect(result.matchedPaths).toEqual([testPaths.opencodeConfig]); }); - it("keeps legacy Cursor plugin names as compatibility aliases", async () => { - const aliases = [ + it("detects every cursor-opencode-provider entry and version-pinned spec", async () => { + const specs = [ + "cursor-opencode-provider", + "cursor-opencode-provider/plugin/opencode2", + "cursor-opencode-provider/server", + "cursor-opencode-provider@0.7.3", + "cursor-opencode-provider@latest/plugin/opencode2", + "cursor-opencode-provider/plugin/opencode2@0.7.3", + "cursor-opencode-provider@0.7.3/server", + " Cursor-OpenCode-Provider/Plugin/OpenCode2 ", + ]; + + const { inspectCursorOpenCodeIntegration } = await import("../src/lib/cursor-detection.js"); + + for (const spec of specs) { + mockFiles.clear(); + mockFiles.set(testPaths.opencodeConfig, JSON.stringify({ plugin: [spec] })); + + const result = await inspectCursorOpenCodeIntegration(); + + expect(result.pluginEnabled, spec).toBe(true); + expect(result.providerConfigured, spec).toBe(false); + expect(result.matchedPaths, spec).toEqual([testPaths.opencodeConfig]); + } + }); + + it("no longer detects OpenCode 1-only Cursor companions or other entries", async () => { + const specs = [ + "@playwo/opencode-cursor-oauth", "opencode-cursor-oauth", "opencode-cursor", "cursor-acp", "open-cursor", "@rama_nigg/open-cursor", "PoolPirate/opencode-cursor", + "cursor-opencode-provider/plugin", + "cursor-opencode-provider/plugin/v2", + "@someone/cursor-opencode-provider", ]; const { inspectCursorOpenCodeIntegration } = await import("../src/lib/cursor-detection.js"); - for (const alias of aliases) { + for (const spec of specs) { mockFiles.clear(); - mockFiles.set( - testPaths.opencodeConfig, - JSON.stringify({ - plugin: [alias], - }), - ); + mockFiles.set(testPaths.opencodeConfig, JSON.stringify({ plugin: [spec] })); const result = await inspectCursorOpenCodeIntegration(); - expect(result.pluginEnabled, alias).toBe(true); - expect(result.providerConfigured, alias).toBe(false); - expect(result.matchedPaths, alias).toEqual([testPaths.opencodeConfig]); + expect(result.pluginEnabled, spec).toBe(false); + expect(result.matchedPaths, spec).toEqual([]); } }); diff --git a/tests/lib.opencode-config-providers.test.ts b/tests/lib.opencode-config-providers.test.ts index e0f2f2e3..79daa99e 100644 --- a/tests/lib.opencode-config-providers.test.ts +++ b/tests/lib.opencode-config-providers.test.ts @@ -86,7 +86,7 @@ describe("opencode config provider discovery", () => { JSON.stringify({ plugin: [ "opencode-gemini-auth", - "@playwo/opencode-cursor-oauth", + "cursor-opencode-provider/plugin/opencode2", "@slkiser/opencode-quota", ], }), @@ -99,17 +99,33 @@ describe("opencode config provider discovery", () => { ]); }); + it("does not infer Cursor from OpenCode 1-only Cursor companion plugins", async () => { + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ + plugin: [ + "@playwo/opencode-cursor-oauth", + "opencode-cursor-oauth", + "@rama_nigg/open-cursor", + ], + }), + "utf8", + ); + + await expect(loadConfiguredProviderIds({ configRootDir: workspaceDir })).resolves.toEqual([]); + }); + it("deduplicates provider ids inferred from provider blocks and plugin specs", async () => { writeFileSync( join(globalConfigDir, "opencode.json"), - JSON.stringify({ plugin: ["open-cursor"] }), + JSON.stringify({ plugin: ["cursor-opencode-provider"] }), "utf8", ); writeFileSync( join(workspaceDir, "opencode.json"), JSON.stringify({ provider: { "alibaba-coding-plan": {}, cursor: {} }, - plugin: [["@playwo/opencode-cursor-oauth", { enabled: true }]], + plugin: [["cursor-opencode-provider/server", { enabled: true }]], }), "utf8", ); @@ -130,7 +146,9 @@ describe("opencode config provider discovery", () => { join(workspaceDir, "opencode.json"), JSON.stringify({ provider: { openai: {} }, - plugins: [{ package: "@playwo/opencode-cursor-oauth", options: { enabled: true } }], + plugins: [ + { package: "cursor-opencode-provider/plugin/opencode2", options: { enabled: true } }, + ], }), "utf8", ); diff --git a/tests/providers.cursor.test.ts b/tests/providers.cursor.test.ts index 27170c0f..87cebc7b 100644 --- a/tests/providers.cursor.test.ts +++ b/tests/providers.cursor.test.ts @@ -11,7 +11,7 @@ vi.mock("../src/lib/provider-availability.js", () => ({ })); vi.mock("../src/lib/cursor-detection.js", () => ({ - CURSOR_CANONICAL_PLUGIN_PACKAGE: "@playwo/opencode-cursor-oauth", + CURSOR_CANONICAL_PLUGIN_PACKAGE: "cursor-opencode-provider", inspectCursorAuthPresence: vi.fn(async () => ({ state: "missing", presentPaths: [], diff --git a/tests/upstream-plugin-specs.test.ts b/tests/upstream-plugin-specs.test.ts index 0bbc692c..bd49c1c1 100644 --- a/tests/upstream-plugin-specs.test.ts +++ b/tests/upstream-plugin-specs.test.ts @@ -5,7 +5,6 @@ import { UPSTREAM_PLUGIN_REFERENCE_ROOT, UPSTREAM_PLUGIN_SPECS, } from "../scripts/lib/upstream-plugin-specs.mjs"; -import { CURSOR_CANONICAL_PLUGIN_PACKAGE } from "../src/lib/cursor-detection.js"; describe("upstream-plugin-specs", () => { it("tracks the expected upstream plugin ids", () => { @@ -66,10 +65,4 @@ describe("upstream-plugin-specs", () => { ), ).toEqual(["opencode-agy-auth"]); }); - - it("keeps the runtime Cursor package name aligned with the upstream spec", () => { - expect(getUpstreamPluginSpec("opencode-cursor-oauth")?.packageName).toBe( - CURSOR_CANONICAL_PLUGIN_PACKAGE, - ); - }); }); From 9fda93c8f0d958a658695d084ec73223e1489be2 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:31 +0200 Subject: [PATCH 54/97] fix(cursor): update Cursor model prices Cursor spend estimates now know the models that cursor-opencode-provider 0.7.3 exposes, so current history no longer drops to "Known API spend". Sources (checked 2026-09-26): - https://cursor.com/docs/models-and-pricing.md (Cursor Models and Other Models tables; Other Models are "charged at the model's API price") - https://models.dev/api.json (cross-check for Other Models) - cursor-opencode-provider 0.7.3 dist/pricing-data.js (model id list only) Cursor Models pool (bundled Cursor rates, Auto+Composer bucket): composer-2.5, composer-2.5-fast, grok-4.5, grok-4.5-fast, grok-4.6, grok-4.6-fast, grok-4.7, grok-4.7-fast. Cursor publishes separate Fast rates for these, and the plugin emits them as separate -fast ids. Other Models pool (API bucket, models.dev key): 35 new ids, each checked so the models.dev rate equals Cursor's published rate. Claude Fable 5 and 5.1, Haiku 4.5, Opus 4.5-5.5, Sonnet 4-5, Gemini 2.5 Flash and 3.5 Flash, GLM 5.2, GPT-5 Mini, 5.1, 5.4 Mini/Nano, 5.5, 5.6 Luna/Sol/Terra, Kimi K2.7 Code and K3. -1m ids map to the base price only where Cursor documents 1M context "at the same per-token rates": Claude Opus 4.6/4.7/4.8/5/5.5, Sonnet 4.5/4.6/5, and Kimi K3. Bundled models.dev snapshot: added the 14 missing keys and corrected gemini-2.5-flash cache_read (0.075 -> 0.03) and gpt-5.1 cache_read (0.13 -> 0.125), copied from the live models.dev response with the refresh script's field selection. A full pnpm pricing:refresh would drop 71 older models, including four existing Cursor alias targets, so it was not run (same approach as dfdc39ad0 for Kimi K3). Left unknown (price not confirmed): default (Auto bills at the routed model's price); gemini-3.6/3.7/3.8-flash (models.dev differs from Cursor); muse-spark-1.3 (meta is not a snapshot provider); claude-fable-5(-1)-1m, claude-sonnet-4-1m, gpt-5.4/5.5/5.6-*-1m, grok-4.7-1m(-fast) (tiered or undocumented long-context rates). --- docs/readme/troubleshooting.md | 2 +- src/data/modelsdev-pricing.min.json | 85 +++++++++- src/lib/cursor-pricing.ts | 95 ++++++++++- tests/lib.cursor-usage.test.ts | 41 +++++ tests/pricing-resolver.coverage.test.ts | 199 ++++++++++++++++++++++++ 5 files changed, 418 insertions(+), 4 deletions(-) diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index a8126932..d7a6d544 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -266,6 +266,6 @@ Run `/quota_status` and check pricing snapshot health plus the `opencode.db` pat | `/tokens_*` is empty | Start OpenCode once so it creates `opencode.db`, then run a session with model usage. | | Pricing looks stale | Run `/pricing_refresh`. | | Runtime pricing does not change output | Check `pricingSnapshot.source` in `opencode-quota/quota-toast.json`; `bundled` keeps packaged pricing active. | -| Cursor model has unknown pricing | Run `/pricing_refresh`; Cursor `auto` and `composer*` use bundled deterministic pricing. | +| Cursor model has unknown pricing | Run `/pricing_refresh`; Cursor `auto`, `composer*`, and `grok-4.5`–`grok-4.7` use bundled Cursor pricing. |
diff --git a/src/data/modelsdev-pricing.min.json b/src/data/modelsdev-pricing.min.json index 48cc9ec5..0cc03148 100644 --- a/src/data/modelsdev-pricing.min.json +++ b/src/data/modelsdev-pricing.min.json @@ -68,6 +68,18 @@ "cache_read": 0.3, "cache_write": 0.3 }, + "claude-fable-5": { + "input": 10, + "output": 50, + "cache_read": 1, + "cache_write": 12.5 + }, + "claude-fable-5-1": { + "input": 10, + "output": 50, + "cache_read": 0.25, + "cache_write": 12.5 + }, "claude-haiku-4-5": { "input": 1, "output": 5, @@ -122,6 +134,30 @@ "cache_read": 0.5, "cache_write": 6.25 }, + "claude-opus-4-7": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "claude-opus-4-8": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "claude-opus-5": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "claude-opus-5-5": { + "input": 4, + "output": 20, + "cache_read": 0.2, + "cache_write": 5 + }, "claude-sonnet-4-0": { "input": 3, "output": 15, @@ -151,6 +187,12 @@ "output": 15, "cache_read": 0.3, "cache_write": 3.75 + }, + "claude-sonnet-5": { + "input": 2, + "output": 10, + "cache_read": 0.2, + "cache_write": 2.5 } }, "google": { @@ -181,7 +223,7 @@ "gemini-2.5-flash": { "input": 0.3, "output": 2.5, - "cache_read": 0.075 + "cache_read": 0.03 }, "gemini-2.5-flash-image": { "input": 0.3, @@ -276,6 +318,11 @@ "output": 12, "cache_read": 0.2 }, + "gemini-3.5-flash": { + "input": 1.5, + "output": 9, + "cache_read": 0.15 + }, "gemini-embedding-001": { "input": 0.15, "output": 0 @@ -330,6 +377,11 @@ "output": 3, "cache_read": 0.1 }, + "kimi-k2.7-code": { + "input": 0.95, + "output": 4, + "cache_read": 0.19 + }, "kimi-k3": { "input": 3, "output": 15, @@ -425,7 +477,7 @@ "gpt-5.1": { "input": 1.25, "output": 10, - "cache_read": 0.13 + "cache_read": 0.125 }, "gpt-5.1-chat-latest": { "input": 1.25, @@ -495,6 +547,29 @@ "input": 30, "output": 180 }, + "gpt-5.5": { + "input": 5, + "output": 30, + "cache_read": 0.5 + }, + "gpt-5.6-luna": { + "input": 0.2, + "output": 1.2, + "cache_read": 0.02, + "cache_write": 0.25 + }, + "gpt-5.6-sol": { + "input": 4, + "output": 20, + "cache_read": 0.4, + "cache_write": 5 + }, + "gpt-5.6-terra": { + "input": 2, + "output": 12, + "cache_read": 0.2, + "cache_write": 2.5 + }, "o1": { "input": 15, "output": 60, @@ -739,6 +814,12 @@ "output": 4, "cache_read": 0.24, "cache_write": 0 + }, + "glm-5.2": { + "input": 1.4, + "output": 4.4, + "cache_read": 0.26, + "cache_write": 0 } } } diff --git a/src/lib/cursor-pricing.ts b/src/lib/cursor-pricing.ts index 47ad14ca..bf92867d 100644 --- a/src/lib/cursor-pricing.ts +++ b/src/lib/cursor-pricing.ts @@ -7,7 +7,15 @@ export type CursorLocalPricingModel = | "composer-1" | "composer-1.5" | "composer-2" - | "composer-2-fast"; + | "composer-2-fast" + | "composer-2.5" + | "composer-2.5-fast" + | "grok-4.5" + | "grok-4.5-fast" + | "grok-4.6" + | "grok-4.6-fast" + | "grok-4.7" + | "grok-4.7-fast"; export type CursorResolvedModel = | { kind: "local"; model: CursorLocalPricingModel; pool: "auto_composer" } @@ -58,6 +66,46 @@ const CURSOR_LOCAL_PRICING: Readonly> = { @@ -67,19 +115,55 @@ const CURSOR_LOCAL_MODEL_ALIASES: Readonly > = { + // `-1m` ids are the long-context entries of cursor-opencode-provider. Only models that + // Cursor documents as 1M context "at the same per-token rates" map to the base price. "claude-4.5-sonnet": { providerHint: "anthropic", modelHint: "claude-sonnet-4-5" }, "claude-4.6-opus-high": { providerHint: "anthropic", modelHint: "claude-opus-4-6" }, "claude-4.6-opus": { providerHint: "anthropic", modelHint: "claude-opus-4-6" }, "claude-4.6-sonnet-medium": { providerHint: "anthropic", modelHint: "claude-sonnet-4-6" }, "claude-4.6-sonnet": { providerHint: "anthropic", modelHint: "claude-sonnet-4-6" }, + "claude-fable-5": { providerHint: "anthropic", modelHint: "claude-fable-5" }, + "claude-fable-5-1": { providerHint: "anthropic", modelHint: "claude-fable-5-1" }, + "claude-haiku-4-5": { providerHint: "anthropic", modelHint: "claude-haiku-4-5" }, + "claude-opus-4-5": { providerHint: "anthropic", modelHint: "claude-opus-4-5" }, + "claude-opus-4-6": { providerHint: "anthropic", modelHint: "claude-opus-4-6" }, + "claude-opus-4-6-1m": { providerHint: "anthropic", modelHint: "claude-opus-4-6" }, + "claude-opus-4-7": { providerHint: "anthropic", modelHint: "claude-opus-4-7" }, + "claude-opus-4-7-1m": { providerHint: "anthropic", modelHint: "claude-opus-4-7" }, + "claude-opus-4-8": { providerHint: "anthropic", modelHint: "claude-opus-4-8" }, + "claude-opus-4-8-1m": { providerHint: "anthropic", modelHint: "claude-opus-4-8" }, + "claude-opus-5": { providerHint: "anthropic", modelHint: "claude-opus-5" }, + "claude-opus-5-1m": { providerHint: "anthropic", modelHint: "claude-opus-5" }, + "claude-opus-5-5": { providerHint: "anthropic", modelHint: "claude-opus-5-5" }, + "claude-opus-5-5-1m": { providerHint: "anthropic", modelHint: "claude-opus-5-5" }, + "claude-sonnet-4": { providerHint: "anthropic", modelHint: "claude-sonnet-4-0" }, + "claude-sonnet-4-5": { providerHint: "anthropic", modelHint: "claude-sonnet-4-5" }, + "claude-sonnet-4-5-1m": { providerHint: "anthropic", modelHint: "claude-sonnet-4-5" }, + "claude-sonnet-4-6": { providerHint: "anthropic", modelHint: "claude-sonnet-4-6" }, + "claude-sonnet-4-6-1m": { providerHint: "anthropic", modelHint: "claude-sonnet-4-6" }, + "claude-sonnet-5": { providerHint: "anthropic", modelHint: "claude-sonnet-5" }, + "claude-sonnet-5-1m": { providerHint: "anthropic", modelHint: "claude-sonnet-5" }, + "gemini-2.5-flash": { providerHint: "google", modelHint: "gemini-2.5-flash" }, "gemini-3-flash": { providerHint: "google", modelHint: "gemini-3-flash-preview" }, "gemini-3-pro": { providerHint: "google", modelHint: "gemini-3-pro-preview" }, "gemini-3.1-pro": { providerHint: "google", modelHint: "gemini-3.1-pro-preview" }, + "gemini-3.5-flash": { providerHint: "google", modelHint: "gemini-3.5-flash" }, + "glm-5.2": { providerHint: "zai", modelHint: "glm-5.2" }, + "gpt-5-mini": { providerHint: "openai", modelHint: "gpt-5-mini" }, + "gpt-5.1": { providerHint: "openai", modelHint: "gpt-5.1" }, "gpt-5.2": { providerHint: "openai", modelHint: "gpt-5.2" }, "gpt-5.2-codex": { providerHint: "openai", modelHint: "gpt-5.2-codex" }, "gpt-5.3-codex": { providerHint: "openai", modelHint: "gpt-5.3-codex" }, @@ -87,9 +171,18 @@ export const CURSOR_OFFICIAL_MODEL_ALIASES: Readonly< "gpt-5.4": { providerHint: "openai", modelHint: "gpt-5.4" }, "gpt-5.4-high": { providerHint: "openai", modelHint: "gpt-5.4" }, "gpt-5.4-medium": { providerHint: "openai", modelHint: "gpt-5.4" }, + "gpt-5.4-mini": { providerHint: "openai", modelHint: "gpt-5.4-mini" }, + "gpt-5.4-nano": { providerHint: "openai", modelHint: "gpt-5.4-nano" }, + "gpt-5.5": { providerHint: "openai", modelHint: "gpt-5.5" }, + "gpt-5.6-luna": { providerHint: "openai", modelHint: "gpt-5.6-luna" }, + "gpt-5.6-sol": { providerHint: "openai", modelHint: "gpt-5.6-sol" }, + "gpt-5.6-terra": { providerHint: "openai", modelHint: "gpt-5.6-terra" }, grok: { providerHint: "xai", modelHint: "grok-code-fast-1" }, "grok-code-fast-1": { providerHint: "xai", modelHint: "grok-code-fast-1" }, "kimi-k2.5": { providerHint: "moonshotai", modelHint: "kimi-k2.5" }, + "kimi-k2.7-code": { providerHint: "moonshotai", modelHint: "kimi-k2.7-code" }, + "kimi-k3": { providerHint: "moonshotai", modelHint: "kimi-k3" }, + "kimi-k3-1m": { providerHint: "moonshotai", modelHint: "kimi-k3" }, "opus-4.5": { providerHint: "anthropic", modelHint: "claude-opus-4-5" }, "opus-4.5-thinking": { providerHint: "anthropic", modelHint: "claude-opus-4-5" }, "opus-4.6": { providerHint: "anthropic", modelHint: "claude-opus-4-6" }, diff --git a/tests/lib.cursor-usage.test.ts b/tests/lib.cursor-usage.test.ts index 5fbdbaa4..473010dd 100644 --- a/tests/lib.cursor-usage.test.ts +++ b/tests/lib.cursor-usage.test.ts @@ -89,6 +89,47 @@ describe("cursor usage", () => { expect(summary.unknownModels).toEqual([]); }); + it("buckets cursor-opencode-provider fast and 1m entries by Cursor pool", async () => { + const { iterAssistantMessages } = await import("../src/lib/opencode-storage.js"); + (iterAssistantMessages as any).mockResolvedValue([ + { + role: "assistant", + providerID: "cursor", + modelID: "composer-2.5-fast", + tokens: { input: 1_000_000, output: 1_000_000, cache: { read: 0, write: 0 } }, + }, + { + role: "assistant", + providerID: "cursor", + modelID: "grok-4.7", + tokens: { input: 1_000_000, output: 500_000, cache: { read: 0, write: 0 } }, + }, + { + role: "assistant", + providerID: "cursor", + modelID: "claude-opus-4-8-1m", + tokens: { input: 1_000_000, output: 1_000_000, cache: { read: 0, write: 0 } }, + }, + { + role: "assistant", + providerID: "cursor", + modelID: "gpt-5.5-1m", + tokens: { input: 10, output: 20, cache: { read: 0, write: 0 } }, + }, + ]); + + const summary = await getCurrentCursorUsageSummary({ + nowMs: new Date(2026, 2, 19, 10, 0, 0, 0).getTime(), + billingCycleStartDay: 7, + }); + + expect(summary.autoComposer.messageCount).toBe(2); + expect(summary.autoComposer.costUsd).toBeCloseTo(23, 6); + expect(summary.api.messageCount).toBe(1); + expect(summary.api.costUsd).toBeCloseTo(30, 6); + expect(summary.unknownModels.map((model) => model.sourceModelID)).toEqual(["gpt-5.5-1m"]); + }); + it("treats new Cursor official fallback aliases as api usage instead of unknown models", async () => { const { iterAssistantMessages } = await import("../src/lib/opencode-storage.js"); (iterAssistantMessages as any).mockResolvedValue([ diff --git a/tests/pricing-resolver.coverage.test.ts b/tests/pricing-resolver.coverage.test.ts index 1870cfe6..7f74152e 100644 --- a/tests/pricing-resolver.coverage.test.ts +++ b/tests/pricing-resolver.coverage.test.ts @@ -324,6 +324,205 @@ describe("resolvePricingKey snapshot coverage", () => { cache_read: 0.35, output: 7.5, }); + expect(lookupCursorLocalCost("composer-2.5")).toEqual({ + input: 0.5, + cache_read: 0.2, + output: 2.5, + }); + expect(lookupCursorLocalCost("composer-2.5-fast")).toEqual({ + input: 3, + cache_read: 0.5, + output: 15, + }); + expect(lookupCursorLocalCost("grok-4.5")).toEqual({ input: 2, cache_read: 0.5, output: 6 }); + expect(lookupCursorLocalCost("grok-4.5-fast")).toEqual({ input: 4, cache_read: 1, output: 18 }); + expect(lookupCursorLocalCost("grok-4.6")).toEqual({ input: 2, cache_read: 0.5, output: 6 }); + expect(lookupCursorLocalCost("grok-4.6-fast")).toEqual({ input: 4, cache_read: 1, output: 12 }); + expect(lookupCursorLocalCost("grok-4.7")).toEqual({ input: 2, cache_read: 0.5, output: 6 }); + expect(lookupCursorLocalCost("grok-4.7-fast")).toEqual({ input: 4, cache_read: 1, output: 12 }); + }); + + it("prices cursor-opencode-provider model ids at Cursor's published rates", () => { + const cursorModelsPool = [ + "composer-2.5", + "composer-2.5-fast", + "grok-4.5", + "grok-4.5-fast", + "grok-4.6", + "grok-4.6-fast", + "grok-4.7", + "grok-4.7-fast", + ]; + for (const modelID of cursorModelsPool) { + expect(resolveCursorModel(`cursor/${modelID}`), modelID).toEqual({ + kind: "local", + model: modelID, + pool: "auto_composer", + }); + } + + const otherModelsPool = [ + ["claude-fable-5", "anthropic", "claude-fable-5", { input: 10, output: 50, cache_read: 1 }], + [ + "claude-fable-5-1", + "anthropic", + "claude-fable-5-1", + { input: 10, output: 50, cache_read: 0.25 }, + ], + [ + "claude-haiku-4-5", + "anthropic", + "claude-haiku-4-5", + { input: 1, output: 5, cache_read: 0.1 }, + ], + [ + "claude-opus-4-5", + "anthropic", + "claude-opus-4-5", + { input: 5, output: 25, cache_read: 0.5 }, + ], + [ + "claude-opus-4-6", + "anthropic", + "claude-opus-4-6", + { input: 5, output: 25, cache_read: 0.5 }, + ], + [ + "claude-opus-4-7", + "anthropic", + "claude-opus-4-7", + { input: 5, output: 25, cache_read: 0.5 }, + ], + [ + "claude-opus-4-8", + "anthropic", + "claude-opus-4-8", + { input: 5, output: 25, cache_read: 0.5 }, + ], + ["claude-opus-5", "anthropic", "claude-opus-5", { input: 5, output: 25, cache_read: 0.5 }], + [ + "claude-opus-5-5", + "anthropic", + "claude-opus-5-5", + { input: 4, output: 20, cache_read: 0.2 }, + ], + [ + "claude-sonnet-4", + "anthropic", + "claude-sonnet-4-0", + { input: 3, output: 15, cache_read: 0.3 }, + ], + [ + "claude-sonnet-4-5", + "anthropic", + "claude-sonnet-4-5", + { input: 3, output: 15, cache_read: 0.3 }, + ], + [ + "claude-sonnet-4-6", + "anthropic", + "claude-sonnet-4-6", + { input: 3, output: 15, cache_read: 0.3 }, + ], + [ + "claude-sonnet-5", + "anthropic", + "claude-sonnet-5", + { input: 2, output: 10, cache_read: 0.2 }, + ], + [ + "gemini-2.5-flash", + "google", + "gemini-2.5-flash", + { input: 0.3, output: 2.5, cache_read: 0.03 }, + ], + [ + "gemini-3-flash", + "google", + "gemini-3-flash-preview", + { input: 0.5, output: 3, cache_read: 0.05 }, + ], + [ + "gemini-3.1-pro", + "google", + "gemini-3.1-pro-preview", + { input: 2, output: 12, cache_read: 0.2 }, + ], + [ + "gemini-3.5-flash", + "google", + "gemini-3.5-flash", + { input: 1.5, output: 9, cache_read: 0.15 }, + ], + ["glm-5.2", "zai", "glm-5.2", { input: 1.4, output: 4.4, cache_read: 0.26 }], + ["gpt-5-mini", "openai", "gpt-5-mini", { input: 0.25, output: 2, cache_read: 0.025 }], + ["gpt-5.1", "openai", "gpt-5.1", { input: 1.25, output: 10, cache_read: 0.125 }], + ["gpt-5.2", "openai", "gpt-5.2", { input: 1.75, output: 14, cache_read: 0.175 }], + ["gpt-5.3-codex", "openai", "gpt-5.3-codex", { input: 1.75, output: 14, cache_read: 0.175 }], + ["gpt-5.4", "openai", "gpt-5.4", { input: 2.5, output: 15, cache_read: 0.25 }], + ["gpt-5.4-mini", "openai", "gpt-5.4-mini", { input: 0.75, output: 4.5, cache_read: 0.075 }], + ["gpt-5.4-nano", "openai", "gpt-5.4-nano", { input: 0.2, output: 1.25, cache_read: 0.02 }], + ["gpt-5.5", "openai", "gpt-5.5", { input: 5, output: 30, cache_read: 0.5 }], + ["gpt-5.6-luna", "openai", "gpt-5.6-luna", { input: 0.2, output: 1.2, cache_read: 0.02 }], + ["gpt-5.6-sol", "openai", "gpt-5.6-sol", { input: 4, output: 20, cache_read: 0.4 }], + ["gpt-5.6-terra", "openai", "gpt-5.6-terra", { input: 2, output: 12, cache_read: 0.2 }], + [ + "kimi-k2.7-code", + "moonshotai", + "kimi-k2.7-code", + { input: 0.95, output: 4, cache_read: 0.19 }, + ], + ["kimi-k3", "moonshotai", "kimi-k3", { input: 3, output: 15, cache_read: 0.3 }], + ] as const; + for (const [modelID, providerHint, modelHint, rates] of otherModelsPool) { + expect(resolveCursorModel(`cursor/${modelID}`), modelID).toEqual({ + kind: "official", + providerHint, + modelHint, + pool: "api", + }); + expect(lookupCost(providerHint, modelHint), modelID).toMatchObject(rates); + } + }); + + it("prices -1m ids at the base rate only where Cursor documents no long-context surcharge", () => { + const sameRateLongContext = [ + ["claude-opus-4-6-1m", "claude-opus-4-6"], + ["claude-opus-4-7-1m", "claude-opus-4-7"], + ["claude-opus-4-8-1m", "claude-opus-4-8"], + ["claude-opus-5-1m", "claude-opus-5"], + ["claude-opus-5-5-1m", "claude-opus-5-5"], + ["claude-sonnet-4-5-1m", "claude-sonnet-4-5"], + ["claude-sonnet-4-6-1m", "claude-sonnet-4-6"], + ["claude-sonnet-5-1m", "claude-sonnet-5"], + ["kimi-k3-1m", "kimi-k3"], + ] as const; + for (const [modelID, baseModelID] of sameRateLongContext) { + expect(resolveCursorModel(`cursor/${modelID}`), modelID).toEqual( + resolveCursorModel(`cursor/${baseModelID}`), + ); + } + + // Unpublished (Auto), tiered long-context surcharges, or models.dev rates that differ + // from Cursor's published rates stay unknown instead of guessing a price. + const unconfirmed = [ + "default", + "claude-fable-5-1m", + "claude-fable-5-1-1m", + "claude-sonnet-4-1m", + "gemini-3.6-flash", + "gemini-3.7-flash", + "gemini-3.8-flash", + "gpt-5.4-1m", + "gpt-5.5-1m", + "gpt-5.6-sol-1m", + "grok-4.7-1m", + "grok-4.7-1m-fast", + "muse-spark-1.3", + ]; + for (const modelID of unconfirmed) { + expect(resolveCursorModel(`cursor/${modelID}`), modelID).toEqual({ kind: "unknown" }); + } }); it("keeps every Cursor API alias aligned with a priced snapshot key", () => { From 252f7c75e009b06d9ea9167d4965069203086eae Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:32 +0200 Subject: [PATCH 55/97] chore(upstream): track cursor-opencode-provider and OpenCode 2 companions Cursor: the upstream spec now tracks cursor-opencode-provider (oakimov/cursor-opencode-provider, npm latest) under the plugin id cursor-opencode-provider, replacing @playwo/opencode-cursor-oauth. The old models.js/proxy.js rewrite rules are dropped. The new package ships no OAuth client secret (browser login is PKCE), so sanitization instead fails the sync if a Cursor API key (crsr_...) or access token (JWT) ever lands in the snapshot. Pricing coverage now reads the model ids from the snapshot's dist/pricing-data.js (CURSOR_MODEL_COSTS). gemini-3.6/3.7/3.8-flash and muse-spark-1.3 are allowlisted as intentionally unknown, matching 404a804b2. The reference guard test now checks the OpenCode 2 entries (./plugin/opencode2, ./server), the cursor integration id, the oauth/key credential methods, and CURSOR_API_KEY. Restored the consistency test: CURSOR_CANONICAL_PLUGIN_PACKAGE equals the tracked Cursor package name. Dist-tags: each spec now names the npm dist-tag it follows. Google AGY follows alpha (the OpenCode 2 build; latest is still the OpenCode 1 build). Gemini CLI and Cursor follow latest; opencode-gemini-auth latest is 2.0.1, the OpenCode 2 line. Synced references: cursor-opencode-provider 0.7.3 added, opencode-cursor-oauth removed, opencode-agy-auth 1.2.10 -> 1.2.11-alpha.0, opencode-gemini-auth 2.0.1 unchanged. --- references/upstream-plugins/README.md | 8 +- .../cursor-opencode-provider/DISCLAIMER.md | 9 + .../cursor-opencode-provider/LICENSE | 21 + .../cursor-opencode-provider/README.md | 390 + .../cursor-opencode-provider/SECURITY.md | 42 + .../dist/activity.d.ts | 15 + .../cursor-opencode-provider/dist/activity.js | 76 + .../dist/agent-url.d.ts | 20 + .../dist/agent-url.js | 77 + .../cursor-opencode-provider/dist/auth.d.ts | 56 + .../cursor-opencode-provider/dist/auth.js | 264 + .../dist/compaction-marker.d.ts | 15 + .../dist/compaction-marker.js | 37 + .../dist/context/agents.d.ts | 12 + .../dist/context/agents.js | 136 + .../dist/context/auth-store.d.ts | 21 + .../dist/context/auth-store.js | 64 + .../dist/context/build.d.ts | 24 + .../dist/context/build.js | 200 + .../dist/context/env.d.ts | 7 + .../dist/context/env.js | 65 + .../dist/context/epoch.d.ts | 59 + .../dist/context/epoch.js | 224 + .../dist/context/frozen.d.ts | 31 + .../dist/context/frozen.js | 188 + .../dist/context/git.d.ts | 20 + .../dist/context/git.js | 59 + .../dist/context/index.d.ts | 5 + .../dist/context/index.js | 5 + .../dist/context/layout.d.ts | 17 + .../dist/context/layout.js | 58 + .../dist/context/overlay.d.ts | 44 + .../dist/context/overlay.js | 96 + .../dist/context/paths.d.ts | 75 + .../dist/context/paths.js | 164 + .../dist/context/plugins.d.ts | 8 + .../dist/context/plugins.js | 55 + .../dist/context/rules.d.ts | 25 + .../dist/context/rules.js | 239 + .../dist/context/skills.d.ts | 11 + .../dist/context/skills.js | 110 + .../dist/deadline.d.ts | 8 + .../cursor-opencode-provider/dist/deadline.js | 25 + .../cursor-opencode-provider/dist/debug.d.ts | 25 + .../cursor-opencode-provider/dist/debug.js | 145 + .../cursor-opencode-provider/dist/errors.d.ts | 69 + .../cursor-opencode-provider/dist/errors.js | 219 + .../dist/host-agent-mode.d.ts | 32 + .../dist/host-agent-mode.js | 92 + .../dist/host-event-bridge.d.ts | 22 + .../dist/host-event-bridge.js | 22 + .../dist/image-input.d.ts | 31 + .../dist/image-input.js | 306 + .../dist/image-save-tool.d.ts | 19 + .../dist/image-save-tool.js | 25 + .../dist/image-save.d.ts | 58 + .../dist/image-save.js | 189 + .../dist/image-staging.d.ts | 59 + .../dist/image-staging.js | 91 + .../cursor-opencode-provider/dist/index.d.ts | 43 + .../cursor-opencode-provider/dist/index.js | 16 + .../dist/language-model.d.ts | 218 + .../dist/language-model.js | 3801 +++++ .../dist/model-config.d.ts | 17 + .../dist/model-config.js | 284 + .../dist/model-metadata.d.ts | 10 + .../dist/model-metadata.js | 12 + .../cursor-opencode-provider/dist/models.d.ts | 103 + .../cursor-opencode-provider/dist/models.js | 455 + .../dist/opencode2/catalog.d.ts | 68 + .../dist/opencode2/catalog.js | 106 + .../dist/opencode2/integration.d.ts | 23 + .../dist/opencode2/integration.js | 124 + .../dist/opencode2/todo-tools.d.ts | 64 + .../dist/opencode2/todo-tools.js | 165 + .../dist/opencode2/types.d.ts | 389 + .../dist/opencode2/types.js | 16 + .../dist/plan-execution-kickoff.d.ts | 57 + .../dist/plan-execution-kickoff.js | 186 + .../dist/plugin-core.d.ts | 27 + .../dist/plugin-core.js | 31 + .../dist/plugin-opencode2.d.ts | 6 + .../dist/plugin-opencode2.js | 537 + .../dist/plugin-v2.d.ts | 11 + .../dist/plugin-v2.js | 31 + .../cursor-opencode-provider/dist/plugin.d.ts | 12 + .../cursor-opencode-provider/dist/plugin.js | 395 + .../dist/pricing-data.d.ts | 550 + .../dist/pricing-data.js | 550 + .../dist/pricing.d.ts | 68 + .../cursor-opencode-provider/dist/pricing.js | 136 + .../dist/protocol/apply-patch.d.ts | 109 + .../dist/protocol/apply-patch.js | 163 + .../dist/protocol/ask-question.d.ts | 132 + .../dist/protocol/ask-question.js | 319 + .../dist/protocol/blob-reachability.d.ts | 15 + .../dist/protocol/blob-reachability.js | 140 + .../dist/protocol/blob-store.d.ts | 54 + .../dist/protocol/blob-store.js | 171 + .../dist/protocol/checkpoint.d.ts | 17 + .../dist/protocol/checkpoint.js | 29 + .../dist/protocol/checksum.d.ts | 2 + .../dist/protocol/checksum.js | 23 + .../dist/protocol/client-version.d.ts | 5 + .../dist/protocol/client-version.js | 137 + .../dist/protocol/conversation-bind.d.ts | 32 + .../dist/protocol/conversation-bind.js | 105 + .../protocol/conversation-persistence.d.ts | 34 + .../dist/protocol/conversation-persistence.js | 522 + .../dist/protocol/conversation-state.d.ts | 20 + .../dist/protocol/conversation-state.js | 66 + .../dist/protocol/create-plan.d.ts | 140 + .../dist/protocol/create-plan.js | 372 + .../dist/protocol/device-id.d.ts | 8 + .../dist/protocol/device-id.js | 121 + .../dist/protocol/exec-variants.d.ts | 29 + .../dist/protocol/exec-variants.js | 70 + .../dist/protocol/framing.d.ts | 10 + .../dist/protocol/framing.js | 90 + .../dist/protocol/generate-image.d.ts | 54 + .../dist/protocol/generate-image.js | 101 + .../dist/protocol/git-diff.d.ts | 33 + .../dist/protocol/git-diff.js | 372 + .../dist/protocol/interactions.d.ts | 132 + .../dist/protocol/interactions.js | 409 + .../dist/protocol/kv.d.ts | 25 + .../dist/protocol/kv.js | 83 + .../dist/protocol/messages.d.ts | 23 + .../dist/protocol/messages.js | 1810 +++ .../dist/protocol/progress-continuation.d.ts | 25 + .../dist/protocol/progress-continuation.js | 92 + .../dist/protocol/request.d.ts | 70 + .../dist/protocol/request.js | 108 + .../dist/protocol/stream.d.ts | 38 + .../dist/protocol/stream.js | 70 + .../dist/protocol/struct.d.ts | 37 + .../dist/protocol/struct.js | 268 + .../dist/protocol/switch-mode.d.ts | 178 + .../dist/protocol/switch-mode.js | 434 + .../dist/protocol/thinking.d.ts | 15 + .../dist/protocol/thinking.js | 17 + .../dist/protocol/token-details.d.ts | 33 + .../dist/protocol/token-details.js | 155 + .../dist/protocol/tool-call-bridge.d.ts | 75 + .../dist/protocol/tool-call-bridge.js | 719 + .../dist/protocol/tools.d.ts | 356 + .../dist/protocol/tools.js | 3286 ++++ .../dist/protocol/workspace-grounding.d.ts | 17 + .../dist/protocol/workspace-grounding.js | 37 + .../dist/replay-safety.d.ts | 23 + .../dist/replay-safety.js | 126 + .../dist/session-directory.d.ts | 36 + .../dist/session-directory.js | 67 + .../dist/session.d.ts | 321 + .../cursor-opencode-provider/dist/session.js | 586 + .../cursor-opencode-provider/dist/shared.d.ts | 26 + .../cursor-opencode-provider/dist/shared.js | 24 + .../dist/shell-timeout.d.ts | 119 + .../dist/shell-timeout.js | 516 + .../dist/todo-store.d.ts | 29 + .../dist/todo-store.js | 78 + .../dist/transport/connect.d.ts | 86 + .../dist/transport/connect.js | 1002 ++ .../dist/transport/https-proxy.d.ts | 56 + .../dist/transport/https-proxy.js | 348 + .../cursor-opencode-provider/dist/usage.d.ts | 86 + .../cursor-opencode-provider/dist/usage.js | 300 + .../dist/web-search-tool.d.ts | 29 + .../dist/web-search-tool.js | 32 + .../dist/web-tools.d.ts | 31 + .../dist/web-tools.js | 203 + .../cursor-opencode-provider/package.json | 105 + references/upstream-plugins/lock.json | 22 +- .../opencode-agy-auth/LICENSE | 176 + .../opencode-agy-auth/README.md | 1 + .../opencode-agy-auth/dist/index.d.ts | 11 +- .../opencode-agy-auth/dist/index.js | 1136 +- .../opencode-agy-auth/dist/index.js.map | 2 +- .../opencode-agy-auth/dist/src/constants.d.ts | 2 + .../opencode-agy-auth/dist/src/plugin.d.ts | 2 + .../dist/src/plugin/headers.d.ts | 7 + .../dist/src/plugin/oauth-authorize.d.ts | 9 + .../dist/src/plugin/quota-summary.d.ts | 2 + .../dist/src/plugin/quota.d.ts | 2 + .../dist/src/plugin/tier.d.ts | 28 + .../dist/src/plugin/types.d.ts | 35 + .../dist/src/plugin/v2-fetch.d.ts | 1 + .../dist/src/plugin/v2-session.d.ts | 7 + .../dist/src/plugin/v2-storage.d.ts | 3 + .../opencode-agy-auth/dist/src/plugin/v2.d.ts | 14 + .../dist/src/sdk/agy-cli-version.d.ts | 2 +- .../opencode-agy-auth/package.json | 4 +- .../opencode-cursor-oauth/README.md | 94 - .../opencode-cursor-oauth/dist/auth.d.ts | 22 - .../opencode-cursor-oauth/dist/auth.js | 92 - .../opencode-cursor-oauth/dist/h2-bridge.mjs | 173 - .../opencode-cursor-oauth/dist/index.d.ts | 14 - .../opencode-cursor-oauth/dist/index.js | 229 - .../opencode-cursor-oauth/dist/models.d.ts | 10 - .../opencode-cursor-oauth/dist/models.js | 161 - .../opencode-cursor-oauth/dist/pkce.d.ts | 4 - .../opencode-cursor-oauth/dist/pkce.js | 9 - .../dist/proto/agent_pb.d.ts | 13022 ---------------- .../dist/proto/agent_pb.js | 3250 ---- .../opencode-cursor-oauth/dist/proxy.d.ts | 19 - .../opencode-cursor-oauth/dist/proxy.js | 1128 -- .../opencode-cursor-oauth/package.json | 55 - scripts/check-upstream-plugin-updates.mjs | 2 +- scripts/lib/upstream-plugin-registry.mjs | 4 +- scripts/lib/upstream-plugin-sanitization.mjs | 104 +- scripts/lib/upstream-plugin-specs.mjs | 12 +- tests/pricing-resolver.coverage.test.ts | 48 +- tests/upstream-plugin-issues.test.ts | 50 +- tests/upstream-plugin-lock.test.ts | 32 +- tests/upstream-plugin-registry.test.ts | 98 +- tests/upstream-plugin-review.test.ts | 32 +- tests/upstream-plugin-sanitization.test.ts | 219 +- tests/upstream-plugin-specs.test.ts | 28 +- tests/upstream-plugin-sync.test.ts | 83 +- tests/upstream.cursor-oauth.reference.test.ts | 60 - ...cursor-opencode-provider.reference.test.ts | 54 + 221 files changed, 30862 insertions(+), 18948 deletions(-) create mode 100644 references/upstream-plugins/cursor-opencode-provider/DISCLAIMER.md create mode 100644 references/upstream-plugins/cursor-opencode-provider/LICENSE create mode 100644 references/upstream-plugins/cursor-opencode-provider/README.md create mode 100644 references/upstream-plugins/cursor-opencode-provider/SECURITY.md create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/activity.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/activity.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/agent-url.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/agent-url.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/auth.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/auth.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/compaction-marker.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/compaction-marker.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/agents.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/agents.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/auth-store.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/auth-store.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/build.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/build.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/env.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/env.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/epoch.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/epoch.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/frozen.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/frozen.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/git.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/git.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/index.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/index.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/layout.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/layout.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/overlay.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/overlay.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/paths.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/paths.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/plugins.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/plugins.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/rules.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/rules.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/skills.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/context/skills.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/deadline.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/deadline.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/debug.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/debug.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/errors.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/errors.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/host-agent-mode.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/host-agent-mode.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/host-event-bridge.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/host-event-bridge.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/image-input.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/image-input.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/image-save-tool.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/image-save-tool.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/image-save.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/image-save.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/image-staging.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/image-staging.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/index.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/index.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/language-model.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/language-model.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/model-config.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/model-config.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/model-metadata.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/model-metadata.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/models.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/models.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/opencode2/catalog.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/opencode2/catalog.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/opencode2/integration.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/opencode2/integration.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/opencode2/todo-tools.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/opencode2/todo-tools.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/opencode2/types.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/opencode2/types.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plan-execution-kickoff.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plan-execution-kickoff.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plugin-core.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plugin-core.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plugin-opencode2.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plugin-opencode2.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plugin-v2.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plugin-v2.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plugin.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/plugin.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/pricing.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/pricing.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/apply-patch.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/apply-patch.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/ask-question.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/ask-question.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-reachability.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-reachability.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-store.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-store.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/checkpoint.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/checkpoint.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/checksum.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/checksum.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/client-version.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/client-version.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-bind.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-bind.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-persistence.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-persistence.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-state.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-state.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/create-plan.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/create-plan.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/device-id.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/device-id.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/exec-variants.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/exec-variants.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/framing.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/framing.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/generate-image.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/generate-image.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/git-diff.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/git-diff.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/interactions.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/interactions.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/kv.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/kv.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/messages.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/messages.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/progress-continuation.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/progress-continuation.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/request.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/request.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/stream.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/stream.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/struct.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/struct.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/switch-mode.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/switch-mode.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/thinking.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/thinking.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/token-details.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/token-details.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/tool-call-bridge.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/tool-call-bridge.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/tools.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/tools.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/workspace-grounding.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/protocol/workspace-grounding.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/replay-safety.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/replay-safety.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/session-directory.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/session-directory.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/session.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/session.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/shared.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/shared.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/shell-timeout.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/shell-timeout.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/todo-store.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/todo-store.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/transport/connect.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/transport/connect.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/transport/https-proxy.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/transport/https-proxy.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/usage.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/usage.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/web-search-tool.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/web-search-tool.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/web-tools.d.ts create mode 100644 references/upstream-plugins/cursor-opencode-provider/dist/web-tools.js create mode 100644 references/upstream-plugins/cursor-opencode-provider/package.json create mode 100644 references/upstream-plugins/opencode-agy-auth/LICENSE create mode 100644 references/upstream-plugins/opencode-agy-auth/dist/src/plugin/headers.d.ts create mode 100644 references/upstream-plugins/opencode-agy-auth/dist/src/plugin/tier.d.ts create mode 100644 references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-fetch.d.ts create mode 100644 references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-session.d.ts create mode 100644 references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-storage.d.ts create mode 100644 references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2.d.ts delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/README.md delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/auth.d.ts delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/auth.js delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/h2-bridge.mjs delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/index.d.ts delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/index.js delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/models.d.ts delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/models.js delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/pkce.d.ts delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/pkce.js delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/proto/agent_pb.d.ts delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/proto/agent_pb.js delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/proxy.d.ts delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/dist/proxy.js delete mode 100644 references/upstream-plugins/opencode-cursor-oauth/package.json delete mode 100644 tests/upstream.cursor-oauth.reference.test.ts create mode 100644 tests/upstream.cursor-opencode-provider.reference.test.ts diff --git a/references/upstream-plugins/README.md b/references/upstream-plugins/README.md index 0b38f3a9..64a991b3 100644 --- a/references/upstream-plugins/README.md +++ b/references/upstream-plugins/README.md @@ -4,9 +4,11 @@ This directory stores committed local copies of the published npm packages track Tracked packages: -- `@playwo/opencode-cursor-oauth` (tracked internally under `references/upstream-plugins/opencode-cursor-oauth/`) -- `opencode-gemini-auth` -- `@anthonyhaussman/opencode-agy-auth` (tracked internally under `references/upstream-plugins/opencode-agy-auth/`) +- `cursor-opencode-provider` (npm `latest`) +- `opencode-gemini-auth` (npm `latest`, the OpenCode 2 line since 2.x) +- `@anthonyhaussman/opencode-agy-auth` (npm `alpha`, the OpenCode 2 build; tracked internally under `references/upstream-plugins/opencode-agy-auth/`) + +Each package follows the npm dist-tag set in `scripts/lib/upstream-plugin-specs.mjs`. Companion identities are verified against npm package metadata and their canonical GitHub repositories. diff --git a/references/upstream-plugins/cursor-opencode-provider/DISCLAIMER.md b/references/upstream-plugins/cursor-opencode-provider/DISCLAIMER.md new file mode 100644 index 00000000..eaa11773 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/DISCLAIMER.md @@ -0,0 +1,9 @@ +# EU Interoperability Disclaimer + +This project is expressly designed to comply with European Union law on the legal protection of computer programs, in particular Directive 2009/24/EC of the European Parliament and of the Council of 23 April 2009 (“EU Software Directive”), as implemented in the relevant EU Member States. + +The project is architected on the basis that its reverse-engineering and interface-analysis activities are strictly limited to what EU law expressly permits: acts necessary to use a lawfully acquired program for its intended purpose, including error correction, and acts strictly necessary to obtain the information required to achieve interoperability between an independently created computer program and other programs, under Articles 5 and 6 of Directive 2009/24/EC and corresponding national provisions. It is not designed to copy or reproduce proprietary source code or non-essential implementation details of any third-party software, but to access and use only the minimum interface information objectively required to enable technical interoperability where such information is not otherwise readily available through official or documented means. + +When used as intended—namely, to enable lawful interoperability between independently created software components in accordance with Directive 2009/24/EC and applicable national law - this project is meant to operate in full compliance with those legal provisions. Any use that departs from this intended, interoperability-focused purpose (for example, to circumvent technical protections, replicate proprietary implementations, or violate third-party service terms) falls outside the scope of the project’s design and may not comply with applicable law; responsibility for such misuse lies solely with the user. + +This statement reflects the legal and compliance orientation of the project but does not replace tailored legal advice. diff --git a/references/upstream-plugins/cursor-opencode-provider/LICENSE b/references/upstream-plugins/cursor-opencode-provider/LICENSE new file mode 100644 index 00000000..0b173d5e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Oleksiy Akimov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/references/upstream-plugins/cursor-opencode-provider/README.md b/references/upstream-plugins/cursor-opencode-provider/README.md new file mode 100644 index 00000000..004a46d4 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/README.md @@ -0,0 +1,390 @@ +# cursor-opencode-provider + +Use [Cursor](https://cursor.com) subscription models from [OpenCode](https://opencode.ai) and compatible coding agents by speaking Cursor's Connect-RPC agent protocol. + +This project is a custom **AI SDK provider** (`LanguageModelV3`) plus an **OpenCode plugin** that handles authentication and model discovery. Instead of calling a generic chat-completions API, it encodes and decodes Cursor's protobuf agent protocol over HTTP/2 to Cursor's agent backend. + +> **Runs unchanged across OpenCode, OpenCode 2.0, and four additional coding agents.** OpenCode loads this plugin natively, including through the dedicated OpenCode 2.0 entrypoint. [OCP — OpenCode Plugin Compatibility](https://github.com/oakimov/opencode-plugin-compat) runs the same published plugin, without a provider fork, in **Kilo Code, MiMo Code, pi, and oh-my-pi**. See [Coding-agent compatibility](#coding-agent-compatibility). +> +> **Status:** Usable end-to-end for authentication, model discovery, streaming, and tools. See [Known limitations](#known-limitations). + +## Coding-agent compatibility + +This package remains one OpenCode plugin and AI SDK provider. The external +**[OCP compatibility layer](https://github.com/oakimov/opencode-plugin-compat)** +adapts other hosts around that unchanged package: it translates their plugin +surfaces, model catalogs, tool vocabulary, authentication, and streaming events +instead of requiring a host-specific Cursor provider. + +| Coding agent | How this package runs | +|---|---| +| **OpenCode** | Native plugin and AI SDK provider — [OpenCode 1.x setup](docs/opencode-1.md) | +| **OpenCode 2.0** | Native `cursor-opencode-provider/plugin/opencode2` — [OpenCode 2.0 setup](docs/opencode-2.md) | +| **Kilo Code** | Unchanged through OCP's OpenCode-clone compatibility layer | +| **MiMo Code** | Unchanged through OCP's OpenCode-clone compatibility layer | +| **pi** | Unchanged through `@opencode-compat/pi-bridge` and pi's provider extension API | +| **oh-my-pi (omp)** | Unchanged through the same Pi-family bridge | + +Start with OCP's host guides: + +- [OpenCode clones: Kilo Code and MiMo Code](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/opencode-clones.md) +- [Pi family: pi and oh-my-pi](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/pi-family.md) + +OCP is designed to grow by adding host profiles and narrow adapters or bridges, +so support for other coding agents can be added there while this provider and +its package remain unchanged. + +## Demo + +OpenCode driving a Cursor-routed Grok model through this provider: + +![OpenCode running a Grok model via cursor-opencode-provider](https://raw.githubusercontent.com/oakimov/cursor-opencode-provider/main/assets/opencode-grok.png) + +## Features + +- **Multi-host compatibility** — the same plugin package runs natively in OpenCode and OpenCode 2.0, and unchanged through OCP in Kilo Code, MiMo Code, pi, and oh-my-pi; new host support belongs in the compatibility layer rather than a provider fork +- **OpenCode integration** — registers a `cursor` provider with auth hooks and cached model list +- **Authentication** — browser OAuth (PKCE), or API key from [cursor.com/settings](https://cursor.com/settings) +- **Model discovery** — fetches available models from Cursor's API and caches them locally +- **Image input** — advertises vision only for supported models and forwards OpenCode image attachments to Cursor +- **Streaming** — bidirectional Connect-RPC Runs with stale-session rotation, health checks, semantic/read-idle deadlines, bounded replay-safe recovery, and activity-aware held tool continuations +- **Tool calls** — maps Cursor exec-server messages to AI SDK / OpenCode tool-call parts using only the advertised canonical catalog. Exact advertised subagent names win; standard Cursor roles select compatible OpenCode agents, and native Task maps to OpenCode 1.x `task` or OpenCode 2.0 `subagent`. OCP translates any alternate host vocabulary before it reaches this package. The bridge also covers Cursor's typed read/bash/edit/write/grep/find/ls request/result range, mirrors finalized display-only todo/plan state, and strips OpenCode 1.x and OpenCode 2 read envelopes before returning content to Cursor. +- **Thinking / reasoning** — surfaces extended-thinking deltas where the model supports it + +## Requirements + +- [Bun](https://bun.sh) (for development and tests) +- [OpenCode](https://opencode.ai), or a supported host configured through [OCP](https://github.com/oakimov/opencode-plugin-compat) +- An active Cursor account with API access + +## Installation + +This package is one plugin with host-specific entrypoints. Use the guide for your OpenCode major version: + +| Host | Guide | +|------|--------| +| **OpenCode 1.x** | [docs/opencode-1.md](docs/opencode-1.md) — classic `plugin` plus optional 1.18 `plugin/v2` | +| **OpenCode 2.0** | [docs/opencode-2.md](docs/opencode-2.md) — `plugin/opencode2` only, including [safe transition](docs/opencode-2.md#safe-transition) off `ctx.catalog` and the deprecated `opencode.json` catalog dump | +| **Other agents** | [Coding-agent compatibility](#coding-agent-compatibility) | + +Do not load the 1.x and 2.0 entrypoints in the same host. + +OpenCode 1.x (`~/.config/opencode/opencode.json`): + +```json +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["cursor-opencode-provider"] +} +``` + +OpenCode 2.0 (prefer a dedicated `OPENCODE_CONFIG_DIR`): + +```json +{ + "plugin": ["cursor-opencode-provider/plugin/opencode2"] +} +``` + +Local clones, 1.18 `plugin/v2`, config isolation, and leftover `providers.cursor` cleanup are in the host guides. Shared catalog behavior (variants, 1M, Fast, images, Max Mode) is under [Select a model](#select-a-model). + +```bash +git clone https://github.com/oakimov/cursor-opencode-provider.git +cd cursor-opencode-provider +bun install +bun run build +``` + +You can also install from npm (`npm install cursor-opencode-provider` or `bun add cursor-opencode-provider`). Pin a version in host config if you want, for example `cursor-opencode-provider@0.4.1`. + +## OpenCode setup + +Host config, auth commands, and local-clone wiring differ by major version — follow [OpenCode 1.x](docs/opencode-1.md) or [OpenCode 2.0](docs/opencode-2.md). + +### Authenticate + +| Host | How | +|------|-----| +| **OpenCode 1.x** | `opencode auth login` → **cursor** ([details](docs/opencode-1.md#authenticate)) | +| **OpenCode 2.0** | `/connect` → **Cursor** ([details](docs/opencode-2.md#authenticate)). `CURSOR_API_KEY` is also picked up automatically. | + +Then choose a method: + +| Method | Description | +|--------|-------------| +| **Cursor account (browser login)** | PKCE OAuth — opens cursor.com to sign in | +| **API key** | Paste a key from [cursor.com/settings](https://cursor.com/settings) (`crsr_...`) | + +After login, the plugin fetches your available models and writes them to `/cursor-models.json`. On later startups, a missing, empty, expired, or old-schema cache is refreshed during config load when Cursor auth is available; an existing stale cache remains usable if refresh fails. Completed conversation state is stored separately under `/cursor-conversations/`, with one independently replaced gzip-compressed protobuf snapshot per stable host session. Snapshots are refreshed at each successful Cursor `TurnEnded` and restored after provider restarts; records not updated for more than 24 hours are removed when the provider starts. Cursor's checkpoint is an index of content-addressed KV blobs rather than a self-contained transcript, so the snapshot retains the blobs reachable from the latest checkpoint and discards superseded blobs. This private Cursor state is not a copy of the host's session database. + +### Paths (host cache) + +Model/version caches and Cursor project metadata live under a **host cache root**, resolved in this order: + +1. Explicit `createCursor({ cacheDir })` / host API cache override +2. A structural host path bridge installed before the provider loads +3. Native OpenCode: `$XDG_CACHE_HOME/opencode`, otherwise `~/.cache/opencode` + +The provider never detects or names fork hosts. Compatibility layers own host identity and may inject cache, data, and config roots through the structural bridge; without one, standalone OpenCode behavior is unchanged. + +| Kind | Default (OpenCode) | Notes | +|------|--------------------|-------| +| Model, version, and 24-hour conversation **cache** | `~/.cache/opencode/` | A host bridge may replace the root | +| Cursor **project metadata** (`agent-tools`, terminals, …) | `~/.cache/opencode/projects//` | under `/projects/` | +| OpenCode **auth** (`auth.json`) | `~/.local/share/opencode/` | `$XDG_DATA_HOME/opencode/` when set | +| OpenCode **config** (AGENTS, skills, …) | `~/.config/opencode/` | still OpenCode-named for rule discovery | + +### Select a model + +Pick a model from the cached list (for example `composer-2.5`, `default`, or a Claude/GPT model exposed by your subscription): + +```bash +opencode run --model cursor/composer-2.5 "Hello from Cursor via OpenCode" +``` + +#### Variants + +Cursor models often expose parameterized variants (effort, thinking, fast, context tier, …). The plugin materializes those as OpenCode **model variants**. In the TUI, pick one from the variant dialog or cycle with OpenCode’s `variant_cycle` keybind (default `ctrl+t`). + +The selected variant’s Cursor parameter map is forwarded on the Run as `requested_model.parameters` (isolated under `providerOptions.cursor.cursorVariantParameters` so unrelated OpenCode options are not leaked onto the wire). The provider validates that explicit selection against the current cached tuple; malformed, reordered, or stale selections fail clearly instead of silently falling back to another variant. + +#### 1M / long context + +OpenCode’s context limit is static per model entry, while Cursor’s long-context tier is a variant parameter (`context=1m`). When a model has both a base tier and a `1m` tier, the plugin emits a separate OpenCode entry `-1m` (for example `claude-opus-4-8-1m`) with: + +- `limit.context` set to the 1M window (so overflow checks and compaction match the tier) +- `limit.output` set to `128000` (max generation tokens — not the context window; base entries use `32000`) +- only the long-context variants in its picker +- the real Cursor model id carried in `options.cursorModelId` (not `config.id`, which would make OpenCode merge base variants into the 1M entry) + +The Run still uses Cursor’s original model id; OpenCode’s synthetic `-1m` id is only for picking and limits. + +#### Fast variants with distinct prices + +When Cursor publishes a different Fast rate for a model (currently Composer and Grok), the plugin emits a separate OpenCode entry `-fast` (for example `composer-2.5-fast`) using the same pattern as `-1m`: + +- only the Fast variants in its picker, with `" Fast"` in the catalog name +- the real Cursor model id in `options.cursorModelId`, plus default Fast variant parameters +- its own `cost` row (`composer-2.5-fast` is not nested under `composer-2.5`) + +Claude Fast variants stay on the base entry: Cursor does not publish a separate Fast price for them, so they remain picker variants instead of a second catalog id. Combinations with long context use `-1m-fast`. + +#### Context limit sources + +The provider resolves each catalog context limit in this order: + +1. Live `AvailableModels` metadata (variant `context` parameters first, then context-limit fields) +2. Cursor’s published model table, generated from [`docs.md`](https://cursor.com/docs.md) alongside pricing data +3. A conservative `200000` fallback when neither source publishes a limit + +The docs fallback only fills static catalog metadata. It does not invent Cursor variants or enable a long-context tier that `AvailableModels` did not advertise. + +#### Image input + +The provider advertises `text` + `image` input only when the selected Cursor model supports images; model output remains text. This works in the classic plugin, the 1.18 v2 plugin, and OpenCode 2.0. + +Image support is resolved in this order: + +1. Live `AvailableModels.supportsImages` metadata, including authoritative `false` values +2. The `Capabilities` column in Cursor's [`docs.md`](https://cursor.com/docs.md), generated alongside context and pricing metadata +3. Text-only when neither source describes the model + +OpenCode image file parts are decoded from bytes, base64/data URLs, local file URLs, or HTTP(S) URLs and sent through Cursor's `UserMessage.selected_context.selected_images` field. On fresh or rebased Runs, the provider also harvests image `file-data` from tool results and `file` parts from assistant and historical user messages (the trailing user message stays owned by last-user extraction); previously sent history images are deduplicated by content hash per OpenCode session. Held-open continuation results remain text-only because Cursor's exec-result channel has no image field. Attachments are limited to 20 MiB total, matching OpenCode's desktop attachment budget. PDF, audio, and video inputs are not advertised or silently discarded. + +#### Max mode + +Cursor IDE has a separate **Max Mode** toggle that sets `requested_model.max_mode` and selects the default max / 1m variant. OpenCode has no equivalent custom toggle, so this provider approximates it: + +- Selecting a `*-1m` model (or any resolved params with `context=1m`) sets wire `max_mode` to `true` +- An explicit `providerOptions.cursor.maxMode` hint also turns it on + +There is no independent Max Mode chrome in OpenCode beyond choosing the 1M model / long-context variant. + +## Programmatic usage + +```ts +import { createCursor } from "cursor-opencode-provider" + +const cursor = createCursor({ + name: "cursor", + accessToken: process.env.CURSOR_ACCESS_TOKEN, + // apiBaseURL: "https://api2.cursor.sh", + // agentBaseURL: "https://agentn.us.api5.cursor.sh", // explicit Run host override + // telemetryEnabled: true, // opt in to GetServerConfig telemetry + // cacheDir: "/path/to/host/cache", // optional; else active-host heuristic + // retry: { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 8_000 }, + // continuation: { heartbeatMs: 5_000, semanticIdleMs: 120_000, hardCapMs: 600_000 }, +}) + +const model = cursor.languageModel("composer-2.5") +// model implements AI SDK LanguageModelV3 (doStream / doGenerate) +``` + +Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `crsr_...` key). Optional: `apiBaseURL`, `agentBaseURL`, `cacheDir`, `headers`, `telemetryEnabled`, `retry`, and `continuation`. `cacheDir` pins the host cache root for model/version caches and Cursor project metadata; when omitted, the provider uses an injected structural host path bridge or the native OpenCode XDG cache described in [Paths](#paths-host-cache). Transient failures resume from the latest checkpoint produced by that Run, matching Cursor CLI; without an eligible checkpoint, retries remain limited to replay-safe attempts so completed text or tool work is not duplicated. Once that budget is spent, or replay is unsafe, the terminal error does not re-arm OpenCode's outer retry loop. Pending-tool inactivity is renewed by OpenCode activity from the session or its descendants. The older `baseURL` option is still accepted as a legacy alias for `agentBaseURL`. + +## Environment variables + +| Variable | Description | +|----------|-------------| +| `CURSOR_WEBSITE_URL` | Override OAuth login base URL (default `https://cursor.com`) | +| `CURSOR_API_BASE_URL` | Override API base for auth, model discovery, and `GetServerConfig` agent URL resolution (default `https://api2.cursor.sh`) | +| `CURSOR_GET_SERVER_CONFIG_TELEMETRY` | Set to `1` or `true` to opt the `GetServerConfig` lookup into telemetry in OpenCode/plugin usage | +| `HTTPS_PROXY` / `https_proxy` | HTTP CONNECT proxy for HTTPS. Bun `fetch` uses this for unary RPCs; the Run HTTP/2 session also tunnels through it (see `NO_PROXY`). The Run tunnel supports only `http://` proxy URLs (Basic auth via userinfo); `https://` and SOCKS proxy URLs leave the Run on a direct connection. `HTTP_PROXY` is not used for HTTPS | +| `NO_PROXY` / `no_proxy` | Comma/space hosts that bypass the proxy (`*`, exact host, or `.suffix` / `suffix` domain forms; a `host:port` entry bypasses only that port) | +| `CURSOR_PROVIDER_DEBUG` | Set to `1` or `true` to enable wire-level debug logging | +| `CURSOR_PROVIDER_DEBUG_FILE` | Debug log path (default: `debug-.log` under `$TMPDIR/cursor-provider-logs-/`) | +| `CURSOR_OPENCODE2_DEV_ENTRY` | **Local OpenCode 2.0 only.** Absolute path to a built entry file (usually `dist/index.js`). Rewrites the AI SDK package to `aisdk:file://…` so the daemon imports your local build instead of `npm install`-ing the published package. Export it **before** `opencode2 service start`, then restart after rebuilds. Unset in production. See [OpenCode 2.0 local clone](docs/opencode-2.md#from-a-local-clone-cursor_opencode2_dev_entry). | +| `CURSOR_OPENCODE2_TODOS` | **OpenCode 2.0 only.** Set to `1` or `true` to register plugin-owned `todowrite`/`todoread` (in-memory; no TUI sidebar). Off by default. OpenCode 1.x still uses the host builtin. | +| `XDG_CACHE_HOME` | Base for native OpenCode cache (`$XDG_CACHE_HOME/opencode/`) when no explicit `cacheDir` or structural host path bridge is installed | +| `XDG_DATA_HOME` | When set, OpenCode `auth.json` is read from `$XDG_DATA_HOME/opencode/` instead of `~/.local/share/opencode/` | + +`createCursor({ agentBaseURL })` overrides the agent Run host. When unset, the provider resolves the host from Cursor's `GetServerConfig` API (`agentUrlConfig.agentnUrl`, region-specific — e.g. `agentn.us.api5.cursor.sh`, `agent-gcpp-uswest.api5.cursor.sh`) once per process and holds it in memory (never written to disk), so a held-open Run stream is never repointed mid-session. Explicit agent overrides and GetServerConfig results are validated as HTTPS `*.cursor.sh` hosts (Cursor's agent hostnames vary and may change); non-`cursor.sh` hosts are rejected. Shared HTTP/2 connections are rotated before they become server-aged, while existing Runs may finish on their original connection. The lookup sends `{ "telem_enabled": false }` by default; set `telemetryEnabled: true` in provider config, or `CURSOR_GET_SERVER_CONFIG_TELEMETRY=1` for OpenCode/plugin usage, to opt in. If the lookup fails or does not return a valid Cursor agent host, the model call fails clearly instead of falling back to `agentn.global.api5.cursor.sh`. + +## Development + +```bash +bun install # install dependencies +bun run build # compile TypeScript → dist/ +bun run typecheck # type-check without emit +bun test # run unit tests +bun run test:node-http2 # Node-specific HTTP/2 detach regression +bun run test:watch # watch mode +bun run generate:pricing # refresh src/pricing-data.ts from Cursor docs +bun run check:pricing # fixture coverage for known model ids +``` + +**HARD STOP before any version bump or `v*` tag:** run `generate:pricing` and `check:pricing` in the same session, fix any unmapped Cursor display names, and commit mapping/`pricing-data.ts` updates **before** touching `"version"` or creating the tag. Publish CI regenerates rates from the live docs and will fail the release on an unmapped name — that is a last line of defense, not the primary check. See `AGENTS.md`. + +## Architecture + +``` +OpenCode + └── CursorPlugin (auth, model cache, config hook) + └── createCursor() → LanguageModelV3 + ├── session.ts held-open Run stream + exec bridge + ├── protocol/ protobuf messages, framing, tools, thinking + └── transport/ Connect-RPC over HTTP/2 to Cursor's agent backend +``` + +| Module | Role | +|--------|------| +| `src/plugin.ts` | Classic OpenCode hooks: provider registration, OAuth, API key exchange, token refresh | +| `src/plugin-v2.ts` | OpenCode 1.18 Effect/Promise v2 plugin (`ctx.aisdk.*`); load via `./plugin/v2` only | +| `src/plugin-opencode2.ts` | OpenCode 2.0 plugin (`provider.transform`, integration, tools, aisdk, `shell.create.before`, `websearch`, plan kickoff); load via `./plugin/opencode2` or `./server` | +| `src/opencode2/` | 2.0-only provider inventory, integration/auth, and local API types | +| `src/plugin-core.ts` | Host-neutral SDK factory, package matching, API base/telemetry resolution | +| `src/model-config.ts` | Cursor model → OpenCode model mapping shared by every plugin surface | +| `src/image-input.ts` | AI SDK image attachment validation, decoding, and size limits | +| `src/pricing.ts` / `src/pricing-data.ts` | Cursor docs token rates → classic `cost` and OpenCode 2.0 cost tiers | +| `src/index.ts` | `createCursor` factory; default export is `CursorPlugin` | +| `src/language-model.ts` | AI SDK `LanguageModelV3` adapter (`doStream`, `doGenerate`) | +| `src/session.ts` | Held-open agent Run session and pending exec correlation | +| `src/debug.ts` | Opt-in wire-level debug logging (`CURSOR_PROVIDER_DEBUG`) | +| `src/auth.ts` | PKCE OAuth, API key exchange, JWT refresh | +| `src/models.ts` | `AvailableModels` fetch and `cursor-models.json` cache | +| `src/protocol/conversation-persistence.ts` | Atomic per-session restart snapshots under the 24-hour `cursor-conversations/` cache | +| `src/context/paths.ts` | Host cache root + Cursor project metadata under `/projects//` | +| `src/agent-url.ts` | `GetServerConfig` fetch + in-process memo (region-specific Run host) | +| `src/transport/connect.ts` | HTTP/2 bidi stream and unary RPC calls | +| `src/transport/https-proxy.ts` | `HTTPS_PROXY` / `NO_PROXY` CONNECT tunnel for Run sessions | +| `src/protocol/` | Protobuf encode/decode, checksum/device ids, exec + display tool-call mapping (`tool-call-bridge.ts`) | + +### Injected system guidance + +The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, preferring OpenCode `todowrite` / `todoread` over Cursor TodoWrite when those tools are advertised without inviting catalog-comparison narration, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged. + +Native Cursor todo display updates (`update_todos_tool_call` / `read_todos_tool_call`) are mirrored into `todowrite` / `todoread` when those host tools are advertised. OpenCode 2 dropped the host builtins; set `CURSOR_OPENCODE2_TODOS=1` (or `true`) to register the same canonical names when the catalog omits them (in-memory per session, cleared on `session.deleted` and process restart). With the flag unset, the plugin registers no todo tools. Enabled plugin tools are **direct** catalog tools (`options.codemode: false` plus an `output` schema), not Code Mode helpers. The provider retains the last known full list per OpenCode session — refreshed by bridged writes, direct host `todowrite` calls, and `todoread` results, surviving Run close, checkpoint resume, and later turns — so `merge: true` updates that omit Cursor's completed list still become a host replace-all snapshot (matched by id, then by content across id spaces). This snapshot is in-memory only and never enters the prompt, RequestContext, or persisted restart state, so Cursor's prompt-cache prefix is unaffected; a provider process restart clears it until the next write/read (intentional — see `tasks/lessons.md`). + +If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`src/language-model.ts`](src/language-model.ts) and its focused coverage in [`test/prompt-history.test.ts`](test/prompt-history.test.ts). + +### Cursor edit handshakes + +Cursor's legacy `edit_tool_call` is implemented internally as a correlated `read_args` followed by a whole-file `write_args`; the write is transport mechanics, not a new decision to replace OpenCode's `edit` tool. For regular files inside the workspace, the provider answers that private prerequisite read directly with the complete file (up to Cursor's own 50 MB edit limit), because routing it through OpenCode's ordinary 50 KB-capped `read` tool makes Cursor calculate a truncated replacement. This shortcut is restricted to the exact path named by the active edit call and rejects symlink escapes. External paths first take OpenCode's permission-aware read path; after that exact read succeeds, the provider upgrades its private result to complete content so an explicitly authorized external edit does not inherit OpenCode's preview cap. + +The provider retains the correlation and exposes the resulting existing-file mutation as one unique, line-bounded OpenCode `edit`. It still returns Cursor's expected typed `write_result`. New-file creation remains a `write`, and an `apply_patch`-only catalog receives the same targeted change as an `*** Update File:` patch. + +Ordinary complete native reads also preserve an existing final LF/CRLF. OpenCode's numbered read envelope cannot represent that terminator, and dropping it made exact edit matching fail. This restoration applies only to confirmed-complete, unbounded reads; paged and capped reads keep their existing truncation behavior. + +### `apply_patch` models (GPT-5 and friends) + +OpenCode 1.x does not simply add `apply_patch` for GPT-series models — it **removes** `edit` and `write` from the tool catalog and advertises `apply_patch` in their place (`ToolRegistry.tools`; the model id must contain `gpt-` and neither `oss` nor `gpt-4`). Cursor keeps sending its native write/edit requests regardless, so without translation every file change on a `gpt-5*` model is refused as an unavailable tool. + +The provider translates transparently: an uncorrelated native write becomes an `*** Add File:` patch, while a correlated legacy edit or Pi edit becomes a minimal `*** Update File:` chunk widened to whole lines. This is keyed purely off what the host advertises — it is inert whenever `edit`/`write` are offered normally, and inert again when the host offers neither those nor `apply_patch`. An edit that cannot be expressed faithfully (target unreadable, or the text to replace is absent or ambiguous) is refused with a specific message rather than applied to the wrong region. + +OpenCode 2.0 does not perform this substitution, so the path is 1.x-only in practice. See [`src/protocol/apply-patch.ts`](src/protocol/apply-patch.ts) for the upstream references. + +### Native subagent routing + +The provider reads the permission-filtered subagent catalog from the current host `task` or `subagent` definition and advertises those recipients to Cursor as custom subagents. Native Cursor Task exec remaps onto `task` when advertised, otherwise onto OpenCode 2 `subagent` (`agent` / `sessionID` / `background`). Exact configured names are preserved; unknown future Cursor subtype strings fall back to `general`. If a complete catalog omits every compatible recipient, the request fails on Cursor's typed subagent channel instead of selecting an arbitrary specialist. + +| Host | Executor | Recipients | +|------|----------|------------| +| OpenCode 1.x | `task` | Permission-filtered builtins such as `general` / `explore`, plus configured subagents | +| OpenCode 2.0 | `subagent` | The permission-filtered names listed by the host's tool definition | + +Primary modes and hidden/internal agents are never selected unless OpenCode explicitly exposes them as spawnable. Alternate-host agents and schemas are OCP responsibilities; see its host guides under [Coding-agent compatibility](#coding-agent-compatibility). + +## Package exports + +| Import path | Export | +|-------------|--------| +| `cursor-opencode-provider` | `createCursor`, `CursorPlugin` (named + default) | +| `cursor-opencode-provider/plugin` | `CursorPlugin` (classic Hooks — auth) | +| `cursor-opencode-provider/plugin/v2` | OpenCode 1.18 Effect/Promise v2 plugin (`ctx.aisdk.*`) | +| `cursor-opencode-provider/plugin/opencode2` | OpenCode 2.0 plugin (self-registering: models + auth + tools) | +| `cursor-opencode-provider/server` | Same OpenCode 2.0 entry; OpenCode 2 Host.resolve and OpenCode 1.18 `exports["./server"]` | +| `cursor-opencode-provider/errors` | Structured provider error classes | +| `cursor-opencode-provider/image-save` | Host-neutral `executeCursorImageSave` (pi-bridge / non-plugin hosts) | + +The package root intentionally stays plugin-safe for OpenCode's classic loader. `CursorPluginV2` and non-plugin runtime APIs are **not** re-exported from the package root; load them through their dedicated subpaths. + +## Troubleshooting + +| Problem | What to try | +|---------|-------------| +| No Cursor models in the picker (OpenCode 1.x) | Confirm Cursor auth (`opencode auth login` → **cursor**). Restart OpenCode — if auth is present and the cache is empty, models are fetched on startup. Confirm `provider.cursor.npm` is the package name (or a built `file://…/dist/index.js`). See [OpenCode 1.x troubleshooting](docs/opencode-1.md#troubleshooting). | +| No Cursor models in the picker (`opencode2`) | Confirm `/connect` → **Cursor** (or shared `auth.json`). Use a dedicated `OPENCODE_CONFIG_DIR` (not mixed with 1.x). Ensure `$OPENCODE_CONFIG_DIR/plugins/cursor/` is a package directory re-exporting `plugin/opencode2` (not a bare `.js`). After auth, Cursor models appear in the picker from the in-memory inventory — no `opencode.json` model list is required. Filter by provider **Cursor** (`time.released` is `0`, so models sort last). Leftover `providers.cursor` fights that inventory — [safe transition](docs/opencode-2.md#safe-transition). | +| Auth / 401 errors mid-session | Re-login. OAuth and exchanged API-key JWTs refresh automatically when near expiry; a revoked refresh token needs a fresh login. | +| Local OpenCode 2.0 still runs the published package | Set `CURSOR_OPENCODE2_DEV_ENTRY` to an absolute `…/dist/index.js` path **before** starting the daemon, persist it with `opencode2 service set env CURSOR_OPENCODE2_DEV_ENTRY …`, rebuild (`bun run build`), then `opencode2 service restart`. Loading only `dist/plugin-opencode2.js` is not enough — without the env var, 2.0 still `npm install`s the published package into the host cache. | +| “Too many connections from different devices” | Device IDs are derived from stable OS identifiers (same approach as the Cursor CLI). Avoid running multiple clients that invent different machine fingerprints for the same account. | +| Empty or stale model list | Delete `/cursor-models.json` (native default `~/.cache/opencode/`) and restart OpenCode. A compatibility layer may supply a different host-cache root through the structural path bridge. Existing Cursor auth is enough to refill the cache; re-login only if auth itself is broken. Cache TTL is 24h; a failed background refresh keeps serving the previous cache. | +| Stream hangs or HTTP/2 errors | The provider keeps Cursor's Run open across OpenCode tool calls, rotates aged shared connections, resumes transient interruptions from the latest eligible Cursor checkpoint, and falls back to a fresh-history rebase only before stateful output when no checkpoint exists. Repeated interruption is surfaced as an error instead of a false successful stop; retry the turn after checking connectivity. Behind an HTTP proxy, set `HTTPS_PROXY` (and `NO_PROXY` as needed): unary `fetch` and the Run HTTP/2 CONNECT tunnel both honor them. With debug logging enabled, look for `Run interrupted`, `resuming … checkpoint`, `rebasing fresh Run`, or `h2 connect via HTTPS proxy`. Restart OpenCode after rebuilding a local `file://` install. | +| No response / silent 200 + close | HTTP 200 alone is not a successful agent turn: the provider now requires Cursor's explicit `turn_ended`, captures HTTP/2 trailers/GOAWAY, and recovers once from bare EOF. The Run host still comes from in-memory `GetServerConfig` resolution; set `CURSOR_PROVIDER_DEBUG=1` to confirm the host and termination reason. | +| Visible `` timeout text | Rebuild and restart a local install. Cursor's shell timeout is carried on its exec request; the provider now removes OpenCode's internal timeout envelope before it is rendered or stored, then returns Cursor's typed timeout or background-handoff event instead of treating the text as successful stdout. | +| `Unsupported Cursor exec variant …` | The error names the canonical Cursor CLI request field, its expected result field, and this provider's handling classification. Known `handling=unsupported` variants (Cursor-native capabilities without a safe OpenCode AI SDK bridge) are now soft-denied on the wire with a populated typed error/`rejected`/`allowlisted:false` result and the Run continues — look for `exec: SOFT-DENIED …` in the debug log and a `Workspace root: …` suffix in the deny text — while `unknown request field` still hard-fails as protocol drift; `handling=opencode-tool` or `provider-control` indicates a provider decoder/dispatch regression. Enable the debug log and report the full named error. | +| Need wire-level logs | Set `CURSOR_PROVIDER_DEBUG=1` (optional `CURSOR_PROVIDER_DEBUG_FILE`; the default is `debug-.log` under `$TMPDIR/cursor-provider-logs-/`) and reproduce the issue. Every completed turn emits a `turn usage validation: status=ok|mismatch` line confirming token-accounting integrity and a `cache diagnosis:` line showing warm/cold checkpoint continuity, raw read/write/uncached tokens, cache-read coverage of the prior context, context/category deltas, stable RequestContext hashes, and Run step/tool counts. Checkpoint lines include Cursor's current category totals. Cursor does not expose per-model-call cache splits, so the diagnostic labels that value unavailable instead of inferring it from aggregate `TurnEnded` counters. | +| OpenCode token totals stay stuck until the turn ends | OpenCode TUI/GUI display the last assistant message with non-zero output, they do not sum occupancy. Tool-call steps emit Cursor's last-known checkpoint occupancy (with `output > 0` so the footer accepts it) and `providerMetadata.copilot.totalNanoAiu: 0` so those snapshots are not billed. The billed occupancy is emitted once at `TurnEnded`. If a checkpoint has not arrived yet, the previous turn's occupancy stays visible as stale. With no known snapshot, usage stays zero instead of mislabeling estimates or `TurnEnded` as context. Raw request counters remain in `providerMetadata.cursor`. | +| Context percentage jumps after a long tool turn or compaction | Cursor reports its authoritative context snapshot in checkpoint `token_details`. The provider makes AI SDK input + output equal `usedTokens`, proportionally partitions input into cached/uncached subsets, and exposes `maxTokens`, percentage, and category breakdown under `providerMetadata.cursor.context`. Mid-turn tool steps publish that occupancy as a display-only snapshot; compaction naturally replaces the prior snapshot with Cursor's smaller total. | +| Cache **write** tokens always show as `0` | Cursor's `TurnEnded` protobuf includes `cache_write` (field 4), but captured Runs set it to `0` even when `cache_read` is large. The provider forwards the request-local value when Cursor supplies one; it cannot infer a missing write count. With debug logging, confirm `turn_ended raw wire fields: … f4:wt0=0`. | + +## Security + +Project `instructions` may reference absolute or `~/` paths (OpenCode parity). See [SECURITY.md](./SECURITY.md) for the trust model and `OPENCODE_DISABLE_PROJECT_CONFIG`. + +## Known limitations + +- **Personal use / ToS** — this provider speaks Cursor’s private agent protocol (CLI-shaped client identity). Use only with an account you own; Cursor may change or restrict the API without notice. +- **Cursor's checkpoint controls displayed totals** — live continuation captures show `TurnEnded` counters can decrease between turns and can represent aggregate model work rather than final context occupancy. When a checkpoint contains `ConversationStateStructure.token_details`, the provider makes AI SDK input + output equal Cursor's `used_tokens`; at `TurnEnded`, output/reasoning remain based on `TurnEnded`, while uncached/cache-read/cache-write input are proportionally normalized to the remaining context input. OpenCode TUI/GUI replace each assistant message's tokens (TUI requires `tokens.output > 0`) and add cost per step, so mid-turn tool boundaries emit the last-known occupancy snapshot rather than a delta or a billed zero. Those snapshots set `output=1` so the footer updates and `providerMetadata.copilot.totalNanoAiu: 0` so OpenCode's getUsage reports $0 instead of charging occupancy as a new prompt. A Run without fresh token details retains the previous checkpoint snapshot and marks `providerMetadata.cursor.context` as `source: "checkpoint-previous-turn", stale: true`. If no checkpoint has ever supplied token details, standard usage is zero and context metadata is absent, so `TurnEnded` is never misrepresented as occupancy; its exact values remain under `providerMetadata.cursor.*Raw`. The full known snapshot includes `usedTokens`, `maxTokens`, percentage, category breakdown, source, and staleness. Cursor consistently sends `cache_write=0` in captured Runs, so no alternate write count is available. +- **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` built from OpenCode project context (workspace env, `AGENTS.md` / `instructions`, `.opencode` agents/skills/plugins, git, layout, plus `.claude`/`.agents` skill fallbacks). Expensive workspace state (rules, environment, repository, git, and layout) is frozen per Cursor conversation and persisted for restart recovery. Tools/MCP descriptors, skills, subagents, and plugin metadata are rediscovered each Run then epoch-held (equal ids keep frozen bytes; new ids append); unchanged overlays reuse byte-identical encoded context. Concurrent first Runs share one base build, and compaction/reset clears it. Its canonical root is also used by the [injected system guidance](#injected-system-guidance). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. Cursor-only cloud/sandbox marketplace surfaces are omitted. `env.workspace_paths` / `process_working_directory` stay on the real git workspace; Cursor's metadata root (`project_folder`, MCP `workspace_project_dir`, terminals/transcripts) is advertised under `/projects//` (default `~/.cache/opencode/projects/…`) so dumps like `agent-tools/` do not land in the repo. OpenCode remains the permission authority: its coarse allow/ask/deny configuration is not fabricated into Cursor's unrelated allow/block instruction-list messages. +- **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped into that server's `mcp_descriptors` / `provider_identifier` (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Cursor's MCP-state exec probe is answered from the same advertised descriptors before the actual tool request, using the full canonical tool-definition identity required by native `get_mcp_tools` / GetDynamicTools; exec still reconstructs the full OpenCode tool id. That discovery call is Cursor-native (not an OpenCode catalog tool). Oversized catalogs spill through `write_args` into `agent-tools/.txt`; `WriteSuccess.path` echoes the requested path so the model receives a real `filePath` instead of an empty spill. +- **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` → `todowrite`, `read_todos_tool_call` → `todoread`, `create_plan_tool_call` → `todowrite`); the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. All 37 Cursor CLI exec request/result pairs are inventoried by field and name. Known-but-unsupported variants are soft-denied on the held Run with a populated typed error/`rejected`/`allowlisted:false` result (or `throw` when the CLI has no typed error shape) so the turn continues; unknown future request fields still fail the Run explicitly rather than receiving a guessed response that could deadlock it. +- **Progress-only fragments can get one continuation** — if a real tool-enabled turn ends with a short progress fragment ("Checking the workspace", "Looking into it now") and no host tool call, the provider opens one follow-up Run with a synthetic user nudge so the model can call a listed tool or finish the answer. Complete answers that merely start with those verbs are not continued. Title/compaction turns (`allowTools=false`) never continue. If the continuation fails, the original `turn_ended` is still reported as a successful stop. +- **Tool availability is per OpenCode agent** — Cursor can request native capabilities such as Task even when a child or restricted OpenCode agent did not advertise the corresponding host tool. The provider prompts Cursor with the exact current catalog and checks every decoded host-tool exec request against it. An unavailable request is answered on Cursor's correlated typed result channel and is never emitted as OpenCode's `invalid` tool. +- **Host web tools use collision-safe aliases** — Cursor sees `custom_websearch` / `custom_webfetch`, and the held Run maps each alias back to an executable OpenCode tool with its schema, permission check, and correlated result intact. OpenCode 2 ships `websearch` as a direct, permission-gated catalog tool; this plugin supplies it an Exa provider through `ctx.websearch.transform` and aliases the host tool for Cursor. It does not add a second direct fallback because OpenCode 2's public plugin tool context cannot raise the host permission prompt. Classic 1.x still registers `custom_websearch` through its permission-aware tool API to avoid OpenCode's reserved `websearch` id filter. Exact host `webfetch` is translated the same way. Cursor's UI-bound native web interactions stay disabled because their approval replies cannot carry OpenCode tool results. +- **MCP resource tools (`list_mcp_resources` / `read_mcp_resource`) also use collision-safe aliases, and Cursor's native resource exec is always answered** — OpenCode's resource tools (advertised only when a connected MCP server declares `capabilities.resources`) share bare names with two of Cursor's own native tool-call variants. Left unaliased, Cursor's client can route a call onto its native exec channel instead of the ordinary MCP tool path, which this provider does not translate — confirmed live: reading a resource was dispatched natively and killed the Run before this fix. The provider now aliases both names to `custom_list_mcp_resources` / `custom_read_mcp_resource` (same mechanism as web search/fetch) so normal calls execute through OpenCode's tools, and separately answers Cursor's native resource-exec fields directly on the protocol floor — an empty list, or a "server not found" error for read — so an unsolicited native request from Cursor can never fail the Run. Downloads (`download_path`) have no OpenCode equivalent and are always refused; binary resource content is subject to the same [50 KB / attachment-type limits](#known-limitations) as other tool results. +- **Background shells are non-interactive** — Cursor's native background-shell spawn and soft-background shell-stream timeouts are bridged through OpenCode's foreground-only `bash` tool. With bash/zsh, the classic plugin keeps the original permission/UI command and executes the wrapper through `shell.env` (`BASH_ENV` / `ZDOTDIR`); OpenCode's non-interactive sh/dash argv ignores those startup variables, so the plugin uses a short `exec /bin/sh ''` command backed by the same private wrapper. Native background-spawn requests also carry a self-contained marker-producing fallback when the classic hooks are absent. Spawn/soft-bg wrappers detach with `nohup`, redirect output under `${TMPDIR:-/tmp}/cursor-opencode-{bg,shell}.*`, and return the real PID (or typed timeout/exit) to Cursor. Private markers and OpenCode's `` envelope are stripped before storage/render, with a short still-running / started / timed-out status line left for the bash bubble. Requests that require `write_shell_stdin` are rejected explicitly because OpenCode does not expose an interactive background-process lifecycle through its AI SDK tool interface. The POSIX wrap path is not implemented for native Windows PowerShell/`cmd`. +- **Cursor-native AskQuestion reaches you through OpenCode's question tool** — when Cursor asks a clarifying question, the provider translates the interaction into the host `question` tool, so the prompt appears in OpenCode with its real options and your answer is mapped back to Cursor's option ids (an answer you type yourself becomes Cursor's freeform text). Questions Cursor marks as non-blocking are acknowledged immediately and answered later, matching Cursor CLI. If the current agent does not offer `question` — most subagents deny it, and compaction turns have no tools — the request is declined with that reason and the model asks in its reply instead. +- **Cursor-native SwitchMode follows each OpenCode generation's planning model** — on OpenCode 1.x, advertised `plan_enter` / `plan_exit` tools remain authoritative and Cursor waits for their approve/reject result. OpenCode 2.0 replaced those tools with vendor-maintained primary `plan` / `build` agents, so its plugin applies an approved Cursor mode change through `session.switchAgent` after the owning Run is terminal; leaving plan still uses the advertised `question` gate when no `plan_exit` tool exists. Direct host UI agent switches are also observed. Any real host-agent or stable system-prompt change rotates and reseeds the Cursor conversation instead of resuming an incompatible checkpoint, while title/generation lifecycle prompts remain ephemeral and do not evict the primary cache. Provider-owned Cursor reminders remain the fallback when a host cannot switch agents. +- **Cursor image generation saves through OpenCode's permission where the host API exposes it** — Cursor generates the image on its servers and then writes it like any other file. On the classic plugin/OCP surfaces, the provider holds raw bytes and asks for the same permissions as a normal write (`external_directory`, then `edit`) before saving byte-for-byte. OpenCode 2.0's public plugin `ToolContext` does not expose a permission-request method, so its entrypoint deliberately does not advertise `cursor_image_save`; generation and binary writes are refused before staging instead of bypassing permissions or exposing an always-failing tool. By default supported-host images land in the Cursor project folder under `assets/`, kept out of the git tree as agent artifacts. Editing an existing image with `reference_image_paths` is not supported yet. The classic implementation is adapted from [`jkalasas/opencode-antigravity-image`](https://github.com/jkalasas/opencode-antigravity-image) (MIT) by jkalasas. +- **Cursor-native CreatePlan writes a host plan file, then asks before executing it** — when Cursor raises `create_plan_request_query`, the provider writes plain markdown to your host's own plans directory (`/plans/`, never your repository and never Cursor's `~/.cursor/plans`), with no Cursor YAML frontmatter. Writing the plan does not ask; running it does. If you are in plan mode, the provider prints the plan into the conversation — Cursor sends it over a side channel that would otherwise never display it — and then asks *"Plan at <path> is complete. Would you like to switch to the build agent and start implementing?"* — the same prompt OpenCode's own `plan_exit` uses — and keeps Cursor waiting for your answer. Answer **Yes** and the plan is reported as approved and implementation begins; answer **No** (or dismiss it) and Cursor is told the plan was not accepted, so the model refines it and proposes again instead of starting work. Hosts that advertise their own plan-staging tool run their native review UI instead, and it reports the same approve/refine outcome. Empty args still get the CLI empty-`plan_uri` success ack. Display `create_plan_tool_call` continues to mirror into `todowrite` separately. +- **Other Cursor-native interaction queries remain headless** — remaining UI/approval *queries* (as distinct from display tool calls and the AskQuestion / SwitchMode / CreatePlan / GenerateImage bridges) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects available web capabilities to executable host tools (`custom_websearch`, `custom_webfetch`); native web/PR/MCP/SCM requests are declined so they remain behind host tool permissions. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream. +- **Compaction resets Cursor conversation state without resetting stable workspace context** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool outputs as OpenCode-host observations in the seed history, and re-advertises the session's last tool catalog while refusing execution during the summary itself. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Across both id rotations, the frozen workspace/rules/environment base and its prior encoded-context comparison seed are transferred instead of rebuilt; tools, skills, agents, plugins, and MCP state are still rediscovered, and the complete context is reused only when its bytes remain identical. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state. Debug logs include system-prompt and encoded RequestContext hashes for cache-prefix verification. +- **Conversation bindings survive provider restarts** — after each successful `TurnEnded`, the OpenCode-session binding, latest checkpoint, checkpoint-reachable KV blob graph, frozen request-context base, last real tool catalog, host agent, stable system-prompt hash, and pending post-compaction rebase marker are atomically replaced in that session's private `/cursor-conversations/*.pb.gz` snapshot. The protobuf stores checkpoints, blob ids/data, and request context as bytes without JSON/base64 expansion, then gzip compresses the complete record. A checkpoint alone is insufficient: Cursor requests its referenced hashes through `get_blob` when the next Run starts. Reachability follows Cursor CLI's export traversal (turns, messages, steps, images, summaries, todos, prompts, and nested subagents), pruning superseded blobs; decode failure or a missing referenced hash conservatively retains the complete blob set. The stable OpenCode session id selects the file; Cursor conversation ids may rotate during compaction, agent switches, or prompt changes and remain inside the snapshot. Independent files prevent concurrent OpenCode processes from losing unrelated sessions by replacing a shared aggregate cache. Startup restores records updated within 24 hours and safely flushes older ones without deleting a concurrently published fresh replacement. Experimental legacy JSON snapshots are discarded rather than migrated. Debug logs report hydration as `restored`, `missing`, `invalid`, or `expired`, plus the compressed and protobuf snapshot sizes. The restored catalog is a fallback only for lifecycle turns such as compaction; an ordinary restricted/no-tool turn never resurrects it. Process-global bindings and prompt/catalog state still use a 256-session LRU bound; memory eviction drops the live checkpoint, blobs, and frozen context, which can be rehydrated from the restart cache on later use. +- **Oversized / incomplete checkpoint graphs warn but do not remint** — before reusing a checkpoint, the provider measures its reachable KV graph. State above 100 MiB (Cursor CLI export/transfer budget) or an incomplete graph is logged (`action=warm-reuse`) and the sticky conversation is kept, matching CLI soft-reuse. KV, request-context, MCP-state, interaction, and heartbeat writes are ordered per Run and await HTTP/2 drain, so a large blob reply cannot create an unbounded queue later misreported as heartbeat backpressure. Debug logs distinguish checkpoint-envelope bytes, reachable blob bytes, seed size, and encoded Run-request estimate; raw blob bytes are never presented as tokens. +- **Interrupted Runs resume from checkpoints** — a remote EOF, Connect end-stream, or trailer error is never emitted as a successful `stop`. When the failed Run produced an eligible checkpoint, the provider opens a new RPC for the same conversation and sends that state with `ResumeAction`, so completed text and tool work are not replayed. Before any stateful output, an interruption without a checkpoint can still rebase from OpenCode history. Stateful interruptions without a checkpoint are surfaced because replay would be ambiguous; retry exhaustion remains explicit. A transport closure after `turn_ended` is treated as successful completion. +- **Large reads are capped by OpenCode, and the cap is reported** — OpenCode 1.x `read` stops at `MAX_BYTES = 50 * 1024` and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. OpenCode 2 prints `Read file , lines -` with `N: ` prefixes and, when the page ends before EOF, `[Output truncated. Continue reading with offset: N]` (50 KB or 2,000 lines); it also shortens each individual line after 2,000 characters. The provider strips either envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states every partial result: a native read appends a `[Partial read: …]` marker after the content, an MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. Relative grep, glob, and directory entries are resolved against the workspace root. Shell stdout absolutizes only relative path tokens, not prose. Checkpointed turns, which do not resend the system prompt, get that same root on the user message. The structured `truncated` flag alone is not enough — verified against live `gpt-5.4-mini` and `grok-4.5` sessions, both of which asserted "highly confident" that partial content was the whole file until the textual marker was added. Deliberately paged reads (explicit `offset`/`limit`) are not marked unless the host also hits the byte cap or character-truncates a line. Whole-file writes and overwriting `Add File` patches that echo the marker are rejected before OpenCode executes them; targeted edits remain possible, including edits to source that quotes the warning text. Cursor's private legacy-edit read is handled separately as described above, so an edit can operate on a workspace file larger than 50 KB without turning the capped preview into a replacement. The cap is hardcoded in OpenCode's read tool — it is not the configurable `tool_output.max_bytes`, which governs `Truncate.Service` and not read's line accumulation — so ordinary model reads still require paging with `offset`. +- **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models. + +## License + +MIT diff --git a/references/upstream-plugins/cursor-opencode-provider/SECURITY.md b/references/upstream-plugins/cursor-opencode-provider/SECURITY.md new file mode 100644 index 00000000..10173400 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/SECURITY.md @@ -0,0 +1,42 @@ +# Security notes + +This provider mirrors OpenCode’s trust model for project configuration. That has implications for what ends up in Cursor context. + +## Project `instructions` can include arbitrary local paths + +OpenCode’s `opencode.json` / `opencode.jsonc` `instructions` array may list: + +- relative paths / globs (e.g. `.cursor/rules/*.md`) +- absolute paths +- home-relative paths (`~/…`) +- remote URLs (`https://…`; this provider fetches HTTPS only) + +OpenCode itself expands `~/`, accepts absolute paths, and injects those file contents into the model prompt for **every** provider. This package does the same discovery when building Cursor `RequestContext.rules`. + +So a project config like: + +```json +{ + "instructions": ["~/.ssh/id_rsa", "/etc/passwd"] +} +``` + +can cause those files to be read and sent to the model provider (OpenCode prompt path and/or this provider’s Cursor `RequestContext`). That is intentional OpenCode parity, not a Cursor-only hole. There is no path allowlist that rejects absolute/`~/` instruction paths (matching OpenCode; OpenCode has not treated this as a defect). + +Treat project `opencode.json` as **trusted**. Do not open untrusted repositories with project config enabled if that is unacceptable. + +## Mitigation: disable project config + +To ignore project-level OpenCode config (including project `instructions` and project `AGENTS.md` / `CLAUDE.md` / `CONTEXT.md` discovery), set: + +```bash +export OPENCODE_DISABLE_PROJECT_CONFIG=1 +``` + +OpenCode honors this flag when loading config and assembling prompts. This provider honors the same flag when collecting rules for Cursor `RequestContext`, so project `instructions` are not merged and project instruction files are not auto-discovered. Global config under `~/.config/opencode` (and `~/.claude/CLAUDE.md` when applicable) still applies. + +Truthy values: `1` or `true` (case-insensitive), same as OpenCode. + +## Related hardening in this provider + +- Remote `instructions` URLs are **HTTPS-only** (`http://` is skipped). Redirects (including to a local proxy) are intentional and not blocked. diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/activity.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/activity.d.ts new file mode 100644 index 00000000..d338cc1b --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/activity.d.ts @@ -0,0 +1,15 @@ +export type SessionActivitySource = { + lastActivityAt(sessionId: string): number | undefined; +}; +/** Tracks OpenCode message progress and propagates it through subagent ancestry. */ +export declare class SessionActivityTracker implements SessionActivitySource { + private readonly parentBySession; + private readonly lastActivityBySession; + linkSession(sessionId: string, parentId?: string): void; + recordActivity(sessionId: string, at?: number): void; + lastActivityAt(sessionId: string): number | undefined; + removeSession(sessionId: string): void; + clear(): void; + private prune; +} +export declare const sessionActivity: SessionActivityTracker; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/activity.js b/references/upstream-plugins/cursor-opencode-provider/dist/activity.js new file mode 100644 index 00000000..a31275e0 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/activity.js @@ -0,0 +1,76 @@ +const MAX_ANCESTRY_DEPTH = 64; +const MAX_TRACKED_SESSIONS = 1_024; +const ACTIVITY_RETENTION_MS = 24 * 60 * 60 * 1_000; +/** Tracks OpenCode message progress and propagates it through subagent ancestry. */ +export class SessionActivityTracker { + parentBySession = new Map(); + lastActivityBySession = new Map(); + linkSession(sessionId, parentId) { + if (!sessionId) + return; + this.prune(Date.now()); + if (parentId && parentId !== sessionId) + this.parentBySession.set(sessionId, parentId); + else + this.parentBySession.delete(sessionId); + const existing = this.lastActivityBySession.get(sessionId); + if (existing !== undefined) + this.recordActivity(sessionId, existing); + this.prune(Date.now()); + } + recordActivity(sessionId, at = Date.now()) { + if (!sessionId || !Number.isFinite(at)) + return; + this.prune(at); + const visited = new Set(); + let current = sessionId; + for (let depth = 0; current && depth < MAX_ANCESTRY_DEPTH; depth++) { + if (visited.has(current)) + return; + visited.add(current); + const previous = this.lastActivityBySession.get(current); + if (previous === undefined || at > previous) { + // Map insertion order is our least-recently-active eviction order. + this.lastActivityBySession.delete(current); + this.lastActivityBySession.set(current, at); + } + current = this.parentBySession.get(current); + } + this.prune(at); + } + lastActivityAt(sessionId) { + this.prune(Date.now()); + return this.lastActivityBySession.get(sessionId); + } + removeSession(sessionId) { + this.parentBySession.delete(sessionId); + this.lastActivityBySession.delete(sessionId); + } + clear() { + this.parentBySession.clear(); + this.lastActivityBySession.clear(); + } + prune(now) { + const oldestAllowed = now - ACTIVITY_RETENTION_MS; + for (const [sessionId, activityAt] of this.lastActivityBySession) { + if (activityAt >= oldestAllowed) + break; + this.lastActivityBySession.delete(sessionId); + this.parentBySession.delete(sessionId); + } + while (this.lastActivityBySession.size > MAX_TRACKED_SESSIONS) { + const oldest = this.lastActivityBySession.keys().next().value; + if (!oldest) + break; + this.lastActivityBySession.delete(oldest); + this.parentBySession.delete(oldest); + } + while (this.parentBySession.size > MAX_TRACKED_SESSIONS) { + const oldest = this.parentBySession.keys().next().value; + if (!oldest) + break; + this.parentBySession.delete(oldest); + } + } +} +export const sessionActivity = new SessionActivityTracker(); diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/agent-url.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/agent-url.d.ts new file mode 100644 index 00000000..0a999fc1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/agent-url.d.ts @@ -0,0 +1,20 @@ +type AgentUrlOptions = { + apiBaseURL?: string; + baseURL?: string; + telemetryEnabled?: boolean; + timeoutMs?: number; +}; +/** + * Resolve the Run stream origin for this account via the `GetServerConfig` + * Connect RPC. Memoized for the process lifetime. + * + * - already resolved → return the memo (no fetch) + * - otherwise → fetch `agentUrlConfig.agentnUrl` (then `agentUrl`), memoize, return + * - fetch fails / no valid `agentUrlConfig` → throw (no global-host fallback) + * + * Concurrent callers share a single in-flight fetch. + */ +export declare function resolveAgentUrl(token: string, options?: AgentUrlOptions): Promise; +/** Reset the in-process memo. Tests only. */ +export declare function resetAgentUrlCache(): void; +export {}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/agent-url.js b/references/upstream-plugins/cursor-opencode-provider/dist/agent-url.js new file mode 100644 index 00000000..914b9781 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/agent-url.js @@ -0,0 +1,77 @@ +import { createHash } from "node:crypto"; +import { fetchAgentUrl } from "./transport/connect.js"; +import { trace } from "./debug.js"; +import { CURSOR_API_HOST } from "./shared.js"; +const DEFAULT_API_BASE = `https://${CURSOR_API_HOST}`; +// In-process memo of the region-specific Run stream origin (agentnUrl). Resolved +// once per process — the auth loader warms it, and the first startSession reuses +// it — and held for the process lifetime. Region routing is near-static, and +// re-resolving mid-session would break a held-open bidi Run stream anyway. +// +// Unlike the models/version caches this is NOT persisted to disk. A wrong agent +// host is fatal and silent (HTTP 200 + immediate close, "This region is not +// available for your team"), so persisting it would risk pinning the process — +// or the next process — to a stale region or the legacy global host that some +// accounts reject. Resolving fresh per process is cheap (one unary RPC on the +// API host) and self-heals after a Cursor-side region migration on restart. +// +function normalizeApiBaseURL(baseURL) { + if (!baseURL) + return DEFAULT_API_BASE; + return new URL(baseURL).origin; +} +function resolveCacheKey(token, options) { + // codeql[js/insufficient-password-hash] -- The token is a high-entropy JWT/API key, + // and this digest is only an ephemeral memoization key, never a password verifier. + const tokenHash = createHash("sha256").update(token).digest("hex").slice(0, 16); + return `${tokenHash}|${normalizeApiBaseURL(options.apiBaseURL ?? options.baseURL)}|telem:${options.telemetryEnabled === true}`; +} +const _resolved = new Map(); +// In-flight fetches share the same key as resolved URLs so concurrent callers dedup per account. +const _inflight = new Map(); +/** + * Resolve the Run stream origin for this account via the `GetServerConfig` + * Connect RPC. Memoized for the process lifetime. + * + * - already resolved → return the memo (no fetch) + * - otherwise → fetch `agentUrlConfig.agentnUrl` (then `agentUrl`), memoize, return + * - fetch fails / no valid `agentUrlConfig` → throw (no global-host fallback) + * + * Concurrent callers share a single in-flight fetch. + */ +export async function resolveAgentUrl(token, options = {}) { + const cacheKey = resolveCacheKey(token, options); + const memo = _resolved.get(cacheKey); + if (memo) { + trace(`agent-url: reuse in-process memo → ${memo}`); + return memo; + } + const inflight = _inflight.get(cacheKey); + if (inflight) { + trace("agent-url: awaiting in-flight GetServerConfig"); + return inflight; + } + const promise = (async () => { + try { + const url = await fetchAgentUrl(token, options); + _resolved.set(cacheKey, url); + trace(`agent-url: resolved via GetServerConfig → ${url}`); + return url; + } + catch (err) { + const reason = err instanceof Error ? err.message : String(err); + trace(`agent-url: GetServerConfig failed (${reason}); no fallback agent host will be used`); + throw err; + } + finally { + _inflight.delete(cacheKey); + } + })(); + _inflight.set(cacheKey, promise); + return promise; +} +/** Reset the in-process memo. Tests only. */ +export function resetAgentUrlCache() { + _resolved.clear(); + _inflight.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/auth.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/auth.d.ts new file mode 100644 index 00000000..a02696de --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/auth.d.ts @@ -0,0 +1,56 @@ +export declare class AuthExchangeError extends Error { + cause?: unknown | undefined; + constructor(message: string, cause?: unknown | undefined); +} +export declare class AuthRefreshError extends Error { + cause?: unknown | undefined; + constructor(message: string, cause?: unknown | undefined); +} +export declare class AuthPollError extends Error { + cause?: unknown | undefined; + constructor(message: string, cause?: unknown | undefined); +} +export declare class AuthTimeoutError extends Error { + constructor(message: string); +} +export declare function isExpiringSoon(jwt: string, thresholdS?: number): boolean; +export declare function decodeJwtPayload(jwt: string): Record | null; +/** JWT `exp` claim as epoch milliseconds, or null if missing/malformed. */ +export declare function decodeJwtExpiryMs(jwt: string): number | null; +export declare function useAuthToken(token: string): { + accessToken: string; +}; +export type TokenPair = { + accessToken: string; + refreshToken: string; +}; +export declare function exchangeApiKey(apiKey: string, baseUrl?: string): Promise; +export declare function refreshAccessToken(refreshToken: string, baseUrl?: string): Promise; +/** Clear the apiKey→JWT cache (tests). */ +export declare function clearBearerTokenCache(): void; +/** + * Resolve a Bearer JWT for Cursor API calls. Prefer an already-exchanged + * `accessToken`; otherwise exchange (and cache) from `apiKey`, refreshing + * when the cached JWT is near expiry. + * + * `apiKey` is only ever a raw exchangeable secret when it has Cursor's + * `crsr_` prefix. Callers that generically forward whatever credential value + * they hold — e.g. a host's package-agnostic "aisdk:" SDK loader, which + * doesn't distinguish our OAuth vs. API-key connection methods and may pass + * an already-issued JWT through the `apiKey` field — hand us a token that's + * already good to use as-is; POSTing it to the exchange endpoint 401s. + */ +export declare function resolveBearerToken(input: { + accessToken?: string; + apiKey?: string; + baseUrl?: string; +}): Promise; +export type PkceParams = { + verifier: string; + challenge: string; + uuid: string; +}; +export declare function generatePkceParams(): PkceParams; +export declare function generatePkceChallenge(verifier: string): Promise; +export declare function buildLoginUrl(challenge: string, uuid: string, websiteUrl?: string): string; +export declare function pollForTokens(uuid: string, verifier: string, baseUrl?: string, signal?: AbortSignal, maxAttempts?: number): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/auth.js b/references/upstream-plugins/cursor-opencode-provider/dist/auth.js new file mode 100644 index 00000000..2aafee0a --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/auth.js @@ -0,0 +1,264 @@ +import { CURSOR_API_HOST, CURSOR_WEBSITE_HOST } from "./shared.js"; +import { withAbortDeadline } from "./deadline.js"; +const API_BASE = `https://${CURSOR_API_HOST}`; +const AUTH_REQUEST_TIMEOUT_MS = 5_000; +export class AuthExchangeError extends Error { + cause; + constructor(message, cause) { + super(message); + this.cause = cause; + this.name = "AuthExchangeError"; + } +} +export class AuthRefreshError extends Error { + cause; + constructor(message, cause) { + super(message); + this.cause = cause; + this.name = "AuthRefreshError"; + } +} +export class AuthPollError extends Error { + cause; + constructor(message, cause) { + super(message); + this.cause = cause; + this.name = "AuthPollError"; + } +} +export class AuthTimeoutError extends Error { + constructor(message) { + super(message); + this.name = "AuthTimeoutError"; + } +} +// ── Helpers ── +export function isExpiringSoon(jwt, thresholdS = 300) { + const payload = decodeJwtPayload(jwt); + if (!payload || typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) { + return true; + } + return payload.exp * 1000 - Date.now() < thresholdS * 1000; +} +export function decodeJwtPayload(jwt) { + try { + const segment = jwt.split(".")[1]; + if (!segment) + return null; + const json = Buffer.from(segment, "base64url").toString("utf8"); + const payload = JSON.parse(json); + if (!payload || typeof payload !== "object" || Array.isArray(payload)) + return null; + return payload; + } + catch { + return null; + } +} +/** JWT `exp` claim as epoch milliseconds, or null if missing/malformed. */ +export function decodeJwtExpiryMs(jwt) { + const payload = decodeJwtPayload(jwt); + if (!payload || typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) { + return null; + } + return payload.exp * 1000; +} +function base64url(bytes) { + return btoa(String.fromCharCode(...bytes)) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); +} +// ── Mode A: pass-through auth token ── +export function useAuthToken(token) { + return { accessToken: token }; +} +export async function exchangeApiKey(apiKey, baseUrl = API_BASE) { + return withAbortDeadline(AUTH_REQUEST_TIMEOUT_MS, () => new AuthExchangeError("API key exchange timed out"), async (signal) => { + let res; + try { + res = await fetch(`${baseUrl}/auth/exchange_user_api_key`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: "{}", + signal, + }); + } + catch (cause) { + throw new AuthExchangeError("API key exchange request failed", cause); + } + if (!res.ok) { + throw new AuthExchangeError(`API key exchange failed: ${res.status} ${res.statusText}`); + } + let body; + try { + body = await res.json(); + } + catch (cause) { + throw new AuthExchangeError("API key exchange returned malformed JSON", cause); + } + if (typeof body.accessToken !== "string" || typeof body.refreshToken !== "string") { + throw new AuthExchangeError("Exchange response missing tokens"); + } + return { accessToken: body.accessToken, refreshToken: body.refreshToken }; + }); +} +export async function refreshAccessToken(refreshToken, baseUrl = API_BASE) { + return withAbortDeadline(AUTH_REQUEST_TIMEOUT_MS, () => new AuthRefreshError("Token refresh timed out"), async (signal) => { + let res; + try { + res = await fetch(`${baseUrl}/auth/token`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refreshToken }), + signal, + }); + } + catch (cause) { + throw new AuthRefreshError("Token refresh request failed", cause); + } + if (!res.ok) { + throw new AuthRefreshError(`Token refresh failed: ${res.status} ${res.statusText}`); + } + let body; + try { + body = await res.json(); + } + catch (cause) { + throw new AuthRefreshError("Token refresh returned malformed JSON", cause); + } + if (typeof body.accessToken !== "string" || typeof body.refreshToken !== "string") { + throw new AuthRefreshError("Refresh response missing tokens"); + } + return { accessToken: body.accessToken, refreshToken: body.refreshToken }; + }); +} +// Cache JWTs obtained via API-key exchange so doStream doesn't re-exchange +// on every turn when the caller only supplied `apiKey`. +const _apiKeyTokenCache = new Map(); +const inflightBearer = new Map(); +/** Clear the apiKey→JWT cache (tests). */ +export function clearBearerTokenCache() { + _apiKeyTokenCache.clear(); + inflightBearer.clear(); +} +/** + * Resolve a Bearer JWT for Cursor API calls. Prefer an already-exchanged + * `accessToken`; otherwise exchange (and cache) from `apiKey`, refreshing + * when the cached JWT is near expiry. + * + * `apiKey` is only ever a raw exchangeable secret when it has Cursor's + * `crsr_` prefix. Callers that generically forward whatever credential value + * they hold — e.g. a host's package-agnostic "aisdk:" SDK loader, which + * doesn't distinguish our OAuth vs. API-key connection methods and may pass + * an already-issued JWT through the `apiKey` field — hand us a token that's + * already good to use as-is; POSTing it to the exchange endpoint 401s. + */ +export async function resolveBearerToken(input) { + if (input.accessToken) + return input.accessToken; + if (!input.apiKey) { + throw new Error("Cursor provider: no access token or API key provided"); + } + if (!input.apiKey.startsWith("crsr_")) + return input.apiKey; + const baseUrl = input.baseUrl ?? API_BASE; + const cached = _apiKeyTokenCache.get(input.apiKey); + if (cached && !isExpiringSoon(cached.accessToken)) { + return cached.accessToken; + } + const inflightKey = `${baseUrl}\0${input.apiKey}`; + const existing = inflightBearer.get(inflightKey); + if (existing) + return existing; + async function refreshOrExchange() { + if (cached) { + try { + const refreshed = await refreshAccessToken(cached.refreshToken, baseUrl); + _apiKeyTokenCache.set(input.apiKey, refreshed); + return refreshed.accessToken; + } + catch { + // Fall through to a fresh exchange. + } + } + const pair = await exchangeApiKey(input.apiKey, baseUrl); + _apiKeyTokenCache.set(input.apiKey, pair); + return pair.accessToken; + } + const pending = refreshOrExchange(); + inflightBearer.set(inflightKey, pending); + try { + return await pending; + } + finally { + if (inflightBearer.get(inflightKey) === pending) + inflightBearer.delete(inflightKey); + } +} +// ── Mode C: PKCE browser login ── +async function sha256(data) { + return new Uint8Array(await crypto.subtle.digest("SHA-256", data)); +} +export function generatePkceParams() { + const verifierBytes = new Uint8Array(32); + crypto.getRandomValues(verifierBytes); + const verifier = base64url(verifierBytes); + const uuid = crypto.randomUUID(); + return { verifier, challenge: "", uuid }; // challenge filled async +} +export async function generatePkceChallenge(verifier) { + const enc = new TextEncoder(); + const hash = await sha256(enc.encode(verifier)); + return base64url(hash); +} +export function buildLoginUrl(challenge, uuid, websiteUrl = `https://${CURSOR_WEBSITE_HOST}`) { + return `${websiteUrl}/loginDeepControl?challenge=${encodeURIComponent(challenge)}&uuid=${encodeURIComponent(uuid)}&mode=login&redirectTarget=cli`; +} +export async function pollForTokens(uuid, verifier, baseUrl = API_BASE, signal, maxAttempts = 150) { + let failures = 0; + for (let i = 0; i < maxAttempts; i++) { + if (signal?.aborted) + throw new AuthTimeoutError("Poll cancelled"); + const delay = Math.min(1000 * Math.pow(1.2, i), 10000); + await new Promise((r) => setTimeout(r, delay)); + try { + const url = `${baseUrl}/auth/poll?uuid=${encodeURIComponent(uuid)}&verifier=${encodeURIComponent(verifier)}`; + const res = await fetch(url); + if (res.status === 404) { + failures = 0; + continue; + } + if (!res.ok) { + failures++; + if (failures >= 3) { + throw new AuthPollError(`Poll failed after ${failures} consecutive errors (last: ${res.status})`); + } + continue; + } + const body = await res.json(); + if (body.accessToken && body.refreshToken) { + return { accessToken: body.accessToken, refreshToken: body.refreshToken }; + } + // 200 OK but missing tokens — same fail-fast policy as the other error + // paths, otherwise a partial-body server response stalls the full + // ~5-minute poll budget with no diagnostic. + failures++; + if (failures >= 3) { + throw new AuthPollError("Poll returned 3 consecutive 200 responses without tokens"); + } + } + catch (err) { + if (err instanceof AuthPollError) + throw err; + failures++; + if (failures >= 3) { + throw new AuthPollError("Poll failed after 3 consecutive network errors", err); + } + } + } + throw new AuthTimeoutError(`Poll timed out after ${maxAttempts} attempts (~5 min)`); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/compaction-marker.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/compaction-marker.d.ts new file mode 100644 index 00000000..a2b3e727 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/compaction-marker.d.ts @@ -0,0 +1,15 @@ +/** + * Session-scoped compaction marker for the OpenCode 2.0 plugin. + * + * The classic plugin learns that a turn belongs to OpenCode's compaction agent + * via the `chat.params` hook and forwards it as a provider option. OpenCode 2.0 + * removed `chat.params`; the only place the runtime names the owning agent is + * `session.hook("context")`, which fires before dispatch. The 2.0 plugin writes + * an explicit request-local provider option and records this session marker as + * a compatibility fallback for hosts that do not preserve mutable options. + * + * Bounded so a long-lived server cannot accumulate ids for dead sessions. + */ +export declare function markCompactionSession(sessionID: string, isCompaction: boolean): void; +export declare function isCompactionSession(sessionID: string | undefined): boolean; +export declare function clearCompactionSessions(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/compaction-marker.js b/references/upstream-plugins/cursor-opencode-provider/dist/compaction-marker.js new file mode 100644 index 00000000..9ae0d777 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/compaction-marker.js @@ -0,0 +1,37 @@ +/** + * Session-scoped compaction marker for the OpenCode 2.0 plugin. + * + * The classic plugin learns that a turn belongs to OpenCode's compaction agent + * via the `chat.params` hook and forwards it as a provider option. OpenCode 2.0 + * removed `chat.params`; the only place the runtime names the owning agent is + * `session.hook("context")`, which fires before dispatch. The 2.0 plugin writes + * an explicit request-local provider option and records this session marker as + * a compatibility fallback for hosts that do not preserve mutable options. + * + * Bounded so a long-lived server cannot accumulate ids for dead sessions. + */ +const MAX_TRACKED_SESSIONS = 256; +const compactionSessions = new Set(); +export function markCompactionSession(sessionID, isCompaction) { + if (!sessionID) + return; + if (!isCompaction) { + compactionSessions.delete(sessionID); + return; + } + // Re-insert to keep insertion order meaningful for the eviction below. + compactionSessions.delete(sessionID); + compactionSessions.add(sessionID); + while (compactionSessions.size > MAX_TRACKED_SESSIONS) { + const oldest = compactionSessions.values().next().value; + if (oldest === undefined) + break; + compactionSessions.delete(oldest); + } +} +export function isCompactionSession(sessionID) { + return typeof sessionID === "string" && compactionSessions.has(sessionID); +} +export function clearCompactionSessions() { + compactionSessions.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/agents.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/agents.d.ts new file mode 100644 index 00000000..05f371f2 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/agents.d.ts @@ -0,0 +1,12 @@ +export type CollectedAgent = { + fullPath: string; + name: string; + description: string; + prompt: string; +}; +/** + * Custom agents from the active OpenCode-compatible config roots. A host bridge may + * install a host-neutral bridge before loading this module; without one this + * remains the direct OpenCode discovery path. + */ +export declare function collectAgents(workspaceRoot: string): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/agents.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/agents.js new file mode 100644 index 00000000..b61a9f60 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/agents.js @@ -0,0 +1,136 @@ +import { readdir, readFile, realpath, stat } from "node:fs/promises"; +import path from "node:path"; +import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js"; +async function exists(p) { + try { + await stat(p); + return true; + } + catch { + return false; + } +} +function parseAgentMarkdown(raw, fallbackName) { + let name = fallbackName; + let description = ""; + let body = raw; + if (raw.startsWith("---")) { + const end = raw.indexOf("\n---", 3); + if (end >= 0) { + const fm = raw.slice(3, end).trim(); + body = raw.slice(end + 4).replace(/^\n/, ""); + for (const line of fm.split("\n")) { + const m = line.match(/^(\w+):\s*(.*)$/); + if (!m) + continue; + let val = m[2].trim(); + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1); + } + if (m[1] === "name") + name = val; + if (m[1] === "description") + description = val; + } + } + } + return { name, description, prompt: body.slice(0, 20_000) }; +} +function uniquePaths(paths) { + const seen = new Set(); + return paths.filter((value) => { + const normalized = path.resolve(value); + if (seen.has(normalized)) + return false; + seen.add(normalized); + return true; + }); +} +/** + * Read one host config root's `agent/` and `agents/` trees. Host loaders use + * both spellings and recurse through nested markdown files. Directory symlinks + * are followed with realpath cycle detection, matching host behavior without + * allowing a symlink loop to hang context construction. + */ +async function scanAgentRoot(root, out) { + if (!(await exists(root))) + return; + const visitedDirs = new Set(); + const scanDir = async (dir, relativePrefix) => { + let canonical; + try { + canonical = await realpath(dir); + } + catch { + return; + } + if (visitedDirs.has(canonical)) + return; + visitedDirs.add(canonical); + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); + } + catch { + return; + } + for (const entry of entries) { + const full = path.join(dir, entry.name); + const relative = path.join(relativePrefix, entry.name); + let isDirectory = entry.isDirectory(); + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + try { + const target = await stat(full); + isDirectory = target.isDirectory(); + isFile = target.isFile(); + } + catch { + continue; + } + } + if (isDirectory) { + await scanDir(full, relative); + continue; + } + if (!isFile || !entry.name.endsWith(".md")) + continue; + try { + const raw = await readFile(full, "utf-8"); + const fallbackName = relative.replace(/\\/g, "/").replace(/\.md$/, ""); + const parsed = parseAgentMarkdown(raw, fallbackName); + if (!out.has(parsed.name)) { + out.set(parsed.name, { + fullPath: path.resolve(full), + name: parsed.name, + description: parsed.description, + prompt: parsed.prompt, + }); + } + } + catch { + /* skip unreadable or malformed files without failing context build */ + } + } + }; + // Scan singular before plural, then let root ordering establish host + // precedence. This is deterministic and mirrors the hosts' brace glob. + await scanDir(path.join(root, "agent"), "agent"); + await scanDir(path.join(root, "agents"), "agents"); +} +/** + * Custom agents from the active OpenCode-compatible config roots. A host bridge may + * install a host-neutral bridge before loading this module; without one this + * remains the direct OpenCode discovery path. + */ +export async function collectAgents(workspaceRoot) { + const out = new Map(); + const roots = uniquePaths([ + ...opencodeProjectConfigDirs(workspaceRoot), + ...opencodeGlobalConfigDirs(), + ]); + for (const root of roots) + await scanAgentRoot(root, out); + return [...out.values()]; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/auth-store.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/auth-store.d.ts new file mode 100644 index 00000000..8b63efee --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/auth-store.d.ts @@ -0,0 +1,21 @@ +/** Mirrors OpenCode / SDK OAuth + API auth used for Cursor. `wellknown` is not wired yet. */ +export type StoredAuth = { + type: "oauth"; + access: string; + refresh: string; + expires: number; + accountId?: string; + enterpriseUrl?: string; +} | { + type: "api"; + key: string; + metadata?: Record; +}; +/** + * Read a provider's credentials from OpenCode's `auth.json` (XDG data dir). + * + * Honors `OPENCODE_AUTH_CONTENT` when set — same injection hook OpenCode core + * uses in tests / embedded runs (not an SDK export). + */ +export declare function readStoredAuth(providerId: string): Promise; +export declare function asStoredAuth(value: unknown): StoredAuth | undefined; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/auth-store.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/auth-store.js new file mode 100644 index 00000000..bf176a07 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/auth-store.js @@ -0,0 +1,64 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { hostGlobalDataDir } from "./paths.js"; +function debugEnabled() { + return process.env.CURSOR_PROVIDER_DEBUG === "1" || process.env.CURSOR_PROVIDER_DEBUG === "true"; +} +function debugAuthStore(message) { + if (!debugEnabled()) + return; + console.debug(`[cursor-opencode-provider] auth-store: ${message}`); +} +/** + * Read a provider's credentials from OpenCode's `auth.json` (XDG data dir). + * + * Honors `OPENCODE_AUTH_CONTENT` when set — same injection hook OpenCode core + * uses in tests / embedded runs (not an SDK export). + */ +export async function readStoredAuth(providerId) { + if (process.env.OPENCODE_AUTH_CONTENT) { + try { + const data = JSON.parse(process.env.OPENCODE_AUTH_CONTENT); + return asStoredAuth(data[providerId]); + } + catch { + debugAuthStore("OPENCODE_AUTH_CONTENT is not valid JSON"); + return undefined; + } + } + const filePath = path.join(hostGlobalDataDir(), "auth.json"); + try { + const raw = await readFile(filePath, "utf-8"); + try { + const data = JSON.parse(raw); + return asStoredAuth(data[providerId]); + } + catch { + debugAuthStore("auth.json is not valid JSON"); + return undefined; + } + } + catch (err) { + const code = err?.code; + if (code !== "ENOENT") { + debugAuthStore(`failed to read auth.json (${code ?? "unknown"})`); + } + return undefined; + } +} +export function asStoredAuth(value) { + if (!value || typeof value !== "object") + return undefined; + const v = value; + if (v.type === "oauth" && + typeof v.access === "string" && + typeof v.refresh === "string" && + typeof v.expires === "number") { + return value; + } + if (v.type === "api" && typeof v.key === "string") { + return value; + } + // wellknown is intentionally rejected until resolveAccessToken supports it + return undefined; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/build.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/build.d.ts new file mode 100644 index 00000000..7c79a8ee --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/build.d.ts @@ -0,0 +1,24 @@ +import { type OpencodeToolDef } from "../protocol/tools.js"; +export type BuildRequestContextInput = { + workspaceRoot: string; + tools?: OpencodeToolDef[]; + providerIdentifier?: string; + /** When set, skills/subagents/plugins are epoch-held for this conversation. */ + conversationId?: string; +}; +export declare const DYNAMIC_REQUEST_CONTEXT_KEYS: readonly ["tools", "agent_skills", "custom_subagents", "mcp_file_system_options", "mcp_meta_tool_options", "web_search_enabled", "web_fetch_enabled", "agent_skills_info_complete", "custom_subagents_info_complete", "mcp_file_system_info_complete", "mcp_info_complete", "hooks_additional_context"]; +export type DynamicRequestContextKey = typeof DYNAMIC_REQUEST_CONTEXT_KEYS[number]; +/** + * Full RequestContext payload for live UMA + exec #10 reply. + * Sourced from OpenCode discovery (and .claude/.agents skill fallbacks). + * Honors `instructions` globs the same way OpenCode does. The provider never + * looks in Cursor's own directories; a `.cursor/` path is read only when the + * user's own OpenCode config lists it, exactly as the host would read it. + */ +export declare function buildRequestContext(input: BuildRequestContextInput): Promise>; +/** Rediscover only capability/plugin sections that may change during a chat. */ +export declare function buildDynamicRequestContext(input: BuildRequestContextInput): Promise>; +/** Keep expensive workspace state frozen while replacing every live capability field. */ +export declare function materializeRequestContext(base: Record, dynamic: Record): Record; +/** Strip live capability fields before retaining/persisting a conversation base. */ +export declare function requestContextBase(context: Record): Record; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/build.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/build.js new file mode 100644 index 00000000..8322a2a8 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/build.js @@ -0,0 +1,200 @@ +import path from "node:path"; +import { extractHostSubagentCatalog, toolsToDescriptors, toolsToMcpDescriptors, } from "../protocol/tools.js"; +import { collectRules, findGitWorktree, loadMergedConfig, } from "./rules.js"; +import { collectSkills } from "./skills.js"; +import { collectAgents } from "./agents.js"; +import { collectPlugins } from "./plugins.js"; +import { collectGit } from "./git.js"; +import { collectProjectLayout } from "./layout.js"; +import { buildEnv } from "./env.js"; +import { ensureOpencodeProjectDir } from "./paths.js"; +import { holdCapabilityOverlay } from "./overlay.js"; +import { traceRequestContextPaths } from "../debug.js"; +export const DYNAMIC_REQUEST_CONTEXT_KEYS = [ + "tools", + "agent_skills", + "custom_subagents", + "mcp_file_system_options", + "mcp_meta_tool_options", + "web_search_enabled", + "web_fetch_enabled", + "agent_skills_info_complete", + "custom_subagents_info_complete", + "mcp_file_system_info_complete", + "mcp_info_complete", + "hooks_additional_context", +]; +const DEFAULT_HOST_SUBAGENTS = [ + { + name: "general", + description: "General-purpose agent for complex research and multi-step tasks.", + }, + { + name: "explore", + description: "Read-only agent for searching and understanding the local codebase.", + }, +]; +/** + * Convert the raw host executor catalog into the exact subagent list advertised + * to Cursor. `hostSubagents.complete` answers a narrow question: did the host's + * task tool itself exposes an exhaustive recipient list (schema enum or + * catalog marker)? OpenCode's task tool intentionally uses a plain string, so + * that raw flag is false even though we can still advertise a complete Cursor + * catalog by adding built-in defaults and discovered agent files. + * + * `custom_subagents_info_complete` must describe the final advertised list, not + * the raw extraction source. If there is no executor, the empty list is + * complete. If the host catalog is complete, use it verbatim. Otherwise we make + * the advertised set complete by augmenting with default host agents and all + * locally discovered agents. + */ +function buildAdvertisedSubagentCatalog(hostSubagents, discoveredAgents) { + if (!hostSubagents.executor) + return { agents: [], complete: true }; + if (hostSubagents.complete) + return { agents: hostSubagents.agents, complete: true }; + return { + agents: [...DEFAULT_HOST_SUBAGENTS, ...hostSubagents.agents, ...discoveredAgents], + complete: true, + }; +} +/** + * Full RequestContext payload for live UMA + exec #10 reply. + * Sourced from OpenCode discovery (and .claude/.agents skill fallbacks). + * Honors `instructions` globs the same way OpenCode does. The provider never + * looks in Cursor's own directories; a `.cursor/` path is read only when the + * user's own OpenCode config lists it, exactly as the host would read it. + */ +export async function buildRequestContext(input) { + const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd()); + const { rules, config, worktree } = await collectRules(workspaceRoot); + const [dynamic, git, layout] = await Promise.all([ + buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config), + collectGit(workspaceRoot), + collectProjectLayout(workspaceRoot), + ]); + const base = { + env: buildEnv(workspaceRoot), + rules: rules.map((r) => ({ + full_path: r.fullPath, + content: r.content, + })), + repository_info: git.repositoryInfo, + git_repos: git.gitRepos, + project_layouts: [layout], + rules_info_complete: true, + env_info_complete: true, + repository_info_complete: true, + git_repo_info_complete: true, + git_status_info_complete: true, + }; + const ctx = materializeRequestContext(base, dynamic); + traceRequestContextPaths("buildRequestContext", ctx); + return ctx; +} +async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config) { + const providerIdentifier = input.providerIdentifier ?? "opencode"; + const tools = input.tools ?? []; + const [skills, agents, plugins] = await Promise.all([ + collectSkills(workspaceRoot, worktree), + collectAgents(workspaceRoot), + collectPlugins(workspaceRoot, config), + ]); + const mcpServerNames = Object.keys(config.mcp ?? {}); + const flat = toolsToDescriptors(tools, providerIdentifier, mcpServerNames); + const nested = toolsToMcpDescriptors(tools, providerIdentifier, mcpServerNames); + const projectDir = ensureOpencodeProjectDir(workspaceRoot); + const hostSubagents = extractHostSubagentCatalog(tools); + const advertisedSubagents = buildAdvertisedSubagentCatalog(hostSubagents, agents); + const discoveredByName = new Map(agents.map((agent) => [agent.name, agent])); + const advertisedByName = new Map(); + for (const agent of advertisedSubagents.agents) { + if (!advertisedByName.has(agent.name)) + advertisedByName.set(agent.name, agent); + } + const customSubagents = [...advertisedByName.values()].map((agent) => { + const discovered = discoveredByName.get(agent.name); + return { + full_path: discovered?.fullPath ?? "", + name: agent.name, + description: discovered?.description || agent.description || "Host-configured subagent.", + // The host applies the real configured prompt when Task/Actor executes. + // Cursor only needs enough context to select the recipient intentionally. + prompt: discovered?.prompt || + `Delegate to the host-configured ${agent.name} subagent; its host instructions and tools apply.`, + }; + }); + const liveSkills = skills.map((s) => ({ + id: s.name, + full_path: s.fullPath, + content: s.content, + description: s.description, + })); + const livePlugins = plugins.map((p) => ({ + id: p.id, + line: `opencode-plugin:${p.source}:${p.id}`, + })); + const overlay = input.conversationId + ? holdCapabilityOverlay(input.conversationId, { + skills: liveSkills, + subagents: customSubagents, + plugins: livePlugins, + }) + : { skills: liveSkills.map(({ full_path, content, description }) => ({ full_path, content, description })), subagents: customSubagents, plugins: livePlugins }; + const dynamic = { + tools: flat, + agent_skills: overlay.skills, + custom_subagents: overlay.subagents, + mcp_file_system_options: { + enabled: true, + // Cursor metadata root (mcps / agent-tools), not the git workspace. + workspace_project_dir: projectDir, + mcp_descriptors: nested, + }, + mcp_meta_tool_options: { + enabled: true, + mcp_descriptors: nested, + }, + // This provider always rejects native web_search/web_fetch interaction + // queries with a headless-UI reason (see interactions.ts). Advertise that + // unavailability up front so Cursor prefers the collision-safe + // custom_web* aliases instead of routing through a query doomed to fail. + web_search_enabled: false, + web_fetch_enabled: false, + agent_skills_info_complete: true, + custom_subagents_info_complete: advertisedSubagents.complete, + mcp_file_system_info_complete: true, + mcp_info_complete: true, + }; + if (overlay.plugins.length > 0) { + dynamic.hooks_additional_context = overlay.plugins.map((p) => p.line).join("\n"); + } + return dynamic; +} +/** Rediscover only capability/plugin sections that may change during a chat. */ +export async function buildDynamicRequestContext(input) { + const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd()); + const [worktree, config] = await Promise.all([ + findGitWorktree(workspaceRoot), + loadMergedConfig(workspaceRoot), + ]); + return buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config); +} +/** Keep expensive workspace state frozen while replacing every live capability field. */ +export function materializeRequestContext(base, dynamic) { + const context = structuredClone(base); + for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS) + delete context[key]; + for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS) { + if (Object.hasOwn(dynamic, key)) + context[key] = structuredClone(dynamic[key]); + } + return context; +} +/** Strip live capability fields before retaining/persisting a conversation base. */ +export function requestContextBase(context) { + const base = structuredClone(context); + for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS) + delete base[key]; + return base; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/env.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/env.d.ts new file mode 100644 index 00000000..f5d49105 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/env.d.ts @@ -0,0 +1,7 @@ +export declare function buildEnv(workspaceRoot: string): Record; +/** + * Real workspace root for path resolution (edits, reads, …). + * Uses `env.workspace_paths[0]` — never `project_folder` / + * `mcp_file_system_options.workspace_project_dir` (those are Cursor metadata roots). + */ +export declare function workspaceRootFromRequestContext(requestContext: Record | undefined): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/env.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/env.js new file mode 100644 index 00000000..80624b26 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/env.js @@ -0,0 +1,65 @@ +import { homedir } from "node:os"; +import path from "node:path"; +import { trace } from "../debug.js"; +import { ensureOpencodeProjectDir } from "./paths.js"; +export function buildEnv(workspaceRoot) { + const cwd = path.resolve(workspaceRoot); + let timeZone = "UTC"; + try { + timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; + } + catch { + /* keep UTC */ + } + const home = homedir(); + const osVersion = (() => { + const p = process.platform; + const r = process.release?.version; + return r ? `${p} ${r}` : p; + })(); + // Cursor's project_folder is a metadata root (agent-tools, terminals, …), + // not the git workspace. Keep dumps under OpenCode cache. + const projectFolder = ensureOpencodeProjectDir(cwd); + // process_working_directory must match the workspace, not the host process + // cwd. OpenCode 2.0's daemon is long-lived and often started from $HOME, so + // process.cwd() would advertise the home folder to Cursor and the model would + // treat that as its shell cwd. workspace_paths already uses `cwd` above. + const env = { + os_version: osVersion, + workspace_paths: [cwd], + shell: process.env.SHELL || "/bin/bash", + sandbox_enabled: false, + sandbox_supported: false, + time_zone: timeZone, + project_folder: projectFolder, + terminals_folder: path.join(projectFolder, "terminals"), + agent_transcripts_folder: path.join(projectFolder, "agent-transcripts"), + process_working_directory: cwd, + is_working_dir_home_dir: cwd === path.resolve(home), + }; + trace(`buildEnv: workspace_paths=${JSON.stringify(env.workspace_paths)} ` + + `project_folder=${env.project_folder} ` + + `terminals_folder=${env.terminals_folder} ` + + `agent_transcripts_folder=${env.agent_transcripts_folder} ` + + `process_working_directory=${env.process_working_directory}`); + return env; +} +/** + * Real workspace root for path resolution (edits, reads, …). + * Uses `env.workspace_paths[0]` — never `project_folder` / + * `mcp_file_system_options.workspace_project_dir` (those are Cursor metadata roots). + */ +export function workspaceRootFromRequestContext(requestContext) { + const env = requestContext?.env; + if (env && typeof env === "object") { + const paths = env.workspace_paths; + if (Array.isArray(paths) && typeof paths[0] === "string" && paths[0].trim()) { + const root = path.resolve(paths[0]); + trace(`workspaceRootFromRequestContext: using workspace_paths[0]=${root}`); + return root; + } + } + const fallback = process.cwd(); + trace(`workspaceRootFromRequestContext: workspace_paths missing; fallback cwd=${fallback}`); + return fallback; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/epoch.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/epoch.d.ts new file mode 100644 index 00000000..ba4ca00a --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/epoch.d.ts @@ -0,0 +1,59 @@ +export declare const MAX_CONTEXT_EPOCHS = 256; +export type ContextSourceSnapshot = { + hostSystemHash: string; + guidanceHash: string; + hostAgent: string; + workspaceRoot: string; +}; +export type ContextEpoch = { + conversationId: string; + /** Exact system text seeded at epoch start. Empty when recovered from checkpoint. */ + baselineSystemPrompt: string; + baselineHash: string; + /** True when restart hydrated past a checkpoint without the original baseline bytes. */ + recovered: boolean; + snapshot: ContextSourceSnapshot; +}; +export type AdmitContextEpochInput = { + conversationId: string; + /** True when this Run carries conversation_state (no systemPrompt on the wire). */ + hasCheckpoint: boolean; + hostSystem?: string; + guidance?: string; + hostAgent?: string; + workspaceRoot: string; + /** + * One-shot reminders already wrapped (mode / kickoff). Always chronological — + * never folded into the frozen baseline. + */ + oneShotReminders?: readonly string[]; +}; +export type AdmitContextEpochResult = { + action: "initialize" | "unchanged" | "updated" | "recovered"; + /** Wire systemPrompt for seed Runs only — always the frozen baseline once set. */ + seedSystemPrompt?: string; + /** Combined Mid-Conversation System Message (append after user text). */ + midConversationMessage?: string; + epoch: ContextEpoch; +}; +/** + * Admit System Context at a Safe Provider-Turn Boundary. + * + * - First seed: freeze baseline, return it as seedSystemPrompt; one-shots → mid. + * - Checkpoint turn: never returns seedSystemPrompt; source diffs + one-shots → mid. + * - Reseed same epoch: return frozen baseline bytes (not live host text). + * - Recovered (restart past a checkpoint, original bytes unknown): never freeze + * live host text as a new baseline and never send a seed systemPrompt. + */ +export declare function admitContextEpoch(input: AdmitContextEpochInput): AdmitContextEpochResult; +export declare function getContextEpoch(conversationId: string): ContextEpoch | undefined; +/** Drop epoch state (compaction remint / binding clear). */ +export declare function clearContextEpoch(conversationId: string): void; +/** + * Compaction remints conversation_id. RequestContext workspace base transfers; + * System Context baseline does not — the destination starts a fresh epoch. + */ +export declare function endContextEpoch(previousConversationId: string, nextConversationId?: string): void; +export declare function resetContextEpochsForTests(): void; +/** Append a mid-conversation message after user text (V2: user precedes update). */ +export declare function appendMidConversationMessage(userText: string, midConversationMessage: string | undefined): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/epoch.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/epoch.js new file mode 100644 index 00000000..04acea9a --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/epoch.js @@ -0,0 +1,224 @@ +/** + * Context Epoch — V2-style immutable system baseline + chronological updates. + * + * Mirrors OpenCode research CONTEXT.md: + * - One Baseline System Context per conversation_id (provider-cache prefix) + * - Later Context Source changes → Mid-Conversation System Message on the + * user turn (Cursor checkpoint Runs cannot rewrite systemPrompt) + * - Epoch ends on conversation remint (compaction / post-compaction rebase) + * + * Cursor constraint: systemPrompt is only sent on seed Runs. Checkpointed + * Runs admit updates exclusively via the live user message. + */ +import { createHash } from "node:crypto"; +import { trace } from "../debug.js"; +export const MAX_CONTEXT_EPOCHS = 256; +const byConversationId = new Map(); +function sha(text) { + return createHash("sha256").update(text).digest("hex"); +} +function joinSections(parts) { + return parts.map((part) => part?.trim()).filter((part) => !!part).join("\n\n"); +} +function snapshotOf(input) { + return { + hostSystemHash: sha(input.hostSystem?.trim() || ""), + guidanceHash: sha(input.guidance?.trim() || ""), + hostAgent: input.hostAgent?.trim() || "", + workspaceRoot: input.workspaceRoot, + }; +} +function buildBaseline(input) { + // Durable System Context only. Mode/kickoff one-shots stay chronological. + return joinSections([input.hostSystem, input.guidance]); +} +function wrapReminder(body) { + const trimmed = body.trim(); + if (!trimmed) + return ""; + if (trimmed.startsWith("")) + return trimmed; + return `\n${trimmed}\n`; +} +function renderSourceUpdates(previous, next, live) { + const parts = []; + if (next.hostAgent && previous.hostAgent !== next.hostAgent) { + parts.push(wrapReminder(`Host primary agent is now ${JSON.stringify(next.hostAgent)}. ` + + "Follow this agent's instructions and tool permissions for the rest of the turn. " + + "This is a chronological context update; the original system baseline is unchanged.")); + } + if (live.hostSystem?.trim() && previous.hostSystemHash !== next.hostSystemHash) { + parts.push(wrapReminder("Host system instructions were updated for this session. Effective instructions:\n\n" + + live.hostSystem.trim())); + } + if (live.guidance?.trim() && previous.guidanceHash !== next.guidanceHash) { + parts.push(wrapReminder("OpenCode interaction guidance was updated for this session:\n\n" + live.guidance.trim())); + } + if (previous.workspaceRoot !== next.workspaceRoot && next.workspaceRoot) { + parts.push(wrapReminder(`Workspace root is now ${JSON.stringify(next.workspaceRoot)}. ` + + "Resolve workspace paths against exactly this root; never invent an absolute prefix.")); + } + return parts.filter(Boolean); +} +function touch(epoch) { + byConversationId.delete(epoch.conversationId); + byConversationId.set(epoch.conversationId, epoch); + while (byConversationId.size > MAX_CONTEXT_EPOCHS) { + const oldest = byConversationId.keys().next().value; + if (!oldest) + break; + byConversationId.delete(oldest); + } +} +function combineMessages(parts) { + const text = parts.map((part) => part.trim()).filter(Boolean).join("\n\n"); + return text || undefined; +} +/** + * Admit System Context at a Safe Provider-Turn Boundary. + * + * - First seed: freeze baseline, return it as seedSystemPrompt; one-shots → mid. + * - Checkpoint turn: never returns seedSystemPrompt; source diffs + one-shots → mid. + * - Reseed same epoch: return frozen baseline bytes (not live host text). + * - Recovered (restart past a checkpoint, original bytes unknown): never freeze + * live host text as a new baseline and never send a seed systemPrompt. + */ +export function admitContextEpoch(input) { + const conversationId = input.conversationId; + const nextSnap = snapshotOf(input); + const oneShots = (input.oneShotReminders ?? []).map((part) => part.trim()).filter(Boolean); + const existing = byConversationId.get(conversationId); + if (!existing) { + if (input.hasCheckpoint) { + // Restart / soft-evict recovery: Cursor already holds the baseline in the + // checkpoint. Record the live snapshot without reseeding systemPrompt. + const epoch = { + conversationId, + baselineSystemPrompt: "", + baselineHash: "", + recovered: true, + snapshot: nextSnap, + }; + touch(epoch); + const midConversationMessage = combineMessages(oneShots); + trace(`context epoch: recovered conversationId=${conversationId} ` + + `oneShots=${oneShots.length}`); + return { action: "recovered", midConversationMessage, epoch }; + } + const baselineSystemPrompt = buildBaseline(input); + const baselineHash = sha(baselineSystemPrompt); + const epoch = { + conversationId, + baselineSystemPrompt, + baselineHash, + recovered: false, + snapshot: nextSnap, + }; + touch(epoch); + trace(`context epoch: initialize conversationId=${conversationId} ` + + `baselineHash=${baselineHash.slice(0, 16)} baselineLen=${baselineSystemPrompt.length} ` + + `oneShots=${oneShots.length}`); + return { + action: "initialize", + seedSystemPrompt: baselineSystemPrompt || undefined, + midConversationMessage: combineMessages(oneShots), + epoch, + }; + } + // Optional sources may be unavailable on a turn. Absence is not an explicit + // deletion signal: carrying the prior observation avoids synthetic context + // churn and preserves the warm prefix. + const observedSnap = { + hostSystemHash: input.hostSystem?.trim() + ? nextSnap.hostSystemHash + : existing.snapshot.hostSystemHash, + guidanceHash: input.guidance?.trim() + ? nextSnap.guidanceHash + : existing.snapshot.guidanceHash, + hostAgent: input.hostAgent?.trim() + ? nextSnap.hostAgent + : existing.snapshot.hostAgent, + workspaceRoot: nextSnap.workspaceRoot || existing.snapshot.workspaceRoot, + }; + const updates = renderSourceUpdates(existing.snapshot, observedSnap, input); + const changed = existing.snapshot.hostSystemHash !== observedSnap.hostSystemHash + || existing.snapshot.guidanceHash !== observedSnap.guidanceHash + || existing.snapshot.hostAgent !== observedSnap.hostAgent + || existing.snapshot.workspaceRoot !== observedSnap.workspaceRoot; + if (changed) { + existing.snapshot = observedSnap; + } + touch(existing); + if (input.hasCheckpoint) { + const midConversationMessage = combineMessages([...updates, ...oneShots]); + if (changed || oneShots.length) { + trace(`context epoch: ${changed ? "updated" : "unchanged"} conversationId=${conversationId} ` + + `checkpoint=1 updates=${updates.length} oneShots=${oneShots.length}`); + } + return { + action: changed ? "updated" : "unchanged", + midConversationMessage, + epoch: existing, + }; + } + // Recovered epochs have no original baseline bytes. A later seed Run must + // not freeze live host text as if it were the prefix Cursor already holds. + if (existing.recovered && !existing.baselineSystemPrompt) { + const midConversationMessage = combineMessages([...updates, ...oneShots]); + trace(`context epoch: recovered-hold conversationId=${conversationId} ` + + `checkpoint=0 updates=${updates.length} oneShots=${oneShots.length} seed=omitted`); + return { + action: changed ? "updated" : "recovered", + midConversationMessage, + epoch: existing, + }; + } + const midConversationMessage = combineMessages([...updates, ...oneShots]); + if (changed) { + trace(`context epoch: updated conversationId=${conversationId} checkpoint=0 ` + + `updates=${updates.length} seed=frozen-baseline`); + } + return { + action: changed ? "updated" : "unchanged", + seedSystemPrompt: existing.baselineSystemPrompt || undefined, + midConversationMessage, + epoch: existing, + }; +} +export function getContextEpoch(conversationId) { + const epoch = byConversationId.get(conversationId); + if (!epoch) + return undefined; + touch(epoch); + return epoch; +} +/** Drop epoch state (compaction remint / binding clear). */ +export function clearContextEpoch(conversationId) { + if (!conversationId) + return; + if (byConversationId.delete(conversationId)) { + trace(`context epoch: cleared conversationId=${conversationId}`); + } +} +/** + * Compaction remints conversation_id. RequestContext workspace base transfers; + * System Context baseline does not — the destination starts a fresh epoch. + */ +export function endContextEpoch(previousConversationId, nextConversationId) { + clearContextEpoch(previousConversationId); + if (nextConversationId) + clearContextEpoch(nextConversationId); +} +export function resetContextEpochsForTests() { + byConversationId.clear(); +} +/** Append a mid-conversation message after user text (V2: user precedes update). */ +export function appendMidConversationMessage(userText, midConversationMessage) { + if (!midConversationMessage?.trim()) + return userText; + if (!userText.trim()) + return midConversationMessage.trim(); + if (userText.includes(midConversationMessage.trim())) + return userText; + return `${userText}\n\n${midConversationMessage.trim()}`; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/frozen.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/frozen.d.ts new file mode 100644 index 00000000..0d8e3d3e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/frozen.d.ts @@ -0,0 +1,31 @@ +import { type BuildRequestContextInput } from "./build.js"; +export declare const MAX_FROZEN_REQUEST_CONTEXTS = 256; +/** Frozen stable RequestContext base for this conversation, if any. */ +export declare function getFrozenRequestContext(conversationId: string): Record | undefined; +/** Replace the frozen stable base, stripping any live capability fields. */ +export declare function setFrozenRequestContext(conversationId: string, context: Record): void; +/** Drop a conversation's frozen RequestContext (compaction / binding reset). */ +export declare function clearFrozenRequestContext(conversationId: string): void; +/** + * Move a stable workspace base across a conversation-id reset. + * + * Compaction changes Cursor's state/checkpoint identity, not the OpenCode + * workspace. Preserve the expensive base and the prior materialized bytes as a + * comparison seed; getOrBuildRequestContext still rediscovers live capability + * overlays (then epoch-holds them) and only reuses the complete context when + * those bytes also match. + */ +export declare function transferFrozenRequestContext(previousConversationId: string, nextConversationId: string): boolean; +/** Test helper — wipe all frozen contexts. */ +export declare function resetFrozenRequestContextsForTests(): void; +/** + * Return a stable-base + live-overlay RequestContext for `conversationId`. + * The base is built once; capability sections are rediscovered every Run + * and then epoch-held. + */ +export declare function getOrBuildRequestContext(conversationId: string, input: BuildRequestContextInput, opts?: { + refresh?: boolean; +}): Promise<{ + context: Record; + reused: boolean; +}>; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/frozen.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/frozen.js new file mode 100644 index 00000000..c3766dad --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/frozen.js @@ -0,0 +1,188 @@ +import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, } from "./build.js"; +import { clearContextEpoch, endContextEpoch, resetContextEpochsForTests } from "./epoch.js"; +import { clearOverlayHold, resetOverlayHoldsForTests, transferOverlayHold, } from "./overlay.js"; +import { trace } from "../debug.js"; +import { encodeMessage } from "../protocol/messages.js"; +/** + * Freeze the expensive RequestContext base for the life of a conversation_id. + * + * Cursor's current exec-daemon cache treats RequestContext as a baked stable + * base plus live plugin/tool overlays. Rebuilding volatile git/layout data on + * every Run shifts the prompt prefix and tanks prompt-cache hits. + * + * Skills, subagents, plugin metadata, and tool/MCP capabilities are rediscovered + * each Run, then epoch-held (equal ids keep frozen bytes; new ids append) and + * overlaid on that base. If the encoded overlay bytes did not change, the exact + * prior materialized object is reused. + */ +const byConversationId = new Map(); +const materializedByConversationId = new Map(); +const buildsByConversationId = new Map(); +export const MAX_FROZEN_REQUEST_CONTEXTS = 256; +function freezeSnapshot(value, seen = new Set()) { + if (!value || typeof value !== "object") + return value; + const object = value; + if (seen.has(object) || ArrayBuffer.isView(object)) + return value; + seen.add(object); + for (const child of Object.values(object)) + freezeSnapshot(child, seen); + return Object.freeze(value); +} +function remember(conversationId, context) { + // Map insertion order is the LRU order. Refresh existing entries on use. + byConversationId.delete(conversationId); + // Clone + recursively freeze so session.requestContext cannot mutate the + // byte-stable snapshot retained for later Runs in the same conversation. + byConversationId.set(conversationId, freezeSnapshot(structuredClone(context))); + materializedByConversationId.delete(conversationId); + while (byConversationId.size > MAX_FROZEN_REQUEST_CONTEXTS) { + const oldest = byConversationId.keys().next().value; + if (!oldest) + break; + byConversationId.delete(oldest); + materializedByConversationId.delete(oldest); + clearOverlayHold(oldest); + } +} +/** Frozen stable RequestContext base for this conversation, if any. */ +export function getFrozenRequestContext(conversationId) { + const frozen = byConversationId.get(conversationId); + if (!frozen) + return undefined; + // Touch for LRU. + byConversationId.delete(conversationId); + byConversationId.set(conversationId, frozen); + return frozen; +} +/** Replace the frozen stable base, stripping any live capability fields. */ +export function setFrozenRequestContext(conversationId, context) { + if (!conversationId) + return; + remember(conversationId, requestContextBase(context)); +} +/** Drop a conversation's frozen RequestContext (compaction / binding reset). */ +export function clearFrozenRequestContext(conversationId) { + byConversationId.delete(conversationId); + materializedByConversationId.delete(conversationId); + clearOverlayHold(conversationId); + clearContextEpoch(conversationId); +} +/** + * Move a stable workspace base across a conversation-id reset. + * + * Compaction changes Cursor's state/checkpoint identity, not the OpenCode + * workspace. Preserve the expensive base and the prior materialized bytes as a + * comparison seed; getOrBuildRequestContext still rediscovers live capability + * overlays (then epoch-holds them) and only reuses the complete context when + * those bytes also match. + */ +export function transferFrozenRequestContext(previousConversationId, nextConversationId) { + if (!previousConversationId || !nextConversationId) + return false; + const base = byConversationId.get(previousConversationId); + const materialized = materializedByConversationId.get(previousConversationId); + // System Context epoch does not transfer — compaction starts a fresh baseline. + // Overlay hold does transfer: same workspace, same advertised skill/agent/plugin + // bytes, so the comparison seed can still match. clearFrozenRequestContext + // also drops epoch state for each id. + endContextEpoch(previousConversationId, nextConversationId); + transferOverlayHold(previousConversationId, nextConversationId); + byConversationId.delete(previousConversationId); + materializedByConversationId.delete(previousConversationId); + byConversationId.delete(nextConversationId); + materializedByConversationId.delete(nextConversationId); + if (!base) + return false; + remember(nextConversationId, base); + if (materialized) { + materializedByConversationId.set(nextConversationId, { + context: materialized.context, + bytes: Uint8Array.from(materialized.bytes), + }); + } + return true; +} +/** Test helper — wipe all frozen contexts. */ +export function resetFrozenRequestContextsForTests() { + byConversationId.clear(); + materializedByConversationId.clear(); + buildsByConversationId.clear(); + resetOverlayHoldsForTests(); + resetContextEpochsForTests(); +} +function sameBytes(a, b) { + if (a.length !== b.length) + return false; + for (let i = 0; i < a.length; i++) + if (a[i] !== b[i]) + return false; + return true; +} +function rememberMaterialized(conversationId, context) { + const bytes = encodeMessage("RequestContext", context); + const previous = materializedByConversationId.get(conversationId); + if (previous && sameBytes(previous.bytes, bytes)) { + return { context: previous.context, reused: true }; + } + const frozen = freezeSnapshot(structuredClone(context)); + materializedByConversationId.set(conversationId, { + context: frozen, + bytes: Uint8Array.from(bytes), + }); + return { context: frozen, reused: false }; +} +/** + * Return a stable-base + live-overlay RequestContext for `conversationId`. + * The base is built once; capability sections are rediscovered every Run + * and then epoch-held. + */ +export async function getOrBuildRequestContext(conversationId, input, opts) { + const scoped = conversationId ? { ...input, conversationId } : input; + if (opts?.refresh && conversationId) + clearOverlayHold(conversationId); + if (!opts?.refresh && conversationId) { + const base = getFrozenRequestContext(conversationId); + if (base) { + const dynamic = await buildDynamicRequestContext(scoped); + const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic)); + trace(`request_context: materialized conversationId=${conversationId} ` + + `tools=${Array.isArray(materialized.context.tools) ? materialized.context.tools.length : 0} ` + + `reused=${materialized.reused}`); + return materialized; + } + } + // Two overlapping model calls can open Runs for the same conversation. Share + // the first build so both Runs receive the same snapshot instead of racing + // two independently collected git/layout views into the cache. + if (!opts?.refresh && conversationId) { + const inFlight = buildsByConversationId.get(conversationId); + if (inFlight) { + await inFlight; + return getOrBuildRequestContext(conversationId, scoped); + } + } + const build = buildRequestContext(scoped); + if (conversationId) + buildsByConversationId.set(conversationId, build); + let context; + try { + context = await build; + } + finally { + if (conversationId && buildsByConversationId.get(conversationId) === build) { + buildsByConversationId.delete(conversationId); + } + } + if (conversationId) + setFrozenRequestContext(conversationId, context); + // The first full build already contains both base and live overlay. + const materialized = conversationId + ? rememberMaterialized(conversationId, context) + : { context: freezeSnapshot(structuredClone(context)), reused: false }; + trace(`request_context: built+frozen conversationId=${conversationId || "(none)"} ` + + `tools=${Array.isArray(materialized.context.tools) ? materialized.context.tools.length : 0} ` + + `refresh=${!!opts?.refresh}`); + return { context: materialized.context, reused: false }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/git.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/git.d.ts new file mode 100644 index 00000000..b32a689c --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/git.d.ts @@ -0,0 +1,20 @@ +export type RepoInfo = { + relative_workspace_path: string; + remote_urls: string[]; + remote_names: string[]; + repo_name: string; + repo_owner: string; + is_tracked: boolean; + is_local: boolean; + workspace_uri: string; +}; +export type GitRepoInfo = { + path: string; + status: string; + branch_name: string; + remote_url?: string; +}; +export declare function collectGit(workspaceRoot: string): Promise<{ + repositoryInfo: RepoInfo[]; + gitRepos: GitRepoInfo[]; +}>; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/git.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/git.js new file mode 100644 index 00000000..0fb62cab --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/git.js @@ -0,0 +1,59 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import path from "node:path"; +const execFileAsync = promisify(execFile); +async function git(cwd, args) { + try { + const { stdout } = await execFileAsync("git", args, { cwd, encoding: "utf-8", timeout: 5000 }); + return stdout.trim(); + } + catch { + return ""; + } +} +export async function collectGit(workspaceRoot) { + const root = await git(workspaceRoot, ["rev-parse", "--show-toplevel"]); + if (!root) + return { repositoryInfo: [], gitRepos: [] }; + const remotesRaw = await git(root, ["remote", "-v"]); + const remote_urls = []; + const remote_names = []; + for (const line of remotesRaw.split("\n")) { + const m = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)/); + if (!m) + continue; + remote_names.push(m[1]); + remote_urls.push(m[2]); + } + const primary = remote_urls[0] ?? ""; + let repo_owner = ""; + let repo_name = path.basename(root); + const gh = primary.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/); + if (gh) { + repo_owner = gh[1]; + repo_name = gh[2]; + } + const branch = (await git(root, ["rev-parse", "--abbrev-ref", "HEAD"])) || "HEAD"; + const status = await git(root, ["status", "--porcelain", "-b"]); + const repositoryInfo = [ + { + relative_workspace_path: ".", + remote_urls, + remote_names, + repo_name, + repo_owner, + is_tracked: remote_urls.length > 0, + is_local: remote_urls.length === 0, + workspace_uri: `file://${root}`, + }, + ]; + const gitRepos = [ + { + path: root, + status: status.slice(0, 4000), + branch_name: branch, + ...(primary ? { remote_url: primary } : {}), + }, + ]; + return { repositoryInfo, gitRepos }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/index.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/index.d.ts new file mode 100644 index 00000000..c461f794 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/index.d.ts @@ -0,0 +1,5 @@ +export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, type BuildRequestContextInput, } from "./build.js"; +export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js"; +export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js"; +export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, type AdmitContextEpochInput, type AdmitContextEpochResult, type ContextEpoch, type ContextSourceSnapshot, } from "./epoch.js"; +export { HOST_PATH_BRIDGE, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, hostGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js"; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/index.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/index.js new file mode 100644 index 00000000..e54f982e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/index.js @@ -0,0 +1,5 @@ +export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, } from "./build.js"; +export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js"; +export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js"; +export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, } from "./epoch.js"; +export { HOST_PATH_BRIDGE, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, hostGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js"; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/layout.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/layout.d.ts new file mode 100644 index 00000000..04da97cb --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/layout.d.ts @@ -0,0 +1,17 @@ +export type LayoutNode = { + abs_path: string; + children_dirs: LayoutNode[]; + children_files: Array<{ + name: string; + }>; + children_were_processed: boolean; + num_files: number; +}; +/** + * Shallow project layout for RequestContext.project_layouts. + * Caps depth and entries to keep the payload small. + */ +export declare function collectProjectLayout(workspaceRoot: string, opts?: { + maxDepth?: number; + maxEntries?: number; +}): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/layout.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/layout.js new file mode 100644 index 00000000..da6e6111 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/layout.js @@ -0,0 +1,58 @@ +import { readdir, stat } from "node:fs/promises"; +import path from "node:path"; +const SKIP = new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".turbo"]); +/** + * Shallow project layout for RequestContext.project_layouts. + * Caps depth and entries to keep the payload small. + */ +export async function collectProjectLayout(workspaceRoot, opts = {}) { + const maxDepth = opts.maxDepth ?? 2; + const maxEntries = opts.maxEntries ?? 80; + return walk(path.resolve(workspaceRoot), 0, maxDepth, maxEntries); +} +async function walk(dir, depth, maxDepth, maxEntries) { + const node = { + abs_path: dir, + children_dirs: [], + children_files: [], + children_were_processed: depth < maxDepth, + num_files: 0, + }; + if (depth >= maxDepth) + return node; + let names; + try { + names = await readdir(dir); + } + catch { + node.children_were_processed = false; + return node; + } + names.sort(); + let count = 0; + for (const name of names) { + if (count >= maxEntries) + break; + if (name.startsWith(".") && name !== ".opencode" && name !== ".claude" && name !== ".agents") + continue; + if (SKIP.has(name)) + continue; + const full = path.join(dir, name); + let st; + try { + st = await stat(full); + } + catch { + continue; + } + count++; + if (st.isDirectory()) { + node.children_dirs.push(await walk(full, depth + 1, maxDepth, maxEntries)); + } + else { + node.children_files.push({ name }); + node.num_files++; + } + } + return node; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/overlay.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/overlay.d.ts new file mode 100644 index 00000000..fb27d268 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/overlay.d.ts @@ -0,0 +1,44 @@ +/** + * Epoch-hold live RequestContext overlay lists (skills, subagents, plugins). + * + * Same policy as the tool catalog: first nonempty freeze is UTF-16 by id, + * equal ids keep frozen bytes (including content), new ids append at the + * tail, shrink keeps the epoch advertisement. Discovery still runs every + * Run; this only canonicalizes what is advertised. + */ +export declare const MAX_OVERLAY_HOLDS = 256; +export type OverlaySkill = { + id: string; + full_path: string; + content: string; + description: string; +}; +export type OverlaySubagent = { + full_path: string; + name: string; + description: string; + prompt: string; +}; +export type OverlayPlugin = { + id: string; + line: string; +}; +export type OverlayHold = { + skills: OverlaySkill[]; + subagents: OverlaySubagent[]; + plugins: OverlayPlugin[]; +}; +export type OverlayWire = { + skills: Array<{ + full_path: string; + content: string; + description: string; + }>; + subagents: OverlaySubagent[]; + plugins: OverlayPlugin[]; +}; +/** Merge live discovery into the conversation overlay epoch. */ +export declare function holdCapabilityOverlay(conversationId: string, live: OverlayHold): OverlayWire; +export declare function clearOverlayHold(conversationId: string): void; +export declare function transferOverlayHold(previousConversationId: string, nextConversationId: string): void; +export declare function resetOverlayHoldsForTests(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/overlay.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/overlay.js new file mode 100644 index 00000000..f2719c05 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/overlay.js @@ -0,0 +1,96 @@ +/** + * Epoch-hold live RequestContext overlay lists (skills, subagents, plugins). + * + * Same policy as the tool catalog: first nonempty freeze is UTF-16 by id, + * equal ids keep frozen bytes (including content), new ids append at the + * tail, shrink keeps the epoch advertisement. Discovery still runs every + * Run; this only canonicalizes what is advertised. + */ +export const MAX_OVERLAY_HOLDS = 256; +const byConversationId = new Map(); +function utf16(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} +function inFixedOrder(items, idOf) { + return items + .map((item) => ({ ...item })) + .sort((left, right) => utf16(idOf(left), idOf(right))); +} +function holdList(cached, incoming, idOf) { + const orderedIncoming = inFixedOrder(incoming, idOf); + if (!cached || cached.length === 0) + return orderedIncoming; + const cachedIds = new Set(cached.map(idOf)); + const hasNew = orderedIncoming.some((item) => !cachedIds.has(idOf(item))); + if (!hasNew) + return cached; + const newcomers = inFixedOrder(orderedIncoming.filter((item) => !cachedIds.has(idOf(item))), idOf); + return [...cached, ...newcomers]; +} +function remember(conversationId, hold) { + const stored = { + skills: structuredClone(hold.skills), + subagents: structuredClone(hold.subagents), + plugins: structuredClone(hold.plugins), + }; + byConversationId.delete(conversationId); + byConversationId.set(conversationId, stored); + while (byConversationId.size > MAX_OVERLAY_HOLDS) { + const oldest = byConversationId.keys().next().value; + if (!oldest) + break; + byConversationId.delete(oldest); + } + return stored; +} +function skillId(skill) { + return skill.id || skill.full_path; +} +function toWire(hold) { + return { + skills: hold.skills.map(({ full_path, content, description }) => ({ + full_path, + content, + description, + })), + subagents: hold.subagents, + plugins: hold.plugins, + }; +} +/** Merge live discovery into the conversation overlay epoch. */ +export function holdCapabilityOverlay(conversationId, live) { + if (!conversationId) { + return toWire({ + skills: inFixedOrder(live.skills, skillId), + subagents: inFixedOrder(live.subagents, (agent) => agent.name), + plugins: inFixedOrder(live.plugins, (plugin) => plugin.id), + }); + } + const cached = byConversationId.get(conversationId); + const next = { + skills: holdList(cached?.skills, live.skills, skillId), + subagents: holdList(cached?.subagents, live.subagents, (agent) => agent.name), + plugins: holdList(cached?.plugins, live.plugins, (plugin) => plugin.id), + }; + if (next.skills.length === 0 && next.subagents.length === 0 && next.plugins.length === 0) { + byConversationId.delete(conversationId); + return toWire(next); + } + return toWire(remember(conversationId, next)); +} +export function clearOverlayHold(conversationId) { + byConversationId.delete(conversationId); +} +export function transferOverlayHold(previousConversationId, nextConversationId) { + if (!previousConversationId || !nextConversationId) + return; + const hold = byConversationId.get(previousConversationId); + byConversationId.delete(previousConversationId); + byConversationId.delete(nextConversationId); + if (!hold) + return; + remember(nextConversationId, hold); +} +export function resetOverlayHoldsForTests() { + byConversationId.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/paths.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/paths.d.ts new file mode 100644 index 00000000..07ecc3a6 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/paths.d.ts @@ -0,0 +1,75 @@ +export type HostPathEnv = NodeJS.ProcessEnv; +/** Structural host-path capability installed before an unchanged provider loads. */ +export declare const HOST_PATH_BRIDGE: unique symbol; +export type OpenCodePathBridge = { + projectConfigDirs: (workspaceRoot: string) => string[]; + globalConfigDirs: () => string[]; + /** Optional host-owned durable data root; absent means native OpenCode defaults. */ + globalDataDir?: () => string; + /** Optional host-owned cache root; absent means native OpenCode defaults. */ + globalCacheDir?: () => string; + configFileNames?: string[]; +}; +export declare function opencodeProjectConfigDirs(workspaceRoot: string): string[]; +export declare function opencodeGlobalConfigDirs(): string[]; +export declare function opencodeConfigFileNames(): string[]; +/** + * Pin the process-wide cache root. Highest precedence for {@link opencodeGlobalCacheDir}. + * Use for host-injected `Path.cache` or an explicit `createCursor({ cacheDir })`. + */ +export declare function setHostCacheDirOverride(dir: string | undefined): void; +export declare function getHostCacheDirOverride(): string | undefined; +/** Resolve the native OpenCode cache root when no host bridge is installed. */ +export declare function resolveHostCacheDir(env?: HostPathEnv): string; +/** Native OpenCode global config dir. */ +export declare function opencodeGlobalConfigDir(): string; +/** + * Host global cache dir for Cursor project metadata + model/version caches. + * + * Precedence: + * 1. {@link setHostCacheDirOverride} / `createCursor({ cacheDir })` (host `Path.cache`) + * 2. An injected structural host path bridge + * 3. Native OpenCode XDG defaults ({@link resolveHostCacheDir}) + */ +export declare function opencodeGlobalCacheDir(): string; +/** Native OpenCode global data root. */ +export declare function opencodeGlobalDataDir(env?: HostPathEnv): string; +/** Host-portable durable data root; falls back to native OpenCode. */ +export declare function hostGlobalDataDir(env?: HostPathEnv): string; +/** + * Directory for host plan files — always `/plans`, and + * never inside the user's repository. + * + * OpenCode's own `Session.plan` branches on VCS and puts plans in the worktree + * (`/.opencode/plans`) for a git project. The provider deliberately + * does *not* mirror that branch. Writing there means a throwaway plan lands in + * the user's tree untracked-but-unignored, and — because OpenCode installs + * `@opencode-ai/plugin` into every `.opencode` directory it discovers walking up + * from the cwd — creating that directory also bootstraps a project-local + * `node_modules`. The provider must add nothing to a repository it did not + * already contain. + * + * The global-data location is not a degraded fallback: it is the branch OpenCode + * itself uses when there is no VCS, and its plan agent allow-lists that path for + * `edit` / `external_directory` alongside the in-worktree one. {@link + * hostGlobalDataDir} carries an optional injected host translation; without a + * bridge it is the native OpenCode data root. + */ +export declare function hostPlansDir(_workspaceRoot?: string): string; +/** + * Cursor-compatible path slug (`/Users/a/b` → `Users-a-b`). + * Used for per-workspace metadata under the host cache. + */ +export declare function slugifyWorkspacePath(workspaceRoot: string): string; +/** + * Cursor-style project metadata root for a workspace. + * Lives at `/projects//` under the resolved OpenCode/host cache root. + * + * This is what Cursor's RequestContextEnv.project_folder / MCP + * workspace_project_dir point at — agent-tools, terminals, transcripts, etc. + * Must NOT be the git workspace, or those dumps land in the repo. + */ +export declare function opencodeProjectDir(workspaceRoot: string): string; +/** Ensure {@link opencodeProjectDir} exists (mode 0o700) and return it. */ +export declare function ensureOpencodeProjectDir(workspaceRoot: string): string; +export declare function resolveHomeRelative(p: string): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/paths.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/paths.js new file mode 100644 index 00000000..265f679b --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/paths.js @@ -0,0 +1,164 @@ +import { createHash } from "node:crypto"; +import { mkdirSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; +import { trace } from "../debug.js"; +/** Structural host-path capability installed before an unchanged provider loads. */ +export const HOST_PATH_BRIDGE = Symbol.for("opencode.host.path-bridge"); +function pathBridge() { + const value = globalThis[HOST_PATH_BRIDGE]; + if (!value || typeof value !== "object") + return undefined; + const bridge = value; + return typeof bridge.projectConfigDirs === "function" && typeof bridge.globalConfigDirs === "function" + ? bridge + : undefined; +} +function openCodeGlobalDataDir(env = process.env) { + if (env.XDG_DATA_HOME && env.XDG_DATA_HOME.length > 0) { + return path.join(env.XDG_DATA_HOME, "opencode"); + } + return path.join(resolveHome(env), ".local", "share", "opencode"); +} +function openCodeGlobalCacheDir(env = process.env) { + return path.join(xdgCacheHome(env), "opencode"); +} +function bridgeGlobalDataDir() { + const value = pathBridge()?.globalDataDir?.(); + return typeof value === "string" && value.length > 0 ? path.resolve(value) : undefined; +} +function bridgeGlobalCacheDir() { + const value = pathBridge()?.globalCacheDir?.(); + return typeof value === "string" && value.length > 0 ? path.resolve(value) : undefined; +} +export function opencodeProjectConfigDirs(workspaceRoot) { + return pathBridge()?.projectConfigDirs(path.resolve(workspaceRoot)) ?? [ + path.join(path.resolve(workspaceRoot), ".opencode"), + ]; +} +export function opencodeGlobalConfigDirs() { + return pathBridge()?.globalConfigDirs() ?? [opencodeGlobalConfigDir()]; +} +export function opencodeConfigFileNames() { + return pathBridge()?.configFileNames?.length + ? [...pathBridge().configFileNames] + : ["opencode.json", "opencode.jsonc"]; +} +/** Explicit host cache root (e.g. Effect v2 `Path.cache`, or `createCursor({ cacheDir })`). */ +let hostCacheDirOverride; +function resolveHome(env = process.env) { + return env.HOME || env.USERPROFILE || homedir(); +} +function xdgCacheHome(env = process.env) { + if (env.XDG_CACHE_HOME && env.XDG_CACHE_HOME.length > 0) + return env.XDG_CACHE_HOME; + return path.join(resolveHome(env), ".cache"); +} +/** + * Pin the process-wide cache root. Highest precedence for {@link opencodeGlobalCacheDir}. + * Use for host-injected `Path.cache` or an explicit `createCursor({ cacheDir })`. + */ +export function setHostCacheDirOverride(dir) { + hostCacheDirOverride = dir && dir.length > 0 ? path.resolve(dir) : undefined; +} +export function getHostCacheDirOverride() { + return hostCacheDirOverride; +} +/** Resolve the native OpenCode cache root when no host bridge is installed. */ +export function resolveHostCacheDir(env = process.env) { + return bridgeGlobalCacheDir() ?? openCodeGlobalCacheDir(env); +} +/** Native OpenCode global config dir. */ +export function opencodeGlobalConfigDir() { + return path.join(resolveHome(), ".config", "opencode"); +} +/** + * Host global cache dir for Cursor project metadata + model/version caches. + * + * Precedence: + * 1. {@link setHostCacheDirOverride} / `createCursor({ cacheDir })` (host `Path.cache`) + * 2. An injected structural host path bridge + * 3. Native OpenCode XDG defaults ({@link resolveHostCacheDir}) + */ +export function opencodeGlobalCacheDir() { + if (hostCacheDirOverride) + return hostCacheDirOverride; + return resolveHostCacheDir(); +} +/** Native OpenCode global data root. */ +export function opencodeGlobalDataDir(env = process.env) { + return openCodeGlobalDataDir(env); +} +/** Host-portable durable data root; falls back to native OpenCode. */ +export function hostGlobalDataDir(env = process.env) { + return bridgeGlobalDataDir() ?? openCodeGlobalDataDir(env); +} +/** + * Directory for host plan files — always `/plans`, and + * never inside the user's repository. + * + * OpenCode's own `Session.plan` branches on VCS and puts plans in the worktree + * (`/.opencode/plans`) for a git project. The provider deliberately + * does *not* mirror that branch. Writing there means a throwaway plan lands in + * the user's tree untracked-but-unignored, and — because OpenCode installs + * `@opencode-ai/plugin` into every `.opencode` directory it discovers walking up + * from the cwd — creating that directory also bootstraps a project-local + * `node_modules`. The provider must add nothing to a repository it did not + * already contain. + * + * The global-data location is not a degraded fallback: it is the branch OpenCode + * itself uses when there is no VCS, and its plan agent allow-lists that path for + * `edit` / `external_directory` alongside the in-worktree one. {@link + * hostGlobalDataDir} carries an optional injected host translation; without a + * bridge it is the native OpenCode data root. + */ +export function hostPlansDir(_workspaceRoot) { + return path.join(hostGlobalDataDir(), "plans"); +} +/** + * Cursor-compatible path slug (`/Users/a/b` → `Users-a-b`). + * Used for per-workspace metadata under the host cache. + */ +export function slugifyWorkspacePath(workspaceRoot) { + const resolved = path.resolve(workspaceRoot); + return resolved + .replace(/[^a-zA-Z0-9]/g, "-") + .split("-") + .filter(Boolean) + .join("-"); +} +/** + * Cursor-style project metadata root for a workspace. + * Lives at `/projects//` under the resolved OpenCode/host cache root. + * + * This is what Cursor's RequestContextEnv.project_folder / MCP + * workspace_project_dir point at — agent-tools, terminals, transcripts, etc. + * Must NOT be the git workspace, or those dumps land in the repo. + */ +export function opencodeProjectDir(workspaceRoot) { + const projectsRoot = path.join(opencodeGlobalCacheDir(), "projects"); + const slug = slugifyWorkspacePath(workspaceRoot); + let dir = path.join(projectsRoot, slug); + // Mirror Cursor's long-path guard so nested agent-tools paths stay usable. + if (dir.length > 92) { + const hash = createHash("sha256").update(dir).digest("hex").slice(0, 7); + dir = `${dir.slice(0, Math.min(84, dir.length))}-${hash}`; + } + return dir; +} +/** Ensure {@link opencodeProjectDir} exists (mode 0o700) and return it. */ +export function ensureOpencodeProjectDir(workspaceRoot) { + const resolved = path.resolve(workspaceRoot); + const dir = opencodeProjectDir(resolved); + mkdirSync(dir, { recursive: true, mode: 0o700 }); + trace(`project-dir: workspace=${resolved} slug=${slugifyWorkspacePath(resolved)} ` + + `dir=${dir} cache_root=${opencodeGlobalCacheDir()} ` + + `override=${hostCacheDirOverride ?? "(none)"} ` + + `xdg_cache_home=${process.env.XDG_CACHE_HOME ?? "(unset)"}`); + return dir; +} +export function resolveHomeRelative(p) { + if (p.startsWith("~/")) + return path.join(homedir(), p.slice(2)); + return p; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/plugins.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/plugins.d.ts new file mode 100644 index 00000000..510d598f --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/plugins.d.ts @@ -0,0 +1,8 @@ +import type { OpencodeJson } from "./rules.js"; +export type CollectedPlugin = { + id: string; + source: "npm" | "local"; + path?: string; +}; +/** OpenCode plugins from config + local plugin directories (metadata only). */ +export declare function collectPlugins(workspaceRoot: string, config: OpencodeJson): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/plugins.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/plugins.js new file mode 100644 index 00000000..c074f37b --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/plugins.js @@ -0,0 +1,55 @@ +import { readdir, stat } from "node:fs/promises"; +import path from "node:path"; +import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js"; +async function listLocalPlugins(dir) { + try { + await stat(dir); + } + catch { + return []; + } + const out = []; + let entries; + try { + entries = await readdir(dir); + } + catch { + return []; + } + entries.sort(); + for (const name of entries) { + if (!/\.(m?[jt]s)$/.test(name)) + continue; + const full = path.join(dir, name); + out.push({ id: name.replace(/\.(m?[jt]s)$/, ""), source: "local", path: full }); + } + return out; +} +/** OpenCode plugins from config + local plugin directories (metadata only). */ +export async function collectPlugins(workspaceRoot, config) { + const out = []; + const seen = new Set(); + for (const id of [...(config.plugin ?? []), ...(config.plugins ?? [])]) { + if (!id || seen.has(id)) + continue; + seen.add(id); + out.push({ id, source: "npm" }); + } + for (const configDir of opencodeProjectConfigDirs(workspaceRoot)) { + for (const p of await listLocalPlugins(path.join(configDir, "plugins"))) { + if (seen.has(p.id)) + continue; + seen.add(p.id); + out.push(p); + } + } + for (const configDir of opencodeGlobalConfigDirs()) { + for (const p of await listLocalPlugins(path.join(configDir, "plugins"))) { + if (seen.has(p.id)) + continue; + seen.add(p.id); + out.push(p); + } + } + return out; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/rules.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/rules.d.ts new file mode 100644 index 00000000..3112388d --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/rules.d.ts @@ -0,0 +1,25 @@ +export type CollectedRule = { + fullPath: string; + content: string; +}; +export type OpencodeJson = { + instructions?: string[]; + permission?: unknown; + plugin?: string[]; + plugins?: string[]; + mcp?: Record; +}; +export declare function findGitWorktree(start: string): Promise; +/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */ +export declare function isProjectConfigDisabled(): boolean; +/** Fetch a remote instruction with one deadline covering headers and body. */ +export declare function fetchRemoteInstruction(url: string, timeoutMs?: number): Promise; +export declare function loadMergedConfig(workspaceRoot: string): Promise; +/** + * Collect OpenCode instruction files. + */ +export declare function collectRules(workspaceRoot: string): Promise<{ + rules: CollectedRule[]; + config: OpencodeJson; + worktree: string; +}>; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/rules.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/rules.js new file mode 100644 index 00000000..51f50cfd --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/rules.js @@ -0,0 +1,239 @@ +import { readFile, readdir, stat } from "node:fs/promises"; +import { homedir } from "node:os"; +import path from "node:path"; +import { opencodeConfigFileNames, opencodeGlobalConfigDirs, opencodeProjectConfigDirs, resolveHomeRelative } from "./paths.js"; +async function exists(file) { + try { + await stat(file); + return true; + } + catch { + return false; + } +} +async function readJsonConfig(dir) { + for (const name of opencodeConfigFileNames()) { + const file = path.join(dir, name); + if (!(await exists(file))) + continue; + try { + const raw = await readFile(file, "utf-8"); + const stripped = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); + return JSON.parse(stripped); + } + catch { + return {}; + } + } + return {}; +} +export async function findGitWorktree(start) { + let dir = path.resolve(start); + for (;;) { + if (await exists(path.join(dir, ".git"))) + return dir; + const parent = path.dirname(dir); + if (parent === dir) + return path.resolve(start); + dir = parent; + } +} +async function findUp(name, start, stop) { + let dir = path.resolve(start); + const root = path.resolve(stop); + for (;;) { + const candidate = path.join(dir, name); + if (await exists(candidate)) + return candidate; + if (dir === root) + return undefined; + const parent = path.dirname(dir); + if (parent === dir) + return undefined; + dir = parent; + } +} +async function readRule(file) { + if (!(await exists(file))) + return undefined; + try { + const content = await readFile(file, "utf-8"); + if (!content.trim()) + return undefined; + return { fullPath: path.resolve(file), content }; + } + catch { + return undefined; + } +} +function globToRegExp(glob) { + const norm = glob.replace(/\\/g, "/"); + let re = "^"; + for (let i = 0; i < norm.length; i++) { + const c = norm[i]; + if (c === "*") { + if (norm[i + 1] === "*") { + re += ".*"; + i++; + if (norm[i + 1] === "/") + i++; + } + else { + re += "[^/]*"; + } + } + else if (".$^+?()[]{}|".includes(c) || c === "\\") { + re += "\\" + c; + } + else { + re += c; + } + } + return new RegExp(re + "$"); +} +async function expandGlob(pattern, workspaceRoot) { + const abs = path.isAbsolute(pattern) ? pattern : path.join(workspaceRoot, pattern); + if (!abs.includes("*")) + return (await exists(abs)) ? [abs] : []; + const out = []; + const base = abs.split("*")[0] || workspaceRoot; + const startDir = path.dirname(base.endsWith("/") ? base : base); + const regex = globToRegExp(abs); + async function walk(dir, depth) { + if (depth > 8) + return; + let entries; + try { + entries = await readdir(dir); + } + catch { + return; + } + entries.sort(); + for (const name of entries) { + if (name === "node_modules" || name === ".git") + continue; + const full = path.join(dir, name); + let st; + try { + st = await stat(full); + } + catch { + continue; + } + if (st.isDirectory()) + await walk(full, depth + 1); + else if (regex.test(full.replace(/\\/g, "/"))) + out.push(full); + } + } + await walk(startDir, 0); + return out; +} +/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */ +export function isProjectConfigDisabled() { + const value = process.env.OPENCODE_DISABLE_PROJECT_CONFIG?.toLowerCase(); + return value === "true" || value === "1"; +} +/** Fetch a remote instruction with one deadline covering headers and body. */ +export async function fetchRemoteInstruction(url, timeoutMs = 5000) { + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), Math.max(1, timeoutMs)); + try { + const res = await fetch(url, { signal: ctrl.signal }); + if (!res.ok) + return undefined; + return await res.text(); + } + catch { + return undefined; + } + finally { + clearTimeout(timer); + } +} +function mergeConfig(base, overlay) { + return { + ...base, + ...overlay, + instructions: [...(base.instructions ?? []), ...(overlay.instructions ?? [])], + plugin: [...new Set([...(base.plugin ?? []), ...(overlay.plugin ?? [])])], + plugins: [...new Set([...(base.plugins ?? []), ...(overlay.plugins ?? [])])], + mcp: { ...(base.mcp ?? {}), ...(overlay.mcp ?? {}) }, + permission: overlay.permission ?? base.permission, + }; +} +export async function loadMergedConfig(workspaceRoot) { + const globalConfig = await readJsonConfig(opencodeGlobalConfigDirs()[0] ?? ""); + if (isProjectConfigDisabled()) + return mergeConfig({}, globalConfig); + // The bridge supplies native project config roots for an unchanged plugin: + // The active host's project config directories are supplied by the path bridge; OpenCode defaults to .opencode. + // Later roots have higher precedence, matching the host's native ordering. + let projectConfig = await readJsonConfig(workspaceRoot); + for (const configDir of opencodeProjectConfigDirs(workspaceRoot)) { + projectConfig = mergeConfig(projectConfig, await readJsonConfig(configDir)); + } + return mergeConfig(globalConfig, projectConfig); +} +/** + * Collect OpenCode instruction files. + */ +export async function collectRules(workspaceRoot) { + const worktree = await findGitWorktree(workspaceRoot); + const rules = []; + const seen = new Set(); + const config = await loadMergedConfig(workspaceRoot); + const add = async (file) => { + if (!file) + return; + const resolved = path.resolve(file); + if (seen.has(resolved)) + return; + const rule = await readRule(resolved); + if (!rule) + return; + seen.add(resolved); + rules.push(rule); + }; + // Match OpenCode: OPENCODE_DISABLE_PROJECT_CONFIG skips project AGENTS/CLAUDE/CONTEXT + // discovery and project opencode.json (see loadMergedConfig). + if (!isProjectConfigDisabled()) { + for (const name of ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"]) { + const hit = await findUp(name, workspaceRoot, worktree); + if (hit) { + await add(hit); + break; + } + } + } + for (const globalDir of opencodeGlobalConfigDirs()) { + await add(path.join(globalDir, "AGENTS.md")); + } + await add(path.join(homedir(), ".claude", "CLAUDE.md")); + for (const raw of config.instructions ?? []) { + if (raw.startsWith("http://") || raw.startsWith("https://")) { + // HTTPS-only. Redirects (incl. to a local proxy) are intentional — do not + // disable follow-redirects or reject localhost/private/metadata hosts. + let remoteUrl; + try { + remoteUrl = new URL(raw); + } + catch { + continue; + } + if (remoteUrl.protocol !== "https:") + continue; + const content = await fetchRemoteInstruction(remoteUrl.href); + if (!content?.trim() || seen.has(raw)) + continue; + seen.add(raw); + rules.push({ fullPath: raw, content }); + continue; + } + const expanded = resolveHomeRelative(raw); + for (const m of await expandGlob(expanded, workspaceRoot)) + await add(m); + } + return { rules, config, worktree }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/skills.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/context/skills.d.ts new file mode 100644 index 00000000..215d3a25 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/skills.d.ts @@ -0,0 +1,11 @@ +export type CollectedSkill = { + fullPath: string; + name: string; + description: string; + content: string; +}; +/** + * Discover skills the way OpenCode does: `.opencode/skills`, global opencode, + * plus `.claude/skills` and `.agents/skills` (project + home). + */ +export declare function collectSkills(workspaceRoot: string, worktree: string): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/context/skills.js b/references/upstream-plugins/cursor-opencode-provider/dist/context/skills.js new file mode 100644 index 00000000..c83d21b5 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/context/skills.js @@ -0,0 +1,110 @@ +import { readdir, readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import { homedir } from "node:os"; +import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js"; +async function exists(p) { + try { + await stat(p); + return true; + } + catch { + return false; + } +} +function parseFrontmatter(raw) { + if (!raw.startsWith("---")) + return { body: raw }; + const end = raw.indexOf("\n---", 3); + if (end < 0) + return { body: raw }; + const fm = raw.slice(3, end).trim(); + const body = raw.slice(end + 4).replace(/^\n/, ""); + let name; + let description; + for (const line of fm.split("\n")) { + const m = line.match(/^(\w+):\s*(.*)$/); + if (!m) + continue; + const key = m[1]; + let val = m[2].trim(); + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1); + } + if (key === "name") + name = val; + if (key === "description") + description = val; + } + return { name, description, body }; +} +async function loadSkillFile(file) { + try { + const raw = await readFile(file, "utf-8"); + const { name, description, body } = parseFrontmatter(raw); + const dirName = path.basename(path.dirname(file)); + return { + fullPath: path.resolve(file), + name: name || dirName, + description: description || "", + content: body.slice(0, 20_000), + }; + } + catch { + return undefined; + } +} +async function scanSkillsRoot(root, out) { + if (!(await exists(root))) + return; + let entries; + try { + entries = await readdir(root); + } + catch { + return; + } + entries.sort(); + for (const name of entries) { + const skillMd = path.join(root, name, "SKILL.md"); + if (!(await exists(skillMd))) + continue; + const skill = await loadSkillFile(skillMd); + if (!skill) + continue; + if (!out.has(skill.name)) + out.set(skill.name, skill); + } +} +async function walkAncestorsFor(dir, rel, stop, out) { + let cur = path.resolve(dir); + const root = path.resolve(stop); + for (;;) { + await scanSkillsRoot(path.join(cur, rel), out); + if (cur === root) + break; + const parent = path.dirname(cur); + if (parent === cur) + break; + cur = parent; + } +} +/** + * Discover skills the way OpenCode does: `.opencode/skills`, global opencode, + * plus `.claude/skills` and `.agents/skills` (project + home). + */ +export async function collectSkills(workspaceRoot, worktree) { + const out = new Map(); + const home = homedir(); + for (const projectDir of opencodeProjectConfigDirs(workspaceRoot)) { + const relative = path.relative(workspaceRoot, projectDir); + await walkAncestorsFor(workspaceRoot, path.join(relative, "skills"), worktree, out); + } + for (const globalDir of opencodeGlobalConfigDirs()) { + await scanSkillsRoot(path.join(globalDir, "skills"), out); + } + await walkAncestorsFor(workspaceRoot, path.join(".claude", "skills"), worktree, out); + await scanSkillsRoot(path.join(home, ".claude", "skills"), out); + await walkAncestorsFor(workspaceRoot, path.join(".agents", "skills"), worktree, out); + await scanSkillsRoot(path.join(home, ".agents", "skills"), out); + return [...out.values()]; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/deadline.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/deadline.d.ts new file mode 100644 index 00000000..866a3708 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/deadline.d.ts @@ -0,0 +1,8 @@ +/** + * Bound a complete async operation, not only fetch(). Response body readers can + * ignore abort signals, so Promise.race remains the authoritative deadline. + * + * Rejects with `timeoutError()` before aborting so callers observe the domain + * timeout error rather than a generic AbortError. + */ +export declare function withAbortDeadline(timeoutMs: number, timeoutError: () => Error, run: (signal: AbortSignal) => Promise): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/deadline.js b/references/upstream-plugins/cursor-opencode-provider/dist/deadline.js new file mode 100644 index 00000000..3a0ec2e1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/deadline.js @@ -0,0 +1,25 @@ +/** + * Bound a complete async operation, not only fetch(). Response body readers can + * ignore abort signals, so Promise.race remains the authoritative deadline. + * + * Rejects with `timeoutError()` before aborting so callers observe the domain + * timeout error rather than a generic AbortError. + */ +export async function withAbortDeadline(timeoutMs, timeoutError, run) { + const controller = new AbortController(); + let timer; + const deadline = new Promise((_, reject) => { + timer = setTimeout(() => { + reject(timeoutError()); + controller.abort(); + }, timeoutMs); + timer.unref?.(); + }); + try { + return await Promise.race([run(controller.signal), deadline]); + } + finally { + if (timer) + clearTimeout(timer); + } +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/debug.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/debug.d.ts new file mode 100644 index 00000000..769cbd03 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/debug.d.ts @@ -0,0 +1,25 @@ +/** Soft cap for the debug log file; exceeded size triggers truncate + new header. */ +export declare const DEBUG_LOG_MAX_BYTES: number; +/** Whether `CURSOR_PROVIDER_DEBUG` is enabled for this process. */ +export declare function isDebugEnabled(): boolean; +/** Resolve the debug log path (env override or per-uid tmpdir default). */ +export declare function resolveDebugLogPath(): string; +/** + * Ensure the log directory is 0o700 and the log file exists as 0o600. + * Does **not** truncate an existing file — mid-run re-init must keep prior lines. + * Exported for tests; callers normally go through `trace`. + */ +export declare function ensureSecureDebugLog(filePath: string, options?: { + secureParent?: boolean; +}): void; +/** + * If `filePath` is at least `maxBytes`, truncate it and write a size-cap header. + * Returns true when a truncate happened. Exported for tests. + */ +export declare function truncateDebugLogIfOversized(filePath: string, maxBytes?: number): boolean; +export declare function trace(msg: string): void; +/** + * Compact path-advertising summary for RequestContext troubleshooting. + * Safe: no tokens / file contents — only workspace vs metadata roots. + */ +export declare function traceRequestContextPaths(label: string, requestContext: Record | undefined): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/debug.js b/references/upstream-plugins/cursor-opencode-provider/dist/debug.js new file mode 100644 index 00000000..f907ed93 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/debug.js @@ -0,0 +1,145 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +// Wire-level diagnostics. Opt in with CURSOR_PROVIDER_DEBUG=1 (or "true"). +// Default path mirrors Cursor CLI: $TMPDIR/cursor-provider-logs-/debug-.log +// with directory mode 0o700 and file mode 0o600. Override with CURSOR_PROVIDER_DEBUG_FILE. +// First init in a fresh file writes a header; later inits (module reload / second +// isolate sharing CURSOR_PROVIDER_DEBUG_FILE) append another header instead of +// wiping earlier EMITTED lines. Operators who want a clean run should truncate +// the override path before starting the host. Independently, when the file reaches +// DEBUG_LOG_MAX_BYTES it is size-capped (truncated + new header) so self-verify +// runs cannot grow without bound. Tokens / checksums: redact in callers. +const DEBUG_ENABLED = process.env.CURSOR_PROVIDER_DEBUG === "1" || + process.env.CURSOR_PROVIDER_DEBUG === "true"; +/** Soft cap for the debug log file; exceeded size triggers truncate + new header. */ +export const DEBUG_LOG_MAX_BYTES = 10 * 1024 * 1024; +let _traceInitialized = false; +let _debugFile; +let _debugFileUsesManagedDirectory = false; +let _announcedLogPath = false; +/** Whether `CURSOR_PROVIDER_DEBUG` is enabled for this process. */ +export function isDebugEnabled() { + return DEBUG_ENABLED; +} +/** Resolve the debug log path (env override or per-uid tmpdir default). */ +export function resolveDebugLogPath() { + if (process.env.CURSOR_PROVIDER_DEBUG_FILE) { + return process.env.CURSOR_PROVIDER_DEBUG_FILE; + } + const uid = typeof process.getuid === "function" ? process.getuid() : process.pid; + return path.join(os.tmpdir(), `cursor-provider-logs-${uid}`, `debug-${process.pid}.log`); +} +/** + * Ensure the log directory is 0o700 and the log file exists as 0o600. + * Does **not** truncate an existing file — mid-run re-init must keep prior lines. + * Exported for tests; callers normally go through `trace`. + */ +export function ensureSecureDebugLog(filePath, options = {}) { + const dir = path.dirname(filePath); + const secureParent = options.secureParent ?? true; + fs.mkdirSync(dir, secureParent + ? { recursive: true, mode: 0o700 } + : { recursive: true }); + if (secureParent) { + const stat = fs.lstatSync(dir); + if (!stat.isDirectory() || stat.isSymbolicLink()) { + throw new Error(`Debug log directory is not a real directory: ${dir}`); + } + if (typeof process.getuid === "function" && stat.uid !== process.getuid()) { + throw new Error(`Debug log directory is not owned by the current user: ${dir}`); + } + fs.chmodSync(dir, 0o700); + } + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, "", { mode: 0o600 }); + } + fs.chmodSync(filePath, 0o600); +} +function announceLogPath(filePath) { + if (_announcedLogPath) + return; + _announcedLogPath = true; + try { + // Visible in the OpenCode / terminal session so operators know where to look. + console.error(`[cursor-provider] CURSOR_PROVIDER_DEBUG logging to ${filePath}`); + } + catch { + /* ignore */ + } +} +function debugBannerLine() { + return `--- cursor-provider debug (pid ${process.pid}) ${new Date().toISOString()} ---\n`; +} +/** + * If `filePath` is at least `maxBytes`, truncate it and write a size-cap header. + * Returns true when a truncate happened. Exported for tests. + */ +export function truncateDebugLogIfOversized(filePath, maxBytes = DEBUG_LOG_MAX_BYTES) { + if (!fs.existsSync(filePath)) + return false; + const size = fs.statSync(filePath).size; + if (size < maxBytes) + return false; + fs.writeFileSync(filePath, debugBannerLine() + + `[${new Date().toISOString()}] debug: size-cap truncate file=${filePath} ` + + `wasBytes=${size} maxBytes=${maxBytes}\n`, { mode: 0o600 }); + fs.chmodSync(filePath, 0o600); + return true; +} +export function trace(msg) { + if (!DEBUG_ENABLED) + return; + try { + if (!_debugFile) { + _debugFileUsesManagedDirectory = !process.env.CURSOR_PROVIDER_DEBUG_FILE; + _debugFile = resolveDebugLogPath(); + } + if (!_traceInitialized) { + ensureSecureDebugLog(_debugFile, { secureParent: _debugFileUsesManagedDirectory }); + // Drop oversized leftovers from a prior run before appending a reinit banner. + truncateDebugLogIfOversized(_debugFile); + const banner = debugBannerLine(); + const preexisting = fs.existsSync(_debugFile) && fs.statSync(_debugFile).size > 0; + if (preexisting) { + fs.appendFileSync(_debugFile, banner); + } + else { + fs.writeFileSync(_debugFile, banner, { mode: 0o600 }); + } + _traceInitialized = true; + announceLogPath(_debugFile); + fs.appendFileSync(_debugFile, `[${new Date().toISOString()}] debug: enabled file=${_debugFile} ` + + `xdg_cache_home=${process.env.XDG_CACHE_HOME ?? "(unset)"} ` + + `cwd=${process.cwd()}` + + (preexisting ? " reinit=append" : "") + + `\n`); + } + truncateDebugLogIfOversized(_debugFile); + fs.appendFileSync(_debugFile, `[${new Date().toISOString()}] ${msg}\n`); + } + catch { + /* ignore */ + } +} +/** + * Compact path-advertising summary for RequestContext troubleshooting. + * Safe: no tokens / file contents — only workspace vs metadata roots. + */ +export function traceRequestContextPaths(label, requestContext) { + if (!DEBUG_ENABLED) + return; + const env = requestContext?.env && typeof requestContext.env === "object" + ? requestContext.env + : undefined; + const mcp = requestContext?.mcp_file_system_options && + typeof requestContext.mcp_file_system_options === "object" + ? requestContext.mcp_file_system_options + : undefined; + trace(`${label}: workspace_paths=${JSON.stringify(env?.workspace_paths ?? null)} ` + + `process_working_directory=${JSON.stringify(env?.process_working_directory ?? null)} ` + + `project_folder=${JSON.stringify(env?.project_folder ?? null)} ` + + `terminals_folder=${JSON.stringify(env?.terminals_folder ?? null)} ` + + `agent_transcripts_folder=${JSON.stringify(env?.agent_transcripts_folder ?? null)} ` + + `workspace_project_dir=${JSON.stringify(mcp?.workspace_project_dir ?? null)}`); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/errors.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/errors.d.ts new file mode 100644 index 00000000..26846de0 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/errors.d.ts @@ -0,0 +1,69 @@ +export type CursorErrorOrigin = "local-cancel" | "transport" | "server" | "protocol" | "auth"; +export type CursorErrorDiagnostics = { + statusCode?: number; + grpcStatus?: number | string; + rstCode?: number; + code?: string; + retryAfterMs?: number; +}; +export type CursorProviderErrorOptions = CursorErrorDiagnostics & { + origin: CursorErrorOrigin; + transient: boolean; + replaySafe: boolean; + cause?: unknown; +}; +/** Structured provider failure with retry and transport diagnostics. */ +export declare class CursorProviderError extends Error { + readonly origin: CursorErrorOrigin; + readonly transient: boolean; + replaySafe: boolean; + readonly statusCode?: number; + readonly grpcStatus?: number | string; + readonly rstCode?: number; + readonly code?: string; + readonly retryAfterMs?: number; + constructor(message: string, options: CursorProviderErrorOptions); +} +export declare class CursorLocalCancellationError extends CursorProviderError { + constructor(message?: string, cause?: unknown); +} +export declare class CursorTransportError extends CursorProviderError { + constructor(message: string, options?: Omit); +} +export declare class CursorServerError extends CursorProviderError { + constructor(message: string, options: Omit); +} +export declare class CursorProtocolError extends CursorProviderError { + constructor(message: string, options?: Partial & { + cause?: unknown; + }); +} +export declare class CursorAuthError extends CursorProviderError { + constructor(message?: string, options?: Partial & { + cause?: unknown; + }); +} +/** + * OpenCode's SessionRetry treats several substrings in provider error messages + * as always-retryable — notably bare "unavailable" and "exhausted", plus a + * broader regex set (resource_exhausted, service unavailable, …). + * + * When this provider has already spent its Run retry budget, or has decided a + * replay would be unsafe, the host-facing message must not re-arm that loop. + * Keep the real gRPC/HTTP code on structured fields (`code`, `grpcStatus`). + */ +export declare function sanitizeHostTerminalMessage(message: string): string; +export declare class CursorRetryExhaustedError extends CursorProviderError { + readonly attempts: number; + constructor(attempts: number, last: CursorProviderError); +} +export declare function isTransientGrpcStatus(status: number | string): boolean; +export declare function isAuthGrpcStatus(status: number | string): boolean; +export declare function cursorHttpError(operation: string, statusCode: number, diagnostics?: Omit): CursorProviderError; +export declare function cursorGrpcError(operation: string, grpcStatus: number | string, diagnostics?: Omit): CursorProviderError; +export declare function errorCode(error: unknown): string | undefined; +export declare function toCursorProviderError(error: unknown, options?: { + replaySafe: boolean; + fallback?: string; +}): CursorProviderError; +export declare function retrySuppressedError(cause: CursorProviderError, reason: string, attempt: number, maxAttempts: number): CursorProviderError; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/errors.js b/references/upstream-plugins/cursor-opencode-provider/dist/errors.js new file mode 100644 index 00000000..e1119708 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/errors.js @@ -0,0 +1,219 @@ +/** Structured provider failure with retry and transport diagnostics. */ +export class CursorProviderError extends Error { + origin; + transient; + replaySafe; + statusCode; + grpcStatus; + rstCode; + code; + retryAfterMs; + constructor(message, options) { + super(message, options.cause === undefined ? undefined : { cause: options.cause }); + this.name = "CursorProviderError"; + this.origin = options.origin; + this.transient = options.transient; + this.replaySafe = options.replaySafe; + this.statusCode = options.statusCode; + this.grpcStatus = options.grpcStatus; + this.rstCode = options.rstCode; + this.code = options.code; + this.retryAfterMs = options.retryAfterMs; + } +} +export class CursorLocalCancellationError extends CursorProviderError { + constructor(message = "Cursor request cancelled locally", cause) { + super(message, { + origin: "local-cancel", + transient: false, + replaySafe: false, + cause, + }); + this.name = "CursorLocalCancellationError"; + } +} +export class CursorTransportError extends CursorProviderError { + constructor(message, options = { + transient: true, + replaySafe: true, + }) { + super(message, { ...options, origin: "transport" }); + this.name = "CursorTransportError"; + } +} +export class CursorServerError extends CursorProviderError { + constructor(message, options) { + super(message, { ...options, origin: "server" }); + this.name = "CursorServerError"; + } +} +export class CursorProtocolError extends CursorProviderError { + constructor(message, options = {}) { + super(message, { + ...options, + origin: "protocol", + transient: false, + replaySafe: false, + }); + this.name = "CursorProtocolError"; + } +} +export class CursorAuthError extends CursorProviderError { + constructor(message = "Cursor authentication failed; reauthenticate with Cursor", options = {}) { + super(message, { + ...options, + origin: "auth", + transient: false, + replaySafe: false, + }); + this.name = "CursorAuthError"; + } +} +/** + * OpenCode's SessionRetry treats several substrings in provider error messages + * as always-retryable — notably bare "unavailable" and "exhausted", plus a + * broader regex set (resource_exhausted, service unavailable, …). + * + * When this provider has already spent its Run retry budget, or has decided a + * replay would be unsafe, the host-facing message must not re-arm that loop. + * Keep the real gRPC/HTTP code on structured fields (`code`, `grpcStatus`). + */ +export function sanitizeHostTerminalMessage(message) { + return message + .replace(/\bresource[_\s-]?exhausted\b/gi, "capacity_limit") + .replace(/\bservice[_\s-]?unavailable\b/gi, "capacity_limit") + .replace(/\bunavailable\b/gi, "capacity_limit") + .replace(/\boverloaded\b/gi, "capacity_limit") + .replace(/\bretry exhausted\b/gi, "retries finished") + .replace(/\bexhausted\b/gi, "finished"); +} +export class CursorRetryExhaustedError extends CursorProviderError { + attempts; + constructor(attempts, last) { + // Host-facing text must not contain OpenCode SessionRetry trigger words + // ("unavailable", "exhausted", …). Structured fields keep the real code. + super(`Cursor Run failed after ${attempts} attempts: ${sanitizeHostTerminalMessage(last.message)}`, { + origin: last.origin, + transient: false, + replaySafe: false, + statusCode: last.statusCode, + grpcStatus: last.grpcStatus, + rstCode: last.rstCode, + code: last.code, + retryAfterMs: last.retryAfterMs, + cause: last, + }); + this.name = "CursorRetryExhaustedError"; + this.attempts = attempts; + } +} +const TRANSIENT_NETWORK_CODES = new Set([ + "ECONNABORTED", + "ECONNREFUSED", + "ECONNRESET", + "EHOSTUNREACH", + "ENETDOWN", + "ENETRESET", + "ENETUNREACH", + "ENOTFOUND", + "EPIPE", + "ERR_HTTP2_GOAWAY_SESSION", + "ERR_HTTP2_SESSION_ERROR", + "ERR_HTTP2_STREAM_CANCEL", + "ERR_HTTP2_STREAM_ERROR", + "ETIMEDOUT", +]); +export function isTransientGrpcStatus(status) { + const normalized = String(status).toLowerCase().replaceAll("-", "_"); + return (normalized === "8" || + normalized === "13" || + normalized === "14" || + normalized === "internal" || + normalized === "resource_exhausted" || + normalized === "unavailable"); +} +export function isAuthGrpcStatus(status) { + const normalized = String(status).toLowerCase().replaceAll("-", "_"); + return (normalized === "7" || + normalized === "16" || + normalized === "permission_denied" || + normalized === "unauthenticated"); +} +export function cursorHttpError(operation, statusCode, diagnostics = {}) { + if (statusCode === 401 || statusCode === 403) { + return new CursorAuthError(`Cursor authentication failed (HTTP ${statusCode}); reauthenticate with Cursor`, { ...diagnostics, statusCode }); + } + return new CursorServerError(`${operation} HTTP ${statusCode}`, { + ...diagnostics, + statusCode, + transient: statusCode === 429 || statusCode >= 500, + replaySafe: true, + }); +} +export function cursorGrpcError(operation, grpcStatus, diagnostics = {}) { + if (isAuthGrpcStatus(grpcStatus)) { + return new CursorAuthError(`Cursor authentication failed (gRPC ${grpcStatus}); reauthenticate with Cursor`, { ...diagnostics, grpcStatus }); + } + return new CursorServerError(`${operation} gRPC status ${grpcStatus}`, { + ...diagnostics, + grpcStatus, + transient: isTransientGrpcStatus(grpcStatus), + replaySafe: true, + }); +} +export function errorCode(error) { + if (!error || typeof error !== "object") + return undefined; + const code = error.code; + return typeof code === "string" ? code : undefined; +} +export function toCursorProviderError(error, options = { replaySafe: false }) { + if (error instanceof CursorProviderError) { + error.replaySafe = options.replaySafe && error.replaySafe; + return error; + } + const code = errorCode(error); + const name = error instanceof Error ? error.name : "Error"; + const message = error instanceof Error ? error.message : ""; + if (name.startsWith("Auth") || /access token|api key|authentication/i.test(message)) { + return new CursorAuthError(undefined, { code, cause: error }); + } + const httpStatus = /\bHTTP\s+(\d{3})\b/i.exec(message)?.[1]; + if (httpStatus) { + const failure = cursorHttpError("Cursor request failed with", Number(httpStatus), { code }); + failure.replaySafe = options.replaySafe && failure.replaySafe; + return failure; + } + if (code && TRANSIENT_NETWORK_CODES.has(code)) { + return new CursorTransportError(`Cursor transport failure (${code})`, { + transient: true, + replaySafe: options.replaySafe, + code, + cause: error, + }); + } + if (error instanceof TypeError) { + return new CursorProtocolError(options.fallback ?? "Invalid Cursor provider configuration", { + code, + cause: error, + }); + } + return new CursorProtocolError(options.fallback ?? `Cursor provider failure (${name})`, { + code, + cause: error, + }); +} +export function retrySuppressedError(cause, reason, attempt, maxAttempts) { + const subject = sanitizeHostTerminalMessage(cause.message.replace(/^Cursor Run stream /, "Cursor stream ")); + return new CursorProviderError(`${subject} ${reason}; automatic retry unsafe (attempt ${attempt}/${maxAttempts})`, { + origin: cause.origin, + transient: false, + replaySafe: false, + statusCode: cause.statusCode, + grpcStatus: cause.grpcStatus, + rstCode: cause.rstCode, + code: cause.code, + retryAfterMs: cause.retryAfterMs, + cause, + }); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/host-agent-mode.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/host-agent-mode.d.ts new file mode 100644 index 00000000..77b03bdd --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/host-agent-mode.d.ts @@ -0,0 +1,32 @@ +/** + * Host-owned primary-agent synchronization for Cursor SwitchMode. + * + * The language model stays host-neutral: it records a canonical Cursor mode + * (`plan`, `spec`, `agent`, ...). A host entrypoint may install this structural + * callback when its public API can select the corresponding primary agent. + * OpenCode 2.0 uses it to select its vendor-maintained `plan` / `build` agents; + * OpenCode 1.x continues to use advertised `plan_enter` / `plan_exit` tools. + */ +export type HostAgentModeSwitchInput = { + sessionID: string; + targetModeID: string; + /** Concrete Cursor Run that owns the switch. */ + cursorSessionID?: string; +}; +export type HostAgentModeSwitchFn = (input: HostAgentModeSwitchInput) => void | Promise; +export declare function setHostAgentModeSwitch(fn: HostAgentModeSwitchFn | undefined): void; +/** Queue only when this host installed a native primary-agent switch. */ +export declare function queueHostAgentModeSwitch(input: HostAgentModeSwitchInput): boolean; +export declare function cancelHostAgentModeSwitch(sessionID: string | undefined): void; +/** + * Apply the switch only after its Cursor Run is terminal and owns no pending + * execs. This avoids changing the host's permission/catalog state underneath a + * held Run that still needs to finish or receive a tool result. + */ +export declare function flushHostAgentModeSwitch(sessionID: string | undefined, options?: { + cursorSessionID?: string; + terminal?: boolean; + pumpActive?: boolean; + pendingExecs?: number; +}): Promise; +export declare function resetHostAgentModeSwitchForTests(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/host-agent-mode.js b/references/upstream-plugins/cursor-opencode-provider/dist/host-agent-mode.js new file mode 100644 index 00000000..1e5dbac5 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/host-agent-mode.js @@ -0,0 +1,92 @@ +/** + * Host-owned primary-agent synchronization for Cursor SwitchMode. + * + * The language model stays host-neutral: it records a canonical Cursor mode + * (`plan`, `spec`, `agent`, ...). A host entrypoint may install this structural + * callback when its public API can select the corresponding primary agent. + * OpenCode 2.0 uses it to select its vendor-maintained `plan` / `build` agents; + * OpenCode 1.x continues to use advertised `plan_enter` / `plan_exit` tools. + */ +import { trace } from "./debug.js"; +let switchHostAgent; +const pending = new Map(); +const MAX_PENDING_HOST_AGENT_SWITCHES = 256; +export function setHostAgentModeSwitch(fn) { + switchHostAgent = fn; + if (!fn) + pending.clear(); +} +/** Queue only when this host installed a native primary-agent switch. */ +export function queueHostAgentModeSwitch(input) { + const sessionID = input.sessionID.trim(); + const targetModeID = input.targetModeID.trim(); + if (!switchHostAgent || !sessionID || !targetModeID) + return false; + pending.set(sessionID, { + sessionID, + targetModeID, + ...(input.cursorSessionID ? { cursorSessionID: input.cursorSessionID } : {}), + attempts: 0, + }); + while (pending.size > MAX_PENDING_HOST_AGENT_SWITCHES) { + const oldest = pending.keys().next().value; + if (!oldest) + break; + pending.delete(oldest); + } + trace(`host-agent-mode: pending sessionID=${sessionID} ` + + `cursorSessionID=${input.cursorSessionID ?? ""} target=${targetModeID}`); + return true; +} +export function cancelHostAgentModeSwitch(sessionID) { + const key = sessionID?.trim(); + if (key) + pending.delete(key); +} +/** + * Apply the switch only after its Cursor Run is terminal and owns no pending + * execs. This avoids changing the host's permission/catalog state underneath a + * held Run that still needs to finish or receive a tool result. + */ +export async function flushHostAgentModeSwitch(sessionID, options = {}) { + const key = sessionID?.trim(); + if (!key) + return false; + const state = pending.get(key); + if (!state || !switchHostAgent) + return false; + if (options.terminal !== true || options.pumpActive || (options.pendingExecs ?? 0) > 0) { + return false; + } + if (state.cursorSessionID + && options.cursorSessionID + && state.cursorSessionID !== options.cursorSessionID) { + // A newer Run reached its terminal boundary, so the owner was superseded + // before its native-agent switch could be applied. Never let that stale + // request mutate a later turn, and do not retain it indefinitely. + pending.delete(key); + trace(`host-agent-mode: discarded stale Run sessionID=${key} ` + + `owner=${state.cursorSessionID} terminal=${options.cursorSessionID}`); + return false; + } + state.attempts += 1; + try { + await switchHostAgent(state); + pending.delete(key); + trace(`host-agent-mode: switched sessionID=${key} target=${state.targetModeID}`); + return true; + } + catch (error) { + // Keep the request pending so a later explicit provider turn can retry at + // its own terminal boundary. The provider-owned mode reminder remains the + // behavioral fallback until the native host switch succeeds. + delete state.cursorSessionID; + trace(`host-agent-mode: FAILED sessionID=${key} target=${state.targetModeID} ` + + `attempts=${state.attempts} err=${error instanceof Error ? error.message : String(error)}`); + return false; + } +} +export function resetHostAgentModeSwitchForTests() { + switchHostAgent = undefined; + pending.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/host-event-bridge.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/host-event-bridge.d.ts new file mode 100644 index 00000000..f2c9f71d --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/host-event-bridge.d.ts @@ -0,0 +1,22 @@ +/** + * Optional host-neutral event capability installed by a compatibility layer. + * + * The provider does not interpret host-specific event payloads here. It only + * forwards the canonical OpenCode plugin event and the structural host values + * that were supplied to the plugin at load time. + */ +export declare const HOST_EVENT_BRIDGE: unique symbol; +export type HostEventBridge = { + handle(input: { + event: unknown; + client: unknown; + directory: string; + serverUrl: URL; + }): void | Promise; +}; +export declare function dispatchHostEventBridge(input: { + event: unknown; + client: unknown; + directory: string; + serverUrl: URL; +}): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/host-event-bridge.js b/references/upstream-plugins/cursor-opencode-provider/dist/host-event-bridge.js new file mode 100644 index 00000000..d8b117e5 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/host-event-bridge.js @@ -0,0 +1,22 @@ +/** + * Optional host-neutral event capability installed by a compatibility layer. + * + * The provider does not interpret host-specific event payloads here. It only + * forwards the canonical OpenCode plugin event and the structural host values + * that were supplied to the plugin at load time. + */ +export const HOST_EVENT_BRIDGE = Symbol.for("opencode.host.event-bridge"); +export async function dispatchHostEventBridge(input) { + const bridge = globalThis[HOST_EVENT_BRIDGE]; + if (!bridge || typeof bridge !== "object") + return; + const handle = bridge.handle; + if (typeof handle !== "function") + return; + try { + await handle.call(bridge, input); + } + catch { + // An optional compatibility capability must not break the native plugin. + } +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/image-input.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/image-input.d.ts new file mode 100644 index 00000000..ef4ebc99 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/image-input.d.ts @@ -0,0 +1,31 @@ +export declare const MAX_CURSOR_IMAGE_INPUT_BYTES: number; +export type CursorImageInput = { + data: Uint8Array; + filename: string; + mimeType: string; +}; +export type CursorHistoryImageExtraction = { + images: CursorImageInput[]; + hashes: string[]; + candidateCount: number; + duplicateCount: number; +}; +export type CursorPromptImageExtraction = CursorHistoryImageExtraction & { + userImageCount: number; +}; +export declare function hasCursorUserImages(lastUser: Record | undefined): boolean; +export declare function assertCursorUserImageSupport(lastUser: Record | undefined, supportsImages: boolean, modelId: string): void; +export declare function extractCursorUserImages(lastUser: Record | undefined, signal?: AbortSignal, maxBytes?: number): Promise; +export declare function extractCursorHistoryImages(prompt: readonly unknown[], options: { + supportsImages: boolean; + seenHashes?: ReadonlySet; + signal?: AbortSignal; + maxBytes?: number; + filenameOffset?: number; +}): Promise; +export declare function extractCursorPromptImages(prompt: readonly unknown[], lastUser: Record | undefined, options: { + supportsImages: boolean; + seenHistoryHashes?: ReadonlySet; + signal?: AbortSignal; + maxBytes?: number; +}): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/image-input.js b/references/upstream-plugins/cursor-opencode-provider/dist/image-input.js new file mode 100644 index 00000000..31600e8b --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/image-input.js @@ -0,0 +1,306 @@ +import { createHash } from "node:crypto"; +import { readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { UnsupportedFunctionalityError } from "@ai-sdk/provider"; +export const MAX_CURSOR_IMAGE_INPUT_BYTES = 20 * 1024 * 1024; +export function hasCursorUserImages(lastUser) { + return !!lastUser && Array.isArray(lastUser.content) && lastUser.content.some((part) => { + if (!part || typeof part !== "object") + return false; + const file = part; + return file.type === "file" && typeof file.mediaType === "string" && file.mediaType.startsWith("image/"); + }); +} +function unsupported(functionality, message) { + throw new UnsupportedFunctionalityError({ functionality, message }); +} +export function assertCursorUserImageSupport(lastUser, supportsImages, modelId) { + if (!hasCursorUserImages(lastUser) || supportsImages) + return; + unsupported("image input", `Cursor model ${JSON.stringify(modelId)} does not support image input`); +} +function decodeBase64(value) { + const normalized = value.replace(/\s/g, ""); + if (!normalized || !/^[A-Za-z0-9+/_-]*={0,2}$/.test(normalized)) { + return unsupported("image input", "Cursor provider received invalid base64 image data"); + } + const data = Uint8Array.from(Buffer.from(normalized, "base64")); + if (data.length === 0) { + return unsupported("image input", "Cursor provider received an empty image"); + } + return data; +} +function decodeDataUrl(value) { + if (!value.startsWith("data:")) { + return unsupported("image input", "Cursor provider supports base64-encoded image data URLs only"); + } + const commaIndex = value.indexOf(",", 5); + if (commaIndex < 0) { + return unsupported("image input", "Cursor provider supports base64-encoded image data URLs only"); + } + const metadata = value.slice(5, commaIndex); + const finalSeparator = metadata.lastIndexOf(";"); + if (finalSeparator < 0 || metadata.slice(finalSeparator + 1) !== "base64") { + return unsupported("image input", "Cursor provider supports base64-encoded image data URLs only"); + } + const firstSeparator = metadata.indexOf(";"); + const mimeType = metadata.slice(0, firstSeparator); + return { + data: decodeBase64(value.slice(commaIndex + 1)), + mimeType: mimeType || undefined, + }; +} +function inferImageMimeType(data) { + if (data.length >= 8 && + data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47 && + data[4] === 0x0d && data[5] === 0x0a && data[6] === 0x1a && data[7] === 0x0a) + return "image/png"; + if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) { + return "image/jpeg"; + } + if (data.length >= 6) { + const header = Buffer.from(data.subarray(0, 6)).toString("ascii"); + if (header === "GIF87a" || header === "GIF89a") + return "image/gif"; + } + if (data.length >= 12 && + Buffer.from(data.subarray(0, 4)).toString("ascii") === "RIFF" && + Buffer.from(data.subarray(8, 12)).toString("ascii") === "WEBP") + return "image/webp"; + return undefined; +} +function assertImageSize(size, remaining) { + if (size > remaining) { + unsupported("image input", `Cursor provider image attachments exceed the ${MAX_CURSOR_IMAGE_INPUT_BYTES / 1024 / 1024} MiB limit`); + } +} +function cursorImageBudget(value) { + if (!Number.isFinite(value)) + return MAX_CURSOR_IMAGE_INPUT_BYTES; + return Math.min(MAX_CURSOR_IMAGE_INPUT_BYTES, Math.max(0, Math.floor(value))); +} +function imageContentHash(data) { + return createHash("sha256").update(data).digest("hex"); +} +async function readResponseBytes(response, remaining) { + const declaredLength = Number(response.headers.get("content-length")); + if (Number.isFinite(declaredLength) && declaredLength > remaining) { + assertImageSize(declaredLength, remaining); + } + if (!response.body) { + const data = new Uint8Array(await response.arrayBuffer()); + assertImageSize(data.length, remaining); + return data; + } + const chunks = []; + let total = 0; + const reader = response.body.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) + break; + total += value.length; + if (total > remaining) { + await reader.cancel(); + assertImageSize(total, remaining); + } + chunks.push(value); + } + } + finally { + reader.releaseLock(); + } + const data = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + data.set(chunk, offset); + offset += chunk.length; + } + return data; +} +async function resolveImageData(value, remaining, signal) { + if (value instanceof Uint8Array) + return { data: Uint8Array.from(value) }; + if (typeof value === "string") { + return value.startsWith("data:") ? decodeDataUrl(value) : { data: decodeBase64(value) }; + } + if (value.protocol === "data:") + return decodeDataUrl(value.href); + if (value.protocol === "file:") { + const filePath = fileURLToPath(value); + const info = await stat(filePath); + assertImageSize(info.size, remaining); + return { data: Uint8Array.from(await readFile(filePath)), filename: path.basename(filePath) }; + } + if (value.protocol !== "http:" && value.protocol !== "https:") { + return unsupported("image URL input", `Cursor provider does not support image URL protocol ${JSON.stringify(value.protocol)}`); + } + const response = await fetch(value, { signal }); + if (!response.ok) { + return unsupported("image URL input", `Cursor provider could not fetch image URL (HTTP ${response.status})`); + } + return { + data: await readResponseBytes(response, remaining), + mimeType: response.headers.get("content-type")?.split(";", 1)[0]?.trim() || undefined, + filename: path.basename(value.pathname) || undefined, + }; +} +async function decodeCursorImagePart(file, remaining, signal, defaultFilename, resolveLimit = remaining) { + if (typeof file.mediaType !== "string" || !file.mediaType.startsWith("image/")) { + return unsupported("file input", `Cursor provider supports image attachments only, not ${JSON.stringify(file.mediaType)}`); + } + if (!(file.data instanceof Uint8Array) && typeof file.data !== "string" && !(file.data instanceof URL)) { + return unsupported("image input", "Cursor provider received an invalid image data value"); + } + const resolved = await resolveImageData(file.data, resolveLimit, signal); + assertImageSize(resolved.data.length, remaining); + const declaredMimeType = file.mediaType; + const mimeType = declaredMimeType === "image/*" + ? (resolved.mimeType?.startsWith("image/") ? resolved.mimeType : inferImageMimeType(resolved.data)) + : declaredMimeType; + if (!mimeType?.startsWith("image/") || mimeType === "image/*") { + return unsupported("image input", "Cursor provider could not determine the image media type"); + } + return { + data: resolved.data, + filename: (typeof file.filename === "string" && file.filename ? path.basename(file.filename) : undefined) ?? + resolved.filename ?? + defaultFilename, + mimeType, + }; +} +export async function extractCursorUserImages(lastUser, signal, maxBytes = MAX_CURSOR_IMAGE_INPUT_BYTES) { + if (!lastUser || !Array.isArray(lastUser.content)) + return []; + const byteBudget = cursorImageBudget(maxBytes); + const images = []; + let totalBytes = 0; + for (const part of lastUser.content) { + if (!part || typeof part !== "object") + continue; + const file = part; + if (file.type !== "file") + continue; + const image = await decodeCursorImagePart(file, byteBudget - totalBytes, signal, `image-${images.length + 1}`); + totalBytes += image.data.length; + images.push(image); + } + return images; +} +function pushImageFileParts(parts, content) { + for (const part of content) { + if (!part || typeof part !== "object") + continue; + const file = part; + if (file.type === "file" && + typeof file.mediaType === "string" && + file.mediaType.startsWith("image/")) + parts.push(file); + } +} +function cursorHistoryImageParts(prompt) { + const parts = []; + // Last user attachments stay owned by extractCursorUserImages so a prompt that + // still carries images on the trailing user message is not double-attached. + let lastUserIndex = -1; + for (let i = 0; i < prompt.length; i++) { + const message = prompt[i]; + if (!message || typeof message !== "object") + continue; + if (message.role === "user") + lastUserIndex = i; + } + for (let i = 0; i < prompt.length; i++) { + const message = prompt[i]; + if (!message || typeof message !== "object") + continue; + const record = message; + if (!Array.isArray(record.content)) + continue; + if (record.role === "user") { + if (i === lastUserIndex) + continue; + pushImageFileParts(parts, record.content); + continue; + } + if (record.role === "assistant") { + pushImageFileParts(parts, record.content); + continue; + } + if (record.role !== "tool") + continue; + for (const part of record.content) { + if (!part || typeof part !== "object") + continue; + const toolResult = part; + if (toolResult.type !== "tool-result" || !toolResult.output || typeof toolResult.output !== "object") { + continue; + } + const output = toolResult.output; + if (output.type !== "content" || !Array.isArray(output.value)) + continue; + for (const value of output.value) { + if (!value || typeof value !== "object") + continue; + const file = value; + if (file.type === "file-data" && + typeof file.mediaType === "string" && + file.mediaType.startsWith("image/")) + parts.push(file); + } + } + } + return parts; +} +export async function extractCursorHistoryImages(prompt, options) { + const candidates = cursorHistoryImageParts(prompt); + if (!options.supportsImages) { + return { images: [], hashes: [], candidateCount: candidates.length, duplicateCount: 0 }; + } + const maxBytes = cursorImageBudget(options.maxBytes ?? MAX_CURSOR_IMAGE_INPUT_BYTES); + const images = []; + const hashes = []; + const hashesThisTurn = new Set(); + let duplicateCount = 0; + let totalBytes = 0; + for (const file of candidates) { + // Resolve against the per-image cap first so a previously sent duplicate + // does not fail merely because little combined budget remains this turn. + const image = await decodeCursorImagePart(file, MAX_CURSOR_IMAGE_INPUT_BYTES, options.signal, `image-${(options.filenameOffset ?? 0) + images.length + 1}`, MAX_CURSOR_IMAGE_INPUT_BYTES); + const hash = imageContentHash(image.data); + if (options.seenHashes?.has(hash) || hashesThisTurn.has(hash)) { + duplicateCount++; + continue; + } + assertImageSize(image.data.length, maxBytes - totalBytes); + totalBytes += image.data.length; + images.push(image); + hashes.push(hash); + hashesThisTurn.add(hash); + } + return { images, hashes, candidateCount: candidates.length, duplicateCount }; +} +export async function extractCursorPromptImages(prompt, lastUser, options) { + const maxBytes = cursorImageBudget(options.maxBytes ?? MAX_CURSOR_IMAGE_INPUT_BYTES); + const userImages = await extractCursorUserImages(lastUser, options.signal, maxBytes); + const userBytes = userImages.reduce((total, image) => total + image.data.length, 0); + // Seed history dedupe with this-turn last-user hashes so the same bytes on an + // earlier user/assistant/tool message are not attached twice in one Run. + const seenHashes = new Set(options.seenHistoryHashes); + for (const image of userImages) + seenHashes.add(imageContentHash(image.data)); + const history = await extractCursorHistoryImages(prompt, { + supportsImages: options.supportsImages, + seenHashes, + signal: options.signal, + maxBytes: maxBytes - userBytes, + filenameOffset: userImages.length, + }); + return { + ...history, + images: [...userImages, ...history.images], + userImageCount: userImages.length, + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/image-save-tool.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/image-save-tool.d.ts new file mode 100644 index 00000000..874cdaa1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/image-save-tool.d.ts @@ -0,0 +1,19 @@ +import { executeCursorImageSave } from "./image-save.js"; +/** Build the classic OpenCode image-save tool with the host's own Zod helper. */ +export declare function createCursorImageSaveTool(factory: { + tool: (input: Record) => Record; + schema: { + string: () => any; + }; +}): Record; +/** Host-neutral JSON-schema fallback for hosts with no classic helper. */ +export declare const cursorImageSaveTool: { + description: string; + args: { + image_id: { + type: string; + description: string; + }; + }; + execute: typeof executeCursorImageSave; +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/image-save-tool.js b/references/upstream-plugins/cursor-opencode-provider/dist/image-save-tool.js new file mode 100644 index 00000000..84a566ed --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/image-save-tool.js @@ -0,0 +1,25 @@ +import { executeCursorImageSave } from "./image-save.js"; +/** Build the classic OpenCode image-save tool with the host's own Zod helper. */ +export function createCursorImageSaveTool(factory) { + return factory.tool({ + description: "Save an image that Cursor generated during this session to its target path. " + + "Takes only the id of an already-generated image — it cannot write arbitrary " + + "files, and it is not a general-purpose file writer. You do not normally call " + + "this: the Cursor provider issues it after an image is generated.", + args: { + image_id: factory.schema.string().describe("Id of the pending Cursor-generated image to save"), + }, + execute: executeCursorImageSave, + }); +} +/** Host-neutral JSON-schema fallback for hosts with no classic helper. */ +export const cursorImageSaveTool = { + description: "Save an image that Cursor generated during this session to its target path. " + + "Takes only the id of an already-generated image — it cannot write arbitrary " + + "files, and it is not a general-purpose file writer. You do not normally call " + + "this: the Cursor provider issues it after an image is generated.", + args: { + image_id: { type: "string", description: "Id of the pending Cursor-generated image to save" }, + }, + execute: executeCursorImageSave, +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/image-save.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/image-save.d.ts new file mode 100644 index 00000000..1260845a --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/image-save.d.ts @@ -0,0 +1,58 @@ +/** + * Commit a staged Cursor image to disk, gated by OpenCode's own permission. + * + * Host-neutral on purpose: `./image-save-tool.ts` performs the `tool()` value + * import from `@opencode-ai/plugin`, which the OpenCode 2.0 entrypoint must not + * pull in (see the note in `./web-search-tool.ts`). Only that file may import + * the plugin package; this module is safe on every graph. + */ +/** + * Marks a refusal that must reach Cursor as `WriteResult.permission_denied` + * rather than a generic error — the variant its own executor returns and its + * agent branches on. Both sides of this string are ours, so matching on it is + * a contract, not a guess at someone else's message. + */ +export declare const IMAGE_PERMISSION_DENIED_PREFIX = "CursorImagePermissionDenied:"; +/** The subset of OpenCode's plugin ToolContext this needs. */ +export type ImageSaveToolContext = { + worktree: string; + directory: string; + ask(input: { + permission: string; + patterns: string[]; + always: string[]; + metadata: Record; + }): Promise; +}; +export type ImageSaveResult = { + title: string; + output: string; + attachments?: Array<{ + type: "file"; + mime: string; + url: string; + filename?: string; + }>; +}; +/** + * Prove the staged target still resolves inside one of the roots it was mapped + * into. `remapCursorImageWritePath` already chose the location; this re-checks + * it against the *real* paths, because a symlink planted between mapping and + * commit could otherwise redirect the write. Mirrors the containment rule the + * correlated-edit read applies before answering with complete file content. + */ +export declare function resolveContainedImagePath(target: string, allowedRoots: readonly string[]): { + path: string; + relative: string; + root: string; +} | { + error: string; +}; +/** + * Execute the commit. `image_id` is the only input: the path and bytes come + * from this process's staging table, so a caller without a real handle — the + * model of any provider, replaying a used id, or guessing — writes nothing. + */ +export declare function executeCursorImageSave(args: { + image_id?: unknown; +}, ctx: ImageSaveToolContext): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/image-save.js b/references/upstream-plugins/cursor-opencode-provider/dist/image-save.js new file mode 100644 index 00000000..1e48986c --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/image-save.js @@ -0,0 +1,189 @@ +/** + * Commit a staged Cursor image to disk, gated by OpenCode's own permission. + * + * Host-neutral on purpose: `./image-save-tool.ts` performs the `tool()` value + * import from `@opencode-ai/plugin`, which the OpenCode 2.0 entrypoint must not + * pull in (see the note in `./web-search-tool.ts`). Only that file may import + * the plugin package; this module is safe on every graph. + */ +import fs from "node:fs"; +import path from "node:path"; +import { trace } from "./debug.js"; +import { takePendingCursorImage } from "./image-staging.js"; +/** + * Marks a refusal that must reach Cursor as `WriteResult.permission_denied` + * rather than a generic error — the variant its own executor returns and its + * agent branches on. Both sides of this string are ours, so matching on it is + * a contract, not a guess at someone else's message. + */ +export const IMAGE_PERMISSION_DENIED_PREFIX = "CursorImagePermissionDenied:"; +/** + * Prove the staged target still resolves inside one of the roots it was mapped + * into. `remapCursorImageWritePath` already chose the location; this re-checks + * it against the *real* paths, because a symlink planted between mapping and + * commit could otherwise redirect the write. Mirrors the containment rule the + * correlated-edit read applies before answering with complete file content. + */ +export function resolveContainedImagePath(target, allowedRoots) { + const roots = allowedRoots.filter(Boolean).map((root) => path.resolve(root)); + if (roots.length === 0) + return { error: "no writable root is configured" }; + const absolute = path.isAbsolute(target) ? path.resolve(target) : path.resolve(roots[0], target); + // The file need not exist yet; its closest existing ancestor must still + // resolve inside an allowed root. + let probe = absolute; + let realProbe; + while (true) { + try { + realProbe = fs.realpathSync(probe); + break; + } + catch { + const parent = path.dirname(probe); + if (parent === probe) + break; + probe = parent; + } + } + if (realProbe === undefined) + return { error: "target path could not be resolved" }; + const suffix = path.relative(probe, absolute); + const realTarget = suffix ? path.join(realProbe, suffix) : realProbe; + for (const root of roots) { + let realRoot; + try { + realRoot = fs.realpathSync(root); + } + catch { + realRoot = root; + } + const relative = path.relative(realRoot, realTarget); + if (relative && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) { + // Report the root as the caller supplied it (resolved but not realpathed) + // so callers can compare it against their own roots — on macOS a temp dir + // realpaths from /var to /private/var and would never match otherwise. + return { path: realTarget, relative, root }; + } + } + return { error: "target path resolves outside the workspace and project folder" }; +} +/** + * Resolve a root the same way the target was resolved. Comparing a realpath + * target against a lexical root misreads a symlinked project root (macOS + * /var → /private/var, or a worktree reached through a link) as external. + */ +function realRoot(root) { + const resolved = path.resolve(root); + try { + return fs.realpathSync(resolved); + } + catch { + return resolved; + } +} +/** + * Mirror of OpenCode's `containsPath`: a target inside the session directory or + * worktree is internal. A worktree of `/` is skipped there because it would + * match every absolute path and silently disable the external gate. + */ +function isInsideProject(target, worktree, directory) { + for (const root of [directory, worktree]) { + if (!root || root === "/") + continue; + const relative = path.relative(realRoot(root), target); + if (relative === "") + return true; + if (relative && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) + return true; + } + return false; +} +/** + * Execute the commit. `image_id` is the only input: the path and bytes come + * from this process's staging table, so a caller without a real handle — the + * model of any provider, replaying a used id, or guessing — writes nothing. + */ +export async function executeCursorImageSave(args, ctx) { + const imageId = typeof args.image_id === "string" ? args.image_id : ""; + if (!imageId) + return "No image id was provided, so there is nothing to save."; + const image = takePendingCursorImage(imageId); + if (!image) { + // Expired, already committed, or never existed. Identical response for all + // three: nothing here should confirm whether an id was ever valid. + return "No pending Cursor image matches that id. It may have already been saved or expired."; + } + const workspace = ctx.worktree || ctx.directory; + const contained = resolveContainedImagePath(image.path, [image.projectDir, workspace].filter((root) => !!root)); + if ("error" in contained) { + trace(`image save: refused path=${JSON.stringify(image.path)} reason=${contained.error}`); + return `Refusing to save the generated image: ${contained.error}.`; + } + // Mirror what OpenCode's own `write` tool does, in the same order: an + // external-directory gate for targets outside the project, then the `edit` + // permission. Cursor writes generated images into the project folder, which + // is under the host cache and therefore *outside* the worktree — exactly the + // case `assertExternalDirectory` exists for. Asking only for `edit` would + // quietly skip a boundary the host enforces for every one of its own writes. + const editPattern = path.relative(realRoot(workspace), contained.path); + try { + if (!isInsideProject(contained.path, workspace, ctx.directory)) { + const parentDir = path.dirname(contained.path); + const glob = path.join(parentDir, "*").replaceAll("\\", "/"); + await ctx.ask({ + permission: "external_directory", + patterns: [glob], + always: [glob], + metadata: { filepath: contained.path, parentDir }, + }); + } + await ctx.ask({ + permission: "edit", + patterns: [editPattern], + always: ["*"], + metadata: { + filepath: contained.path, + mime: image.mime, + bytes: image.data.length, + source: "cursor-generate-image", + }, + }); + } + catch (error) { + const reason = error instanceof Error ? error.message : String(error); + trace(`image save: permission refused path=${JSON.stringify(contained.path)} reason=${reason}`); + throw new Error(`${IMAGE_PERMISSION_DENIED_PREFIX} ${reason}`); + } + // Cursor writes generated images into an `assets/` subdirectory that will not + // exist on a first generation, so the parent chain is created here — after + // the permission, so a refusal leaves no directories behind. + const directory = path.dirname(contained.path); + try { + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(contained.path, image.data); + } + catch (error) { + // Most often something already occupies a path component (EEXIST/ENOTDIR). + // Say which path is blocked; the raw errno alone tells the model nothing. + const reason = error instanceof Error ? error.message : String(error); + trace(`image save: write failed path=${JSON.stringify(contained.path)} reason=${reason}`); + throw new Error(`Could not save the generated image to ${contained.path}: ${reason}. ` + + `Check that ${directory} is a writable directory.`); + } + trace(`image save: wrote path=${JSON.stringify(contained.path)} ` + + `bytes=${image.data.length} mime=${image.mime}`); + // No `attachments`, deliberately. OpenCode gates media-in-tool-result on an + // allowlist of `model.api.npm` values (@ai-sdk/anthropic, @ai-sdk/openai, …) + // that no third-party provider can join, so an image attachment is always + // "extracted to be sent as a separate user message" (message-v2.ts:299-305). + // That trailing user message makes the next turn stop looking like a tool + // continuation, so the provider opens a fresh Run and Cursor never receives + // the write result it is waiting for — observed live: the Run was superseded + // with pending=1 and a full 1.7 MB rebase was paid. Text only keeps the + // held-Run contract intact; the file is on disk and Cursor holds the image + // server-side already. + return { + title: editPattern, + output: `Saved the generated image to ${contained.path} (${image.data.length} bytes).`, + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/image-staging.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/image-staging.d.ts new file mode 100644 index 00000000..fa06a206 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/image-staging.d.ts @@ -0,0 +1,59 @@ +/** + * Staging for images Cursor generated server-side. + * + * Cursor's GenerateImage runs on its servers and hands the client finished + * bytes. OpenCode's `write` tool takes a string and additionally BOM-splits, + * diffs, and runs `Format.file()` on what it writes, so image bytes cannot pass + * through it — the bytes have to be written by provider code. + * + * That write still must raise OpenCode's permission, and `ctx.ask` is only + * reachable inside a plugin tool's `execute`. So the bytes wait here, keyed by + * a single-use id, and the tool that commits them takes only that id: no path + * and no content ever crosses the tool boundary. A tool visible in the catalog + * therefore cannot be used to write arbitrary bytes anywhere — it can only + * commit an image this provider already received, or do nothing. + * + * Approach adapted from jkalasas/opencode-antigravity-image (MIT), which + * established writing image bytes from inside a plugin tool rather than through + * the host's text `write`. No code was copied; the permission gate, the + * handle-only tool surface, and the containment checks are this provider's. + */ +export type PendingCursorImage = { + id: string; + /** Absolute target, already remapped onto a real location under this host. */ + path: string; + /** Host project folder the target was mapped into; an allowed write root. */ + projectDir?: string; + mime: string; + data: Uint8Array; + /** OpenCode session that may commit this image. */ + sessionId?: string; + createdAt: number; +}; +export type StageCursorImageInput = { + path: string; + projectDir?: string; + mime: string; + data: Uint8Array; + sessionId?: string; +}; +/** Cursor's own edit ceiling is 50 MB; images are far smaller in practice. */ +export declare const MAX_STAGED_IMAGE_BYTES: number; +/** A staged image is committed within one tool round-trip or it is stale. */ +export declare const STAGED_IMAGE_TTL_MS: number; +export declare class StagedImageTooLargeError extends Error { + constructor(bytes: number); +} +/** Hold generated bytes and return the handle the commit tool will be given. */ +export declare function stageCursorImage(input: StageCursorImageInput, now?: number): string; +/** + * Claim a staged image. Single use: a replayed or guessed id finds nothing, so + * the commit tool cannot be driven twice or by a caller that never had a real + * handle. + */ +export declare function takePendingCursorImage(id: string, now?: number): PendingCursorImage | undefined; +/** Drop a staged image whose turn ended without committing it. */ +export declare function discardPendingCursorImage(id: string): void; +/** Test seam. */ +export declare function clearPendingCursorImages(): void; +export declare function pendingCursorImageCount(): number; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/image-staging.js b/references/upstream-plugins/cursor-opencode-provider/dist/image-staging.js new file mode 100644 index 00000000..798c9d17 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/image-staging.js @@ -0,0 +1,91 @@ +/** + * Staging for images Cursor generated server-side. + * + * Cursor's GenerateImage runs on its servers and hands the client finished + * bytes. OpenCode's `write` tool takes a string and additionally BOM-splits, + * diffs, and runs `Format.file()` on what it writes, so image bytes cannot pass + * through it — the bytes have to be written by provider code. + * + * That write still must raise OpenCode's permission, and `ctx.ask` is only + * reachable inside a plugin tool's `execute`. So the bytes wait here, keyed by + * a single-use id, and the tool that commits them takes only that id: no path + * and no content ever crosses the tool boundary. A tool visible in the catalog + * therefore cannot be used to write arbitrary bytes anywhere — it can only + * commit an image this provider already received, or do nothing. + * + * Approach adapted from jkalasas/opencode-antigravity-image (MIT), which + * established writing image bytes from inside a plugin tool rather than through + * the host's text `write`. No code was copied; the permission gate, the + * handle-only tool surface, and the containment checks are this provider's. + */ +import { randomUUID } from "node:crypto"; +/** Cursor's own edit ceiling is 50 MB; images are far smaller in practice. */ +export const MAX_STAGED_IMAGE_BYTES = 50 * 1024 * 1024; +/** A staged image is committed within one tool round-trip or it is stale. */ +export const STAGED_IMAGE_TTL_MS = 10 * 60_000; +/** Bounds memory if a turn generates several images and none are committed. */ +const MAX_PENDING = 8; +const pending = new Map(); +export class StagedImageTooLargeError extends Error { + constructor(bytes) { + super(`Generated image is ${bytes} bytes, above the ${MAX_STAGED_IMAGE_BYTES} byte limit`); + this.name = "StagedImageTooLargeError"; + } +} +function prune(now) { + for (const [id, image] of pending) { + if (now - image.createdAt > STAGED_IMAGE_TTL_MS) + pending.delete(id); + } + // Oldest-first eviction: Map preserves insertion order and ids are only ever + // appended, so the first entries are the oldest. + while (pending.size >= MAX_PENDING) { + const oldest = pending.keys().next().value; + if (oldest === undefined) + break; + pending.delete(oldest); + } +} +/** Hold generated bytes and return the handle the commit tool will be given. */ +export function stageCursorImage(input, now = Date.now()) { + if (input.data.length > MAX_STAGED_IMAGE_BYTES) { + throw new StagedImageTooLargeError(input.data.length); + } + prune(now); + const id = `cursor-image-${randomUUID()}`; + pending.set(id, { + id, + path: input.path, + projectDir: input.projectDir, + mime: input.mime, + data: input.data, + sessionId: input.sessionId, + createdAt: now, + }); + return id; +} +/** + * Claim a staged image. Single use: a replayed or guessed id finds nothing, so + * the commit tool cannot be driven twice or by a caller that never had a real + * handle. + */ +export function takePendingCursorImage(id, now = Date.now()) { + const image = pending.get(id); + if (!image) + return undefined; + pending.delete(id); + if (now - image.createdAt > STAGED_IMAGE_TTL_MS) + return undefined; + return image; +} +/** Drop a staged image whose turn ended without committing it. */ +export function discardPendingCursorImage(id) { + pending.delete(id); +} +/** Test seam. */ +export function clearPendingCursorImages() { + pending.clear(); +} +export function pendingCursorImageCount() { + return pending.size; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/index.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/index.d.ts new file mode 100644 index 00000000..62ba7bf1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/index.d.ts @@ -0,0 +1,43 @@ +import { CursorPlugin } from "./plugin.js"; +import type { CursorContinuationOptions } from "./session.js"; +import type { HostToolDialect } from "./protocol/tools.js"; +export type CursorRetryOptions = { + /** Total attempts including the initial request. Default: 3. */ + maxAttempts?: number; + /** Initial full-jitter backoff ceiling. Default: 500ms. */ + baseDelayMs?: number; + /** Exponential backoff ceiling. Default: 8000ms. */ + maxDelayMs?: number; +}; +export type CreateCursorOptions = { + name: string; + accessToken?: string; + apiKey?: string; + /** API base for auth, model discovery, and GetServerConfig. */ + apiBaseURL?: string; + /** Explicit Cursor agent Run host override. */ + agentBaseURL?: string; + /** @deprecated Use agentBaseURL. Kept as the legacy agent Run host override. */ + baseURL?: string; + headers?: Record; + /** Opt in to telemetry on the GetServerConfig endpoint lookup. Defaults to false. */ + telemetryEnabled?: boolean; + /** OpenCode project / worktree directory for request_context collectors. */ + workspaceRoot?: string; + /** + * Host cache root for Cursor project metadata + model/version caches. + * Prefer the host's Path.cache (Effect v2) when available; otherwise the + * provider resolves the native OpenCode cache dir, or an injected host path bridge. + */ + cacheDir?: string; + /** Held-stream policy. Defaults: heartbeat 5s, semantic idle 120s, tool inactivity 10m. */ + continuation?: CursorContinuationOptions; + /** Fresh-turn retry policy. Defaults: 3 attempts, 500ms base, 8000ms cap. */ + retry?: CursorRetryOptions; + /** Fallback host dialect when schemas are omitted or ambiguous. Default: OpenCode 1.x. */ + defaultDialect?: HostToolDialect; +}; +export declare function createCursor(options: CreateCursorOptions): import("./plugin-core.js").CursorSdk; +export { CursorPlugin }; +export type { CursorContinuationOptions, CursorContinuationPolicy } from "./session.js"; +export default CursorPlugin; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/index.js b/references/upstream-plugins/cursor-opencode-provider/dist/index.js new file mode 100644 index 00000000..708cc9bc --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/index.js @@ -0,0 +1,16 @@ +import { createSdk } from "./plugin-core.js"; +import { CursorPlugin } from "./plugin.js"; +export function createCursor(options) { + return createSdk(options); +} +export { CursorPlugin }; +export default CursorPlugin; +// Keep root runtime exports plugin-safe. OpenCode's legacy plugin loader +// treats package-root exports as potential plugins, so extra public runtime +// APIs belong on subpaths such as "cursor-opencode-provider/errors". +// +// CursorPluginV2 is NOT re-exported here — see plugin-v2.ts. +// OpenCode's legacy plugin loader (getLegacyPlugins) iterates all exports +// and calls getServerPlugin on each; the v2 define() return is not a +// function, causing "Plugin export is not a function". Load it via +// the separate "cursor-opencode-provider/plugin/v2" export path. diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/language-model.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/language-model.d.ts new file mode 100644 index 00000000..dc3c1957 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/language-model.d.ts @@ -0,0 +1,218 @@ +import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider"; +import type { CreateCursorOptions, CursorRetryOptions } from "./index.js"; +import { type OpencodeToolDef } from "./protocol/tools.js"; +import { cursorContextUsageMetadata, type CursorContextUsageSource, type CursorConversationTokenDetails } from "./protocol/token-details.js"; +import { type ConversationBlobGraphStats } from "./protocol/blob-store.js"; +import { type CursorSession } from "./session.js"; +import { CursorProviderError } from "./errors.js"; +import type { SeedHistoryMessage } from "./protocol/request.js"; +type PromptIdentity = { + hostAgent?: string; + systemPromptHash?: string; +}; +export declare const MAX_TURN_STATE_SESSIONS = 256; +/** + * Cursor CLI's conversation **export / transfer-to-cloud** path treats state + * above 100 MiB as large (`conversation-export.ts` default `104857600`). That + * budget must not remint a sticky conversation on resume — CLI soft-reuses. + */ +export declare const MAX_CHECKPOINT_BLOB_GRAPH_BYTES: number; +/** @deprecated Never remints; kept for call-site/tests. Always false. */ +export declare function checkpointBlobGraphRequiresRebase(_stats: ConversationBlobGraphStats, _maxBytes?: number): boolean; +/** Warn-only concern for incomplete / oversized checkpoint graphs (no remint). */ +export declare function checkpointBlobGraphConcern(stats: ConversationBlobGraphStats, maxBytes?: number): "incomplete-checkpoint-graph" | "oversized-checkpoint-graph" | undefined; +export type CursorRetryPolicy = { + maxAttempts: number; + baseDelayMs: number; + maxDelayMs: number; +}; +export declare function resolveRetryPolicy(options: CursorRetryOptions | undefined): CursorRetryPolicy; +export declare function connectFrameError(payload: string): CursorProviderError; +/** Restore the last real catalog solely for lifecycle turns such as compaction. */ +export declare function restoreTurnToolCatalog(sessionKey: string, tools: OpencodeToolDef[]): void; +/** Record the authoritative host todo list for an OpenCode session. */ +export declare function rememberMirroredTodos(sessionKey: string | undefined, todos: ReadonlyArray>): void; +/** Copy of the last known host todo list for an OpenCode session, if any. */ +export declare function snapshotMirroredTodosBySession(sessionKey: string | undefined): Array> | undefined; +/** + * Prompt identity is diagnostics / persistence only and never drives remint. + * Kept as a named helper so call sites / tests that still mention identity + * churn have a single no-op definition. + */ +export declare function promptIdentityWouldRemint(_previous: PromptIdentity, _current: PromptIdentity): boolean; +type V3Part = LanguageModelV3StreamPart; +export declare function createCursorLanguageModel(modelId: string, providerId: string, options: CreateCursorOptions): LanguageModelV3; +export declare function pumpWithRecovery(input: { + initialSession: CursorSession; + controller: ReadableStreamDefaultController; + abortSignal?: AbortSignal; + retryPolicy?: CursorRetryPolicy; + recover: (recovery: CursorRunRecovery) => Promise; + onSession?: (session: CursorSession) => void; + maxRecoveries?: number; +}): Promise; +export type CursorRunRecovery = { + kind: "rebase"; +} | { + kind: "resume"; + conversationId: string; + checkpoint: Uint8Array; +}; +/** Start (or replace) the per-session heartbeat. In-flight writes from a prior attach are ignored. */ +export declare function attachSessionHeartbeat(session: CursorSession): void; +/** + * OpenCode re-sends the full tool-result history on every continuation. Prefer + * the newest result that still has a live pending exec on its tagged session. + */ +export declare function findContinuationSession(toolResults: Array<{ + sessionId: string; + execId: number; +}>): CursorSession | undefined; +/** How long a fresh-turn drain may wait for Cursor `turn_ended` after bridged settle. */ +export declare const FRESH_TURN_DRAIN_TIMEOUT_MS = 8000; +/** + * Error text written onto Cursor execs that the host abandoned by starting a + * new user turn before returning the tool result. Completing the held Run with + * this cancel (then draining to `turn_ended`) keeps the same conversation + * prefix instead of `superseded-by-new-run` mid-exec. + */ +export declare const FRESH_TURN_PENDING_CANCEL_REASON = "Host started a new user turn before this tool result was delivered"; +/** + * Before opening a new user-turn Run, finish the prior held-open Run cleanly: + * settle display-only bridged pendings, cancel any real execs still waiting on + * the host, then drain for `turn_ended`. That preserves the same + * conversation_id / checkpoint prefix instead of aborting mid-tool. + */ +export declare function preparePriorSessionForFreshTurn(openCodeSessionId: string | undefined, opts?: { + timeoutMs?: number; +}): Promise<"drained" | "settled-only" | "busy" | "none">; +/** + * Write error/reject results for every still-open non-bridged pending on the + * held Run so Cursor can finish the agent turn instead of being superseded. + * + * @returns number of pendings successfully cancelled + */ +export declare function cancelPendingExecsForFreshTurn(session: CursorSession): number; +/** + * Read remaining frames on an idle held-open Run until `turn_ended`, a + * response-requiring request we cannot answer without the host, or timeout. + * Used only on the fresh-turn settle path — not a general pump substitute. + */ +export declare function drainSessionUntilTurnEnded(session: CursorSession, opts?: { + timeoutMs?: number; +}): Promise<"turn-ended" | "busy" | "timeout" | "interrupted" | "skipped">; +/** + * Deliver trailing tool results onto a live continuation session. + * Returns the same session when writes succeed (or only bridged results were + * cleared). Returns undefined after closing the session when a write fails, so + * the caller can rebase onto a fresh Run instead of pumping a dead stream. + */ +export declare function deliverContinuationResults(session: CursorSession, trailingToolResults: ExtractedToolResult[]): CursorSession | undefined; +/** + * Read the held-open stream, emitting stream parts, until the turn boundary: + * - a tool call (exec_server_message) → emit tool-call, finish "tool-calls", + * and KEEP the session open for the result on the next doStream call; + * - turn_ended → finish "stop" and close the session; + * - transport EOF before turn_ended → throw for one fresh-Run recovery. + */ +export declare function pump(session: CursorSession, controller: ReadableStreamDefaultController, ids: { + textId: string; + reasoningId: string; +}, abortSignal?: AbortSignal): Promise; +type ExtractedToolResult = { + toolCallId: string; + sessionId: string; + execId: number; + toolName: string; + output: string; + error?: string; +}; +/** + * Tool results that form a live continuation: only the trailing run of `tool` + * messages after the last non-tool message. Mid-prompt historical tool results + * are ignored — they are conversation history, not replies for a held-open Run. + */ +export declare function extractTrailingToolResults(prompt: LanguageModelV3CallOptions["prompt"]): ExtractedToolResult[]; +/** + * Checkpointed Runs do not resend the system prompt. Keep the workspace root + * on the live user message, and require absolute `path` arguments when that is + * the host's file-tool dialect. + */ +export declare function groundCheckpointTurnText(userText: string, checkpoint: boolean, workspaceRoot: string, tools: readonly { + name?: string; + inputSchema?: unknown; +}[]): string; +/** + * Cursor's native UI interactions cannot be surfaced through the AI SDK. + * Redirect only to OpenCode tools that are genuinely advertised this turn; + * compaction keeps its dedicated summary prompt unchanged. + */ +export declare function buildOpenCodeInteractionGuidance(tools: OpencodeToolDef[], isCompaction: boolean, workspaceRoot: string): string | undefined; +/** Rough char→token estimate for mid-turn usage before TurnEnded arrives. */ +export declare function estimateTokens(chars: number): number; +/** Preserve exact request-local Cursor counters as diagnostics. */ +export declare function cursorTurnEndedProviderMetadata(te: Record, tokenDetails?: CursorConversationTokenDetails, contextSource?: CursorContextUsageSource): { + cursor: { + usageVersion: number; + inputTokensRaw: number; + outputTokensRaw: number; + cacheReadRaw: number; + cacheWriteRaw: number; + reasoningTokensRaw: number; + context?: ReturnType; + }; +}; +/** + * Prior prompt turns for a seed ConversationStateStructure. Tool results must + * never be replayed as assistant-authored prose: that teaches the model to + * counterfeit `Tool result (...)` text instead of emitting a real tool call. + * Normal rebases omit old results; compaction can retain all results and + * interrupted continuations retain only the trailing live result suffix as + * explicit OpenCode-host observations. + */ +export declare function extractPromptHistory(prompt: LanguageModelV3CallOptions["prompt"], options?: { + preserveTrailingUser?: boolean; + toolResults?: "omit" | "all" | "trailing"; +}): SeedHistoryMessage[]; +/** OpenCode session id header, if present. */ +export declare function opencodeSessionKey(callOptions: LanguageModelV3CallOptions): string | undefined; +/** + * Map OpenCode's session id header to the active Cursor conversation_id. + * Compaction resets remint via bindConversationId; otherwise the binding is + * sticky for the OpenCode session. Falls back to a random UUID with no header. + */ +export declare function resolveConversationId(callOptions: LanguageModelV3CallOptions): string; +export { sessionIdToUuid } from "./protocol/conversation-bind.js"; +/** Exported for tests — AI SDK V3 span ends that must precede finish / tool-call. */ +export declare function spanEndParts(opts: { + textStarted: boolean; + reasoningStarted: boolean; + textId: string; + reasoningId: string; +}): Array<{ + type: "text-end" | "reasoning-end"; + id: string; +}>; +/** Exported for tests — false for compaction/summary (no tools) and toolChoice none. */ +export declare function computeAllowTools(toolCount: number, toolChoice: LanguageModelV3CallOptions["toolChoice"] | undefined): boolean; +export declare function resolveTurnToolState(input: { + sessionKey?: string; + incomingTools: OpencodeToolDef[]; + toolChoice?: LanguageModelV3CallOptions["toolChoice"]; + isCompaction: boolean; + abortSignal?: AbortSignal; +}): Promise<{ + advertisedTools: OpencodeToolDef[]; + allowTools: boolean; +}>; +export declare function resolveTurnConversationReset(input: { + sessionKey?: string; + isCompaction: boolean; + historyRewrite?: boolean; + /** @deprecated Ignored — prompt identity never remints. Kept for call-site compat. */ + promptIdentity?: PromptIdentity; +}): { + reset: boolean; + reason?: "compaction" | "post-compaction-rebase" | "history-rewrite"; +}; +export declare function resetTurnStateForTests(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/language-model.js b/references/upstream-plugins/cursor-opencode-provider/dist/language-model.js new file mode 100644 index 00000000..ce69a00e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/language-model.js @@ -0,0 +1,3801 @@ +import fs from "node:fs"; +import path from "node:path"; +import { createHash } from "node:crypto"; +import { bidiRunStream, CursorRunInterruptedError, normalizeAgentRunOrigin, } from "./transport/connect.js"; +import { trace, traceRequestContextPaths } from "./debug.js"; +import { buildRunRequest, buildHeartbeat } from "./protocol/request.js"; +import { decodeFramePayload } from "./protocol/framing.js"; +import { debugWalkTurnEnded, decodeMessage, encodeMessage } from "./protocol/messages.js"; +import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, buildUnsupportedExecDeny, classifyMissingReadTarget, isUriReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildListMcpResourcesFallback, buildReadMcpResourceFallback, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, preferCorrelatedTaskDescription, remapCorrelatedEditWriteForCatalog, remapEditToolsForCatalog, buildCompleteEditReadMessages, rejectPartialReadMutation, binaryWritePayload, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, CUSTOM_LIST_MCP_RESOURCES_TOOL, CUSTOM_READ_MCP_RESOURCE_TOOL, hostToolDialectFromTools, opencodePathArg, } from "./protocol/tools.js"; +import { buildGitDiffExecMessages } from "./protocol/git-diff.js"; +import { cursorExecVariantByRequestField, describeCursorExecVariant } from "./protocol/exec-variants.js"; +import { appendCheckpointUserGrounding, appendWorkspaceRootGrounding } from "./protocol/workspace-grounding.js"; +import { progressOnlyContinuationPrompt, shouldContinueProgressOnlyTurn, } from "./protocol/progress-continuation.js"; +import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, isNativeDisplayToolCall, snapshotMirroredTodos, snapshotMirroredTodosFromReadOutput, } from "./protocol/tool-call-bridge.js"; +import { handleKvServerMessage } from "./protocol/kv.js"; +import { buildAskQuestionInteractionReply, buildAsyncAskQuestionCompletion, buildCreatePlanInteractionReply, buildSwitchModeInteractionReply, handleInteractionQuery, } from "./protocol/interactions.js"; +import { ASK_QUESTION_RESULT_FIELD, askQuestionResultFromToolOutput, askQuestionToolInput, } from "./protocol/ask-question.js"; +import { SWITCH_MODE_RESULT_FIELD, getActiveCursorMode, isBridgedCursorPlanModeActive, isCursorPlanModeActive, setActiveCursorMode, switchModeResultFromQuestionOutput, switchModeResultFromToolOutput, switchModeToolInput, takeActiveCursorModeReminder, } from "./protocol/switch-mode.js"; +import { CREATE_PLAN_NOT_APPROVED_REASON, CREATE_PLAN_RESULT_FIELD, CURSOR_PLAN_STAGE_TOOL, createPlanApproved, createPlanStageInput, } from "./protocol/create-plan.js"; +import { flushPlanExecutionKickoff, formatPlanKickoffPath, hasPlanExecutionKickoff, planExecutionKickoffState, planPathFromUri, queuePlanExecutionKickoff, takePlanExecutionKickoffWarning, } from "./plan-execution-kickoff.js"; +import { flushHostAgentModeSwitch, queueHostAgentModeSwitch, } from "./host-agent-mode.js"; +import { CURSOR_IMAGE_SAVE_TOOL, imageMimeForPath, remapCursorImageWritePath, } from "./protocol/generate-image.js"; +import { stageCursorImage } from "./image-staging.js"; +import { IMAGE_PERMISSION_DENIED_PREFIX } from "./image-save.js"; +import { getCheckpoint, setCheckpoint } from "./protocol/checkpoint.js"; +import { cursorContextUsageMetadata, decodeConversationTokenDetails, } from "./protocol/token-details.js"; +import { conversationBlobCount, inspectConversationBlobGraph, } from "./protocol/blob-store.js"; +import { bindConversationId, resolveConversationGroupId, } from "./protocol/conversation-bind.js"; +import { clearPersistedConversationState, hydrateConversationState, persistConversationState, } from "./protocol/conversation-state.js"; +import { initializeConversationPersistence } from "./protocol/conversation-persistence.js"; +import { resolveContinuationPolicy, sessionManager, } from "./session.js"; +import { CursorAuthError, CursorLocalCancellationError, CursorProtocolError, CursorProviderError, CursorRetryExhaustedError, CursorServerError, CursorTransportError, isTransientGrpcStatus, retrySuppressedError, toCursorProviderError, } from "./errors.js"; +import { readCache, cacheFilePath, resolveVariantParameters, resolveVariantMaxMode, extractCursorVariantParameters, resolveCursorWireModelId } from "./models.js"; +import { getOrBuildRequestContext } from "./context/frozen.js"; +import { admitContextEpoch, appendMidConversationMessage, resetContextEpochsForTests, } from "./context/epoch.js"; +import { workspaceRootFromRequestContext } from "./context/env.js"; +import { ensureOpencodeProjectDir, opencodeGlobalCacheDir, setHostCacheDirOverride, } from "./context/paths.js"; +import { resolveAgentUrl } from "./agent-url.js"; +import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION, CURSOR_HISTORY_REWRITE_OPTION, CURSOR_HOST_AGENT_OPTION, } from "./shared.js"; +import { isCompactionSession } from "./compaction-marker.js"; +import { resolveSessionWorkspaceRoot } from "./session-directory.js"; +import { assertCursorUserImageSupport, extractCursorPromptImages } from "./image-input.js"; +import { resolveCursorModelSupportsImages } from "./model-metadata.js"; +import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js"; +import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js"; +import { readAllFieldsStrict } from "./protocol/struct.js"; +import { cursorUsageCountersFromTurnEnded, emptyLanguageModelV3Usage, formatCursorCacheDiagnostics, formatCursorTokenCategories, formatTurnUsageValidation, occupancyUsageFromTokenDetails, occupancyValidationCounters, OPENCODE_DISPLAY_ONLY_COST_METADATA, turnEndedCounter, } from "./usage.js"; +let _availableModels; +// mtime of the cache file the last time we loaded it. Compared on each call +// so discoverModels' background refresh is picked up without a process restart. +let _availableModelsMtimeMs = -1; +// OpenCode omits tools from compaction calls. Keep the last real catalog per +// session so the new Cursor conversation is still born with tool definitions; +// execution remains disabled for the summary turn itself. +const toolCatalogBySession = new Map(); +const sentHistoryImageHashesBySession = new Map(); +// A compaction Run uses its own summary-agent system prompt. Its opaque Cursor +// checkpoint must never become the base for the resumed normal agent: doing so +// suppresses OpenCode's compacted prompt/system seed and makes Cursor narrate +// tool use instead of emitting exec requests. Rebase once on the next turn. +const postCompactionRebaseBySession = new Set(); +const promptIdentityBySession = new Map(); +export const MAX_TURN_STATE_SESSIONS = 256; +const MAX_SENT_HISTORY_IMAGES_PER_SESSION = 256; +const DEFAULT_RETRY_POLICY = { + maxAttempts: 3, + baseDelayMs: 500, + maxDelayMs: 8_000, +}; +const MAX_RETRY_ATTEMPTS = 10; +const MAX_RETRY_DELAY_MS = 30_000; +const RUN_REQUEST_DECODE_FAILED = "CURSOR_RUN_REQUEST_DECODE_FAILED"; +const RUN_REQUEST_UNSUPPORTED = "CURSOR_RUN_REQUEST_UNSUPPORTED"; +const RUN_REPLY_FAILED = "CURSOR_RUN_REPLY_FAILED"; +/** + * Cursor CLI's conversation **export / transfer-to-cloud** path treats state + * above 100 MiB as large (`conversation-export.ts` default `104857600`). That + * budget must not remint a sticky conversation on resume — CLI soft-reuses. + */ +export const MAX_CHECKPOINT_BLOB_GRAPH_BYTES = 100 * 1024 * 1024; +/** @deprecated Never remints; kept for call-site/tests. Always false. */ +export function checkpointBlobGraphRequiresRebase(_stats, _maxBytes = MAX_CHECKPOINT_BLOB_GRAPH_BYTES) { + return false; +} +/** Warn-only concern for incomplete / oversized checkpoint graphs (no remint). */ +export function checkpointBlobGraphConcern(stats, maxBytes = MAX_CHECKPOINT_BLOB_GRAPH_BYTES) { + if (!stats.complete) + return "incomplete-checkpoint-graph"; + if (stats.bytes > maxBytes) + return "oversized-checkpoint-graph"; + return undefined; +} +const RESPONSE_REQUIRED_CHANNEL_BY_FIELD = new Map([ + [2, "exec"], + [4, "kv"], + [7, "interaction"], +]); +function responseRequiredChannel(payload) { + const fields = readAllFieldsStrict(payload); + if (fields) { + const channels = fields + .map((field) => RESPONSE_REQUIRED_CHANNEL_BY_FIELD.get(field.fn)) + .filter((channel) => !!channel); + if (channels.length > 1) + return "multiple"; + return channels[0]; + } + // Request tags are single-byte because all must-reply top-level fields are <16. + const tag = payload[0]; + return tag !== undefined ? RESPONSE_REQUIRED_CHANNEL_BY_FIELD.get(tag >> 3) : undefined; +} +function retryInteger(name, value, fallback) { + const resolved = value === undefined ? fallback : value; + if (typeof resolved !== "number" || !Number.isSafeInteger(resolved) || resolved <= 0) { + throw new CursorProtocolError(`Cursor retry ${name} must be a positive integer`); + } + return resolved; +} +export function resolveRetryPolicy(options) { + if (options !== undefined && (options === null || typeof options !== "object" || Array.isArray(options))) { + throw new CursorProtocolError("Cursor retry options must be an object"); + } + for (const key of Object.keys(options ?? {})) { + if (!["maxAttempts", "baseDelayMs", "maxDelayMs"].includes(key)) { + throw new CursorProtocolError(`Unknown Cursor retry option: ${key}`); + } + } + const maxAttempts = retryInteger("maxAttempts", options?.maxAttempts, DEFAULT_RETRY_POLICY.maxAttempts); + const baseDelayMs = retryInteger("baseDelayMs", options?.baseDelayMs, DEFAULT_RETRY_POLICY.baseDelayMs); + const maxDelayMs = retryInteger("maxDelayMs", options?.maxDelayMs, DEFAULT_RETRY_POLICY.maxDelayMs); + if (maxAttempts > MAX_RETRY_ATTEMPTS) { + throw new CursorProtocolError(`Cursor retry maxAttempts must be no greater than ${MAX_RETRY_ATTEMPTS}`); + } + if (baseDelayMs > MAX_RETRY_DELAY_MS || maxDelayMs > MAX_RETRY_DELAY_MS) { + throw new CursorProtocolError(`Cursor retry delays must be no greater than ${MAX_RETRY_DELAY_MS}ms`); + } + if (baseDelayMs > maxDelayMs) { + throw new CursorProtocolError("Cursor retry baseDelayMs must be no greater than maxDelayMs"); + } + return { maxAttempts, baseDelayMs, maxDelayMs }; +} +function retryDelayMs(error, attempt, policy) { + if (error.retryAfterMs !== undefined) + return Math.min(MAX_RETRY_DELAY_MS, error.retryAfterMs); + const ceiling = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** Math.max(0, attempt - 1)); + return Math.floor(Math.random() * ceiling); +} +function sleepForRetry(delayMs, signal) { + if (signal?.aborted) + return Promise.reject(new CursorLocalCancellationError("Cursor retry cancelled")); + return new Promise((resolve, reject) => { + const timer = setTimeout(finish, delayMs); + const onAbort = () => finish(new CursorLocalCancellationError("Cursor retry cancelled")); + function finish(error) { + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + if (error) + reject(error); + else + resolve(); + } + signal?.addEventListener("abort", onAbort, { once: true }); + }); +} +function retryDelayFromValue(value) { + if (typeof value === "string") { + const seconds = /^(\d+(?:\.\d+)?)s$/.exec(value.trim()); + if (seconds) + return Math.ceil(Number(seconds[1]) * 1_000); + const protobufDelay = retryInfoProtobufDelayMs(value.trim()); + if (protobufDelay !== undefined) + return protobufDelay; + } + if (!value || typeof value !== "object") + return undefined; + const duration = value; + const seconds = Number(duration.seconds ?? 0); + const nanos = Number(duration.nanos ?? 0); + if (!Number.isFinite(seconds) || !Number.isFinite(nanos) || seconds < 0 || nanos < 0) + return undefined; + return Math.ceil(seconds * 1_000 + nanos / 1_000_000); +} +/** Decode google.rpc.RetryInfo.value without adding another protobuf schema. */ +function retryInfoProtobufDelayMs(encoded) { + if (!encoded || encoded.length > 512 || !/^[A-Za-z0-9+/_-]+={0,2}$/.test(encoded)) { + return undefined; + } + let bytes; + try { + bytes = Buffer.from(encoded.replaceAll("-", "+").replaceAll("_", "/"), "base64"); + } + catch { + return undefined; + } + const readVarint = (input, start) => { + let value = 0n; + let shift = 0n; + for (let offset = start; offset < input.length && offset < start + 10; offset++) { + const byte = input[offset]; + value |= BigInt(byte & 0x7f) << shift; + if ((byte & 0x80) === 0) + return [value, offset + 1]; + shift += 7n; + } + return undefined; + }; + const outerKey = readVarint(bytes, 0); + if (!outerKey || outerKey[0] !== 0x0an) + return undefined; + const outerLength = readVarint(bytes, outerKey[1]); + if (!outerLength || outerLength[0] > BigInt(bytes.length - outerLength[1])) + return undefined; + const duration = bytes.subarray(outerLength[1], outerLength[1] + Number(outerLength[0])); + let offset = 0; + let seconds = 0n; + let nanos = 0n; + while (offset < duration.length) { + const key = readVarint(duration, offset); + if (!key) + return undefined; + offset = key[1]; + const field = Number(key[0] >> 3n); + if (Number(key[0] & 7n) !== 0) + return undefined; + const item = readVarint(duration, offset); + if (!item) + return undefined; + offset = item[1]; + if (field === 1) + seconds = item[0]; + else if (field === 2) + nanos = item[0]; + } + if (seconds > BigInt(Number.MAX_SAFE_INTEGER) || nanos > 999999999n) + return undefined; + return Math.ceil(Number(seconds) * 1_000 + Number(nanos) / 1_000_000); +} +export function connectFrameError(payload) { + try { + const envelope = JSON.parse(payload); + const code = typeof envelope.error?.code === "string" ? envelope.error.code : "unknown"; + if (code === "unauthenticated" || code === "permission_denied") { + return new CursorAuthError(`Cursor authentication failed (${code}); reauthenticate with Cursor`, { code }); + } + let retryAfterMs = retryDelayFromValue(envelope.error?.retryAfter ?? envelope.error?.retry_after); + let hasRetryInfo = false; + if (Array.isArray(envelope.error?.details)) { + for (const detail of envelope.error.details) { + if (!detail || typeof detail !== "object") + continue; + const record = detail; + const type = record.type; + if (type === "google.rpc.RetryInfo" || (typeof type === "string" && type.endsWith("/google.rpc.RetryInfo"))) { + hasRetryInfo = true; + retryAfterMs ??= retryDelayFromValue(record.retryDelay ?? record.retry_delay ?? record.value); + } + } + } + return new CursorServerError(`Cursor API error (code=${code})`, { + transient: isTransientGrpcStatus(code) || hasRetryInfo, + replaySafe: true, + code, + retryAfterMs: retryAfterMs === undefined + ? undefined + : Math.min(MAX_RETRY_DELAY_MS, retryAfterMs), + }); + } + catch { + return new CursorProtocolError("Cursor returned a malformed Connect error envelope"); + } +} +const toolCatalogWaitersBySession = new Map(); +function publishToolCatalogWaiters(sessionKey, tools) { + const waiters = toolCatalogWaitersBySession.get(sessionKey); + if (!waiters || waiters.size === 0) + return; + toolCatalogWaitersBySession.delete(sessionKey); + for (const waiter of waiters) + waiter.resolve(tools); +} +function waitForSiblingToolCatalog(sessionKey, signal) { + const existing = toolCatalogBySession.get(sessionKey); + if (existing && existing.length > 0) { + return Promise.resolve(structuredClone(existing)); + } + if (signal?.aborted) { + return Promise.reject(new CursorLocalCancellationError("Cursor tool-catalog wait cancelled")); + } + return new Promise((resolve, reject) => { + let settled = false; + let set = toolCatalogWaitersBySession.get(sessionKey); + const finish = (tools, error) => { + if (settled) + return; + settled = true; + set?.delete(waiter); + if (set?.size === 0) + toolCatalogWaitersBySession.delete(sessionKey); + signal?.removeEventListener("abort", onAbort); + if (error) + reject(error); + else + resolve(structuredClone(tools)); + }; + const onAbort = () => finish(undefined, new CursorLocalCancellationError("Cursor tool-catalog wait cancelled")); + const waiter = { + resolve: (tools) => finish(tools), + cancel: onAbort, + }; + if (!set) { + set = new Set(); + toolCatalogWaitersBySession.set(sessionKey, set); + } + set.add(waiter); + signal?.addEventListener("abort", onAbort, { once: true }); + // Re-check after enqueue: a sibling may have published between the initial + // map lookup and waiter registration. + const raced = toolCatalogBySession.get(sessionKey); + if (raced && raced.length > 0) + finish(raced); + }); +} +function rememberToolCatalog(sessionKey, tools) { + toolCatalogBySession.delete(sessionKey); + toolCatalogBySession.set(sessionKey, structuredClone(tools)); + publishToolCatalogWaiters(sessionKey, tools); + while (toolCatalogBySession.size > MAX_TURN_STATE_SESSIONS) { + const oldest = toolCatalogBySession.keys().next().value; + if (!oldest) + break; + toolCatalogBySession.delete(oldest); + } +} +/** Restore the last real catalog solely for lifecycle turns such as compaction. */ +export function restoreTurnToolCatalog(sessionKey, tools) { + if (!sessionKey || tools.length === 0) + return; + rememberToolCatalog(sessionKey, tools); +} +function snapshotToolCatalog(sessionKey) { + if (!sessionKey) + return []; + return structuredClone(toolCatalogBySession.get(sessionKey) ?? []); +} +// Last known full host todo list per OpenCode session. Run-local +// `session.mirroredTodos` dies with the Run (turn_ended closes the session), +// but merges in later turns still need a base — same shape of problem as the +// tool catalog above, same solution. In-memory only: never enters the prompt, +// RequestContext, or persisted restart state, so the cache prefix is untouched. +// Refreshed on every todo write/read we observe; replaced wholesale, never +// merged, so it tracks host truth instead of accumulating guesses. +const mirroredTodosBySession = new Map(); +/** Record the authoritative host todo list for an OpenCode session. */ +export function rememberMirroredTodos(sessionKey, todos) { + if (!sessionKey) + return; + mirroredTodosBySession.delete(sessionKey); + mirroredTodosBySession.set(sessionKey, todos.map((t) => ({ ...t }))); + while (mirroredTodosBySession.size > MAX_TURN_STATE_SESSIONS) { + const oldest = mirroredTodosBySession.keys().next().value; + if (!oldest) + break; + mirroredTodosBySession.delete(oldest); + } +} +/** Copy of the last known host todo list for an OpenCode session, if any. */ +export function snapshotMirroredTodosBySession(sessionKey) { + if (!sessionKey) + return undefined; + const todos = mirroredTodosBySession.get(sessionKey); + return todos ? todos.map((t) => ({ ...t })) : undefined; +} +/** + * Store a bridged/observed full todo snapshot on both the live Run session + * (for merges later this turn) and the per-OpenCode-session copy (for merges + * in later turns and across checkpoint resumes). + */ +function storeMirroredTodos(session, todos) { + session.mirroredTodos = todos.map((t) => ({ ...t })); + rememberMirroredTodos(session.openCodeSessionId, session.mirroredTodos); +} +function rememberPostCompactionRebase(sessionKey) { + postCompactionRebaseBySession.delete(sessionKey); + postCompactionRebaseBySession.add(sessionKey); + while (postCompactionRebaseBySession.size > MAX_TURN_STATE_SESSIONS) { + const oldest = postCompactionRebaseBySession.values().next().value; + if (!oldest) + break; + postCompactionRebaseBySession.delete(oldest); + } +} +function normalizePromptIdentity(value) { + const hostAgent = value.hostAgent?.trim(); + const systemPromptHash = value.systemPromptHash?.trim(); + return { + ...(hostAgent ? { hostAgent } : {}), + ...(systemPromptHash ? { systemPromptHash } : {}), + }; +} +function rememberPromptIdentity(sessionKey, value) { + const normalized = normalizePromptIdentity(value); + if (!normalized.hostAgent && !normalized.systemPromptHash) + return; + promptIdentityBySession.delete(sessionKey); + promptIdentityBySession.set(sessionKey, normalized); + while (promptIdentityBySession.size > MAX_TURN_STATE_SESSIONS) { + const oldest = promptIdentityBySession.keys().next().value; + if (!oldest) + break; + promptIdentityBySession.delete(oldest); + } +} +/** + * Prompt identity is diagnostics / persistence only and never drives remint. + * Kept as a named helper so call sites / tests that still mention identity + * churn have a single no-op definition. + */ +export function promptIdentityWouldRemint(_previous, _current) { + return false; +} +function sentHistoryImageHashes(sessionKey) { + if (!sessionKey) + return undefined; + const hashes = sentHistoryImageHashesBySession.get(sessionKey); + if (!hashes) + return undefined; + sentHistoryImageHashesBySession.delete(sessionKey); + sentHistoryImageHashesBySession.set(sessionKey, hashes); + return hashes; +} +function rememberSentHistoryImageHashes(sessionKey, hashes) { + if (!sessionKey || hashes.length === 0) + return; + const remembered = sentHistoryImageHashesBySession.get(sessionKey) ?? new Set(); + for (const hash of hashes) { + remembered.delete(hash); + remembered.add(hash); + while (remembered.size > MAX_SENT_HISTORY_IMAGES_PER_SESSION) { + const oldest = remembered.values().next().value; + if (!oldest) + break; + remembered.delete(oldest); + } + } + sentHistoryImageHashesBySession.delete(sessionKey); + sentHistoryImageHashesBySession.set(sessionKey, remembered); + while (sentHistoryImageHashesBySession.size > MAX_TURN_STATE_SESSIONS) { + const oldest = sentHistoryImageHashesBySession.keys().next().value; + if (!oldest) + break; + sentHistoryImageHashesBySession.delete(oldest); + } +} +export function createCursorLanguageModel(modelId, providerId, options) { + // Host Path.cache / explicit override wins over XDG heuristics for this process. + if (options.cacheDir) + setHostCacheDirOverride(options.cacheDir); + // Start loading and pruning restart state as soon as the provider is built. + // startSession awaits this same per-cache-root initialization before binding. + void initializeConversationPersistence(opencodeGlobalCacheDir()).catch((error) => { + trace(`conversation persistence: startup load failed: ${String(error)}`); + }); + return { + specificationVersion: "v3", + provider: providerId, + modelId, + supportedUrls: {}, + async doStream(callOptions) { + return doStreamImpl(modelId, options, callOptions); + }, + async doGenerate(callOptions) { + const result = await doStreamImpl(modelId, options, callOptions); + const parts = []; + const reader = result.stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) + break; + parts.push(value); + } + return foldStreamParts(parts); + }, + }; +} +async function doStreamImpl(modelId, options, callOptions) { + // A raw `crsr_...` API key must be exchanged for a JWT before it can be used + // as a Bearer token (the plugin path does this in auth.ts). The accessToken + // path is already a JWT from OAuth/key-exchange, so we use it as-is. + // resolveBearerToken caches apiKey exchanges so we don't hit /auth/exchange + // on every turn. + const { resolveBearerToken } = await import("./auth.js"); + const token = await resolveBearerToken({ + accessToken: options.accessToken, + apiKey: options.apiKey, + baseUrl: resolveApiBaseURL(options), + }); + const prompt = callOptions.prompt; + const retryPolicy = resolveRetryPolicy(options.retry); + // pumpWithRecovery owns the complete per-turn attempt budget. Opening a + // replacement session here must be a single attempt; otherwise setup retry + // loops nest inside recovery and `maxAttempts` no longer caps total Runs. + const openSession = (startOptions) => startSession(modelId, token, callOptions, options, startOptions); + // ── Continuation vs fresh turn ── + // OpenCode embeds *all* historical tool results in every prompt. Only the + // trailing tool-message suffix (after the last assistant/user message) is a + // live continuation. Treating mid-prompt history as continuation caused + // false "orphaned tool results" errors after Cursor turn_ended and OpenCode + // started the next step with old tools still in the prompt body. + const trailingToolResults = extractTrailingToolResults(prompt); + let session = findContinuationSession(trailingToolResults); + if (session) { + // Write pending results onto the held-open Run. A dead stream closes the + // session and returns undefined so we fall through to history rebase + // instead of pumping a connection that can no longer accept writes. + session = deliverContinuationResults(session, trailingToolResults); + } + if (!session) { + const sessionKey = opencodeSessionKey(callOptions); + if (!session && trailingToolResults.length > 0) { + // True continuation (prompt ends with tool results) but the held-open Run + // is gone (or its write path just failed). Rebase the complete OpenCode + // prompt onto a fresh conversation: its seed history includes the + // completed tool result, so no result or advertised tool is lost and + // Cursor can continue instead of deadlocking. + const ids = trailingToolResults.map((r) => `${r.sessionId}:${r.execId}`).join(","); + trace(`continuation: ${trailingToolResults.length} interrupted trailing tool result(s) [${ids}] — rebasing fresh Run`); + session = await openSession({ recovery: { kind: "rebase" } }); + } + else { + // Fresh turn (prompt ends with user/assistant text). Historical tool + // results may exist mid-prompt; they are not live exec replies. + const historical = extractToolResults(prompt).length; + if (historical > 0) { + trace(`fresh turn: ignoring ${historical} historical tool result(s) (not trailing)`); + } + // Host may start a new user turn while the prior Run still has pending + // tools. Finish that Run on the same conversation (cancel stranded execs, + // drain turn_ended) before opening the new Run so the checkpoint prefix + // is preserved. registerSession will not close a prior Run that still + // has real pending execs; a failed prepare leaves that Run held. + try { + await preparePriorSessionForFreshTurn(sessionKey); + } + catch (error) { + trace(`fresh turn: prepare-prior failed — opening the new Run and leaving ` + + `any still-pending prior held — ${error.message}`); + } + session = await openSession(); + } + } + let activeSession = session; + return { + stream: new ReadableStream({ + async pull(controller) { + // The outer try is a safety net for any throw that escapes pump() — + // e.g. an unhandled decode/gunzip error or a frames-iterator throw on + // a non-200 HTTP/2 response. Without it the pull promise rejects, the + // ReadableStream errors, and the session is never cleaned up. + try { + try { + controller.enqueue({ type: "stream-start", warnings: [] }); + } + catch (e) { + // Controller already cancelled by the consumer — stop pumping. + trace(`pull: stream-start enqueue failed (cancelled) err=${e.message}`); + return; + } + activeSession = await pumpWithRecovery({ + initialSession: activeSession, + controller, + abortSignal: callOptions.abortSignal, + retryPolicy, + recover: (recovery) => openSession({ recovery }), + onSession: (next) => { activeSession = next; }, + }); + try { + controller.close(); + } + catch (e) { + trace(`pull: close failed (already closed/cancelled) err=${e.message}`); + } + // pumpWithRecovery has returned only after the Run reached terminal + // turn_ended and endPump cleared ownership. This is the first safe + // point to start a second OpenCode generation. A failed prior handoff + // is retried only because this explicit new provider turn reached its + // own terminal boundary — never from a timer or tight loop. + const kickoff = await flushPlanExecutionKickoff(activeSession.openCodeSessionId, { + cursorSessionID: activeSession.sessionId, + terminal: activeSession.closed, + pumpActive: activeSession.pumpActive || activeSession.pumpOwner != null, + pendingExecs: activeSession.pending.size, + }); + if (!kickoff) { + const state = planExecutionKickoffState(activeSession.openCodeSessionId); + if (state?.status === "failed") { + setActiveCursorMode(activeSession.openCodeSessionId, "plan"); + } + } + await flushHostAgentModeSwitch(activeSession.openCodeSessionId, { + cursorSessionID: activeSession.sessionId, + terminal: activeSession.closed, + pumpActive: activeSession.pumpActive || activeSession.pumpOwner != null, + pendingExecs: activeSession.pending.size, + }); + } + catch (e) { + activeSession.pumpActive = false; + trace(`pull: pump threw (cleaning up): ${e.message}`); + sessionManager.close(activeSession); + try { + controller.error(e instanceof Error ? e : new Error(String(e))); + } + catch { + /* controller already errored/closed */ + } + } + }, + cancel() { + // OpenCode cancels the ReadableStream after "tool-calls"; keep the + // Cursor Run stream alive so the next doStream can write results. + trace("ReadableStream cancel() → closeUnlessPending"); + sessionManager.closeUnlessPending(activeSession); + }, + }), + }; +} +export async function pumpWithRecovery(input) { + let session = input.initialSession; + const retryPolicy = input.retryPolicy ?? { + ...DEFAULT_RETRY_POLICY, + maxAttempts: (input.maxRecoveries ?? 1) + 1, + }; + const maxRecoveries = retryPolicy.maxAttempts - 1; + input.onSession?.(session); + for (let attempt = 0;; attempt++) { + const pumpedSession = session; + const pumpOwner = Symbol("cursor-pump"); + sessionManager.beginPump(pumpedSession, pumpOwner); + try { + await pump(pumpedSession, input.controller, { + textId: crypto.randomUUID(), + reasoningId: crypto.randomUUID(), + }, input.abortSignal); + return session; + } + catch (error) { + const failure = toCursorProviderError(error, { + replaySafe: error instanceof CursorProviderError ? error.replaySafe : false, + fallback: "Cursor Run interrupted", + }); + if (!failure.transient) + throw failure; + const checkpoint = pumpedSession.resumeCheckpoint; + if (!failure.replaySafe && !checkpoint) { + throw retrySuppressedError(failure, "after visible output or stateful server activity", attempt + 1, maxRecoveries + 1); + } + if (attempt >= maxRecoveries) { + throw new CursorRetryExhaustedError(attempt + 1, failure); + } + trace(`Run interrupted: sessionId=${pumpedSession.sessionId} attempt=${attempt + 1}/${maxRecoveries} ` + + `err=${failure.message} — ${checkpoint ? `resuming ${checkpoint.length}B checkpoint` : "rebasing fresh Run"}`); + sessionManager.close(pumpedSession, "remote-error", failure); + const delayMs = retryDelayMs(failure, attempt + 1, retryPolicy); + trace(`Run retry backoff: attempt=${attempt + 1}/${maxRecoveries} delayMs=${delayMs}`); + await sleepForRetry(delayMs, input.abortSignal); + const recovery = checkpoint + ? { + kind: "resume", + conversationId: pumpedSession.conversationId, + checkpoint: Uint8Array.from(checkpoint), + } + : { kind: "rebase" }; + session = await input.recover(recovery); + if (recovery.kind === "resume") { + session.usageEstimate = { ...pumpedSession.usageEstimate }; + session.editToolCalls = new Map(pumpedSession.editToolCalls); + // mirroredTodos rides along via rememberMirroredTodos (per-OpenCode- + // session, seeded in startSession) — no handoff needed here. + } + input.onSession?.(session); + } + finally { + sessionManager.endPump(pumpedSession, pumpOwner); + } + } +} +const heartbeatWritePendingBySession = new WeakMap(); +const heartbeatGenerationBySession = new WeakMap(); +function bumpHeartbeatGeneration(session) { + const generation = (heartbeatGenerationBySession.get(session) ?? 0) + 1; + heartbeatGenerationBySession.set(session, generation); + heartbeatWritePendingBySession.set(session, false); + return generation; +} +function isCurrentHeartbeatGeneration(session, generation) { + return heartbeatGenerationBySession.get(session) === generation; +} +/** Start (or replace) the per-session heartbeat. In-flight writes from a prior attach are ignored. */ +export function attachSessionHeartbeat(session) { + session.heartbeatCancel?.(); + if (session.heartbeat) { + clearInterval(session.heartbeat); + session.heartbeat = null; + } + const generation = bumpHeartbeatGeneration(session); + const interval = setInterval(() => { + if (session.closed || !isCurrentHeartbeatGeneration(session, generation)) + return; + if (heartbeatWritePendingBySession.get(session)) { + trace(`heartbeat skipped: prior heartbeat write is still pending sessionId=${session.sessionId}`); + return; + } + heartbeatWritePendingBySession.set(session, true); + const stream = session.stream; + void writeWithBackpressure(stream, buildHeartbeat(), "heartbeat") + .then(() => { + if (!isCurrentHeartbeatGeneration(session, generation) || session.closed) + return; + sessionManager.recordHeartbeatWrite(session); + }) + .catch((cause) => { + if (!isCurrentHeartbeatGeneration(session, generation) || session.closed) + return; + sessionManager.close(session, "heartbeat-write-failed", toCursorProviderError(cause, { + replaySafe: false, + fallback: "Cursor heartbeat write failed", + })); + }) + .finally(() => { + if (isCurrentHeartbeatGeneration(session, generation)) { + heartbeatWritePendingBySession.set(session, false); + } + }); + }, session.policy.heartbeatMs); + interval.unref?.(); + session.heartbeat = interval; + session.heartbeatCancel = () => { + if (session.heartbeat) + clearInterval(session.heartbeat); + session.heartbeat = null; + if (isCurrentHeartbeatGeneration(session, generation)) + bumpHeartbeatGeneration(session); + }; +} +async function startSession(modelId, token, callOptions, options, startOptions) { + const continuationPolicy = resolveContinuationPolicy(options.continuation); + const prompt = callOptions.prompt; + const incomingTools = extractTools(callOptions); + const sessionKey = opencodeSessionKey(callOptions); + const cacheDir = opencodeGlobalCacheDir(); + if (sessionKey) { + const restored = await hydrateConversationState(cacheDir, sessionKey).catch((error) => { + trace(`conversation persistence: restore failed sessionKey=${sessionKey}: ${String(error)}`); + return undefined; + }); + if (restored?.postCompactionRebase) + rememberPostCompactionRebase(sessionKey); + if (restored?.toolCatalog.length) + restoreTurnToolCatalog(sessionKey, restored.toolCatalog); + if (restored?.hostAgent || restored?.systemPromptHash) { + rememberPromptIdentity(sessionKey, { + ...(restored.hostAgent ? { hostAgent: restored.hostAgent } : {}), + ...(restored.systemPromptHash ? { systemPromptHash: restored.systemPromptHash } : {}), + }); + } + } + const providerOptions = callOptions.providerOptions?.cursor; + const hostAgent = typeof providerOptions?.[CURSOR_HOST_AGENT_OPTION] === "string" + ? String(providerOptions[CURSOR_HOST_AGENT_OPTION]).trim() || undefined + : undefined; + // The classic plugin marks OpenCode's agent="compaction" through chat.params. + // OpenCode 2.0 removed that hook, so its plugin writes the same request-local + // option from session hooks. The session marker is a fallback only when a + // host does not preserve mutable options; explicit false prevents concurrent + // title/generate calls from inheriting a compaction marker. + // Do not infer this from tools/toolChoice: standalone no-tool calls are valid. + const compactionOption = providerOptions?.[CURSOR_COMPACTION_OPTION]; + const isCompaction = compactionOption === true || (compactionOption === undefined && isCompactionSession(sessionKey)); + const toolState = await resolveTurnToolState({ + sessionKey, + incomingTools, + toolChoice: callOptions.toolChoice, + isCompaction, + abortSignal: callOptions.abortSignal, + }); + const tools = toolState.advertisedTools; + const webToolAliases = buildCustomWebToolAliases(tools); + const cursorTools = webToolAliases.advertisedTools; + for (const [alias, candidates] of webToolAliases.ambiguous) { + trace(`web tool alias skipped: alias=${alias} ambiguous=[${candidates.join(",")}]`); + } + for (const [alias, original] of webToolAliases.aliases) { + trace(`web tool alias: ${alias} -> ${original}`); + } + const allowTools = toolState.allowTools; + const discoveredSubagentCatalog = extractHostSubagentCatalog(cursorTools); + let recovery = startOptions?.recovery; + let resumeRecovery = recovery?.kind === "resume" ? recovery : undefined; + let resuming = !!resumeRecovery; + const lifecycle = !allowTools && !isCompaction && !recovery; + // v1 sets `options.workspaceRoot` correctly per invocation (`input.directory`, + // one plugin instance per project). OpenCode 2.0 runs one daemon across many + // projects, so its static option is only a last-resort fallback. Prefer the + // per-request `x-opencode-directory` header, then the session mark recorded + // from `session.hook("context")` via `getSessionDirectory`. + const workspaceRoot = resolveSessionWorkspaceRoot({ + sessionKey, + headers: callOptions.headers, + workspaceRoot: options.workspaceRoot, + }); + const baseSystemPrompt = extractSystemPrompt(prompt); + const interactionGuidance = buildOpenCodeInteractionGuidance(cursorTools, isCompaction, workspaceRoot); + // Prompt-identity diagnostics are filled after Context Epoch admission below + // (baseline hash is the epoch baseline, not a per-turn host hash). + let frozenSystemPromptHash; + const resetState = resolveTurnConversationReset({ + sessionKey, + isCompaction, + historyRewrite: providerOptions?.[CURSOR_HISTORY_REWRITE_OPTION] === true, + }); + // Compaction must not reuse the prior conversation; its first normal turn + // must also rebase so the summary-agent checkpoint cannot replace the normal + // system prompt and OpenCode's newly compacted history. + let bound = resuming + ? { conversationId: resumeRecovery.conversationId, reset: false, previousId: undefined } + : bindConversationId(sessionKey, { + reset: resetState.reset || recovery?.kind === "rebase", + ephemeral: lifecycle, + }); + let conversationState = lifecycle + ? undefined + : resuming + ? resumeRecovery.checkpoint + : (bound.reset ? undefined : getCheckpoint(bound.conversationId)); + let checkpointGraph = conversationState + ? inspectConversationBlobGraph(bound.conversationId, conversationState) + : { count: 0, bytes: 0, complete: true }; + const forcedResetReason = undefined; + // CLI soft-reuses incomplete / oversized graphs (100 MiB is export-only). + // Never remint — warn and keep the sticky conversation + checkpoint. + if (conversationState) { + const concern = checkpointBlobGraphConcern(checkpointGraph); + if (concern) { + trace(`checkpoint state warning: reason=${concern} ` + + `conversationId=${bound.conversationId} checkpointBytes=${conversationState.length} ` + + `blobCount=${checkpointGraph.count} blobBytes=${checkpointGraph.bytes} ` + + `limitBytes=${MAX_CHECKPOINT_BLOB_GRAPH_BYTES} ` + + `detail=${checkpointGraph.fallbackReason ?? "-"} action=warm-reuse`); + } + } + const conversationId = bound.conversationId; + const conversationGroupId = resolveConversationGroupId(sessionKey, conversationId); + if (bound.reset) { + if (sessionKey) { + await clearPersistedConversationState(cacheDir, sessionKey, bound.previousId).catch((error) => { + trace(`conversation persistence: reset cleanup failed sessionKey=${sessionKey}: ${String(error)}`); + }); + } + trace(`conversation reset: reason=${forcedResetReason ?? (recovery?.kind === "rebase" ? "interrupted-run" : (resetState.reason ?? "unknown"))} ` + + `sessionKey=${sessionKey ?? "(none)"} ` + + `previousId=${bound.previousId ?? "-"} → conversationId=${conversationId}`); + } + const lastUser = [...prompt].reverse().find((message) => message.role === "user"); + let userText = recovery?.kind === "rebase" + ? "Continue the interrupted turn from the conversation history above. Do not repeat completed work." + : (extractUserText(lastUser) || "."); + // After an approved SwitchMode, inject the Cursor CLI-shaped mode reminder + // (same contract the CLI uses after flipping unifiedMode). + const startedWithCheckpoint = !!conversationState; + if (startedWithCheckpoint) { + userText = groundCheckpointTurnText(userText, true, workspaceRoot, cursorTools); + } + const activeMode = getActiveCursorMode(sessionKey); + const nativePlanPromptOwnsMode = hostAgent === "plan" + && (activeMode === "plan" || activeMode === "spec"); + // Mode/kickoff are chronological Mid-Conversation updates (V2), including on + // checkpoint Turns — never folded into the frozen system baseline. + const modeReminder = isCompaction || lifecycle || nativePlanPromptOwnsMode + ? undefined + : takeActiveCursorModeReminder(sessionKey, { + advertisedTools: cursorTools.map((tool) => tool.name), + }); + const kickoffWarning = isCompaction || lifecycle + ? undefined + : takePlanExecutionKickoffWarning(sessionKey); + const oneShotReminders = [ + modeReminder, + kickoffWarning ? `${kickoffWarning}` : undefined, + ].filter((part) => !!part); + let systemPrompt; + if (isCompaction || lifecycle) { + // Ephemeral summary/title Runs — do not initialize a sticky Context Epoch. + systemPrompt = startedWithCheckpoint + ? undefined + : [baseSystemPrompt, interactionGuidance, ...oneShotReminders].filter(Boolean).join("\n\n"); + if (startedWithCheckpoint && oneShotReminders.length) { + userText = appendMidConversationMessage(userText, oneShotReminders.join("\n\n")); + } + } + else { + const admitted = admitContextEpoch({ + conversationId, + hasCheckpoint: startedWithCheckpoint, + hostSystem: baseSystemPrompt, + guidance: interactionGuidance, + hostAgent, + workspaceRoot, + oneShotReminders, + }); + systemPrompt = startedWithCheckpoint ? undefined : admitted.seedSystemPrompt; + userText = appendMidConversationMessage(userText, admitted.midConversationMessage); + // A recovered epoch has no baseline bytes. Keep the hash restored from + // the checkpoint snapshot so this turn's TurnEnded save does not drop it. + const previousIdentity = sessionKey ? promptIdentityBySession.get(sessionKey) : undefined; + frozenSystemPromptHash = admitted.epoch.baselineHash.trim() || previousIdentity?.systemPromptHash; + if (sessionKey) { + rememberPromptIdentity(sessionKey, { + hostAgent: hostAgent || previousIdentity?.hostAgent, + systemPromptHash: frozenSystemPromptHash, + }); + } + } + const history = extractPromptHistory(prompt, { + preserveTrailingUser: recovery?.kind === "rebase", + toolResults: isCompaction ? "all" : (recovery?.kind === "rebase" ? "trailing" : "omit"), + }); + await loadAvailableModels(); + // Resolve the region-specific Run stream origin once per process (memoized + // in agent-url.ts). Explicit agent host overrides skip GetServerConfig but + // still go through the Cursor agent-host allowlist. + const agentBaseUrl = resolveExplicitAgentBaseURL(options) ?? + (await resolveAgentUrl(token, { + apiBaseURL: resolveApiBaseURL(options), + telemetryEnabled: resolveTelemetryEnabled(options), + })); + // OpenCode merges model, agent, and selected-variant options before placing + // them under providerOptions.cursor. Read only the plugin's dedicated nested + // payload so unrelated options never become requested_model.parameters. + const picked = extractCursorVariantParameters(providerOptions); + const cursorModelId = resolveCursorWireModelId(providerOptions, modelId); + const reasoningEffort = typeof providerOptions?.reasoningEffort === "string" + ? providerOptions.reasoningEffort + : undefined; + const hintMaxMode = !!(providerOptions?.maxMode ?? false); + const modelInfo = _availableModels?.find((m) => m.id === cursorModelId); + const supportsImages = resolveCursorModelSupportsImages(cursorModelId, modelInfo?.supportsImages); + if (!resuming) { + assertCursorUserImageSupport(lastUser, supportsImages, cursorModelId); + } + const imageExtraction = resuming + ? { images: [], hashes: [], candidateCount: 0, duplicateCount: 0, userImageCount: 0 } + : await extractCursorPromptImages(prompt, lastUser, { + supportsImages, + // Content hashes are retained for the OpenCode session so growing + // history does not re-upload old screenshots. A recovery rebase opens + // a new Cursor conversation, so it must resend the same payload. + seenHistoryHashes: recovery?.kind === "rebase" + ? undefined + : sentHistoryImageHashes(sessionKey), + signal: callOptions.abortSignal, + }); + if (!supportsImages && imageExtraction.candidateCount > 0) { + trace(`image input: dropped ${imageExtraction.candidateCount} tool/assistant history image(s); ` + + `model=${cursorModelId} does not support images`); + } + if (imageExtraction.duplicateCount > 0) { + trace(`image input: skipped ${imageExtraction.duplicateCount} previously sent history image(s)`); + } + const images = imageExtraction.images; + const parameterValues = resolveVariantParameters(modelInfo, { + reasoningEffort, + maxMode: hintMaxMode, + picked, + }); + // An explicit pick owns the context tier; otherwise honor the maxMode hint. + // OpenCode's variant paramMap does not include maxMode when the user selects 1m. + const maxMode = resolveVariantMaxMode(parameterValues, { + picked, + maxMode: hintMaxMode, + }); + // Do NOT pass callOptions.abortSignal into the h2 Run stream. OpenCode aborts + // that signal when a turn ends with tool-calls; the Cursor stream must stay + // open until we write the exec results on the next doStream. + const stream = await bidiRunStream(token, { + baseURL: agentBaseUrl, + headers: options.headers, + }); + // Freeze RequestContext per conversation_id (CLI parity). Rebuilding every + // Run mutates volatile slices (git porcelain, layout) and breaks prompt cache. + const { context: requestContext, reused: requestContextReused } = await getOrBuildRequestContext(conversationId, { workspaceRoot, tools: cursorTools }); + const contextSubagents = Array.isArray(requestContext.custom_subagents) + ? requestContext.custom_subagents + .map((agent) => agent && typeof agent === "object" && typeof agent.name === "string" + ? { + name: agent.name, + description: typeof agent.description === "string" + ? agent.description + : undefined, + } + : undefined) + .filter((agent) => !!agent) + : []; + const subagentCatalog = { + ...discoveredSubagentCatalog, + agents: [...new Map([...discoveredSubagentCatalog.agents, ...contextSubagents] + .map((agent) => [agent.name, agent])).values()], + }; + // Resolve descriptors once from the merged OpenCode config so MCP identity is + // consistent across AgentRunRequest and both request_context reply paths. + const toolDescriptors = Array.isArray(requestContext.tools) + ? requestContext.tools + : []; + // CLI parity: echo the last conversation_checkpoint_update as conversation_state. + // After compaction or an unsafe checkpoint reset there is no checkpoint — + // seed a new Cursor conversation from OpenCode's authoritative history. + const reqBytes = buildRunRequest({ + text: userText, + images, + modelId: cursorModelId, + conversationId, + conversationGroupId, + systemPrompt: conversationState ? undefined : systemPrompt, + history: conversationState ? undefined : history, + conversationState, + parameterValues, + maxMode, + tools: cursorTools, + toolDescriptors, + requestContext, + action: resuming ? "resume" : "user", + }); + // Content hashes — Cursor content-addresses large payloads; logging these lets + // us match a server get_blob_args.blob_id to what it wants served. + const sha = (b) => createHash("sha256").update(b).digest("hex"); + const skillsCount = Array.isArray(requestContext.agent_skills) + ? requestContext.agent_skills.length + : 0; + const hooksCtx = typeof requestContext.hooks_additional_context === "string" + ? requestContext.hooks_additional_context + : ""; + const historyChars = history.reduce((n, m) => n + m.content.length, 0); + const seedChars = conversationState + ? 0 + : (systemPrompt?.length ?? 0) + userText.length + historyChars; + const seedEstimateIn = estimateTokens(seedChars); + const checkpointEnvelopeEstimateIn = conversationState + ? estimateTokens(userText.length + conversationState.length) + : 0; + const runRequestWireEstimateIn = estimateTokens(reqBytes.length); + const encodedRequestContext = encodeMessage("RequestContext", requestContext); + const priorTokenDetails = decodeConversationTokenDetails(conversationState); + const requestContextHash = sha(encodedRequestContext); + const systemPromptHash = systemPrompt ? sha(systemPrompt) : undefined; + const usageEstimate = { + // This is used only before Cursor supplies authoritative TurnEnded usage. + // Estimate the actual protobuf request, not history/system text omitted by + // checkpoint Runs. The opaque remote interpretation of KV blobs is logged + // separately and deliberately is not mislabeled as a token count. + inputTokens: runRequestWireEstimateIn, + outputTokens: 0, + cacheRead: 0, + cacheWrite: 0, + reasoningTokens: 0, + }; + trace(`outbound Run: model=${cursorModelId} opencodeModel=${modelId} ` + + `conversationId=${conversationId} conversationGroupId=${conversationGroupId} ` + + `params=${JSON.stringify(parameterValues ?? [])} ` + + `maxMode=${maxMode} systemPromptLen=${systemPrompt?.length ?? 0} ` + + `tools=${tools.length} incomingTools=${incomingTools.length} compaction=${isCompaction} ` + + `skills=${skillsCount} hooks=${hooksCtx ? hooksCtx.split("\n").length : 0} ` + + `availableModels=${_availableModels?.length ?? 0} userTextLen=${userText.length} ` + + `images=${images.length} imageBytes=${images.reduce((total, image) => total + image.data.length, 0)} ` + + `historyMsgs=${history.length} historyChars=${historyChars} ` + + `checkpointLen=${conversationState?.length ?? 0} checkpointBlobCount=${checkpointGraph.count} ` + + `checkpointBlobBytes=${checkpointGraph.bytes} checkpointGraphComplete=${checkpointGraph.complete} ` + + `seedChars=${seedChars} seedEstimateIn=${seedEstimateIn} ` + + `checkpointEnvelopeEstimateIn=${checkpointEnvelopeEstimateIn} ` + + `reset=${bound.reset} ` + + `resume=${resuming} requestContextReused=${requestContextReused} ` + + `usageEstimateMode=run-request-wire usageEstimateIn=${usageEstimate.inputTokens} ` + + `runRequestBytes=${reqBytes.length} requestContextBytes=${encodedRequestContext.length}`); + if (hooksCtx) + trace(`outbound Run hooks_additional_context: ${hooksCtx}`); + trace(`hash run_request sha256=${sha(reqBytes)}`); + if (systemPrompt) + trace(`hash systemPrompt sha256=${sha(systemPrompt)}`); + trace(`hash requestContext sha256=${requestContextHash}`); + if (conversationState) + trace(`hash checkpoint sha256=${sha(conversationState)}`); + try { + await writeWithBackpressure(stream, reqBytes, "initial Run request"); + rememberSentHistoryImageHashes(sessionKey, imageExtraction.hashes); + } + catch (error) { + stream.destroy(); + throw error; + } + const hostToolDialect = hostToolDialectFromTools(tools, options.defaultDialect); + trace(`host tool dialect: filePathKey=${hostToolDialect.filePathKey} shellTool=${hostToolDialect.shellTool} ` + + `tools=[${tools.map((t) => t.name).join(",")}]`); + const session = { + sessionId: crypto.randomUUID(), + conversationId, + cacheDir, + resumeCheckpoint: undefined, + tokenDetails: priorTokenDetails, + tokenDetailsFresh: false, + cacheDiagnostics: { + sessionKey, + conversationId, + conversationGroupId, + modelId: cursorModelId, + priorTokenDetails, + startedWithCheckpoint: !!conversationState, + requestContextReused, + requestContextHash, + systemPromptHash, + checkpointUpdates: 0, + tokenDetailUpdates: 0, + pumpPasses: 0, + stepStarts: 0, + stepCompletes: 0, + displayToolCalls: 0, + execRequests: 0, + createPlanInTurn: false, + switchModeInTurn: false, + }, + openCodeSessionId: lifecycle ? undefined : sessionKey, + hostAgent, + stableSystemPromptHash: frozenSystemPromptHash, + postCompactionRebase: isCompaction, + toolCatalog: snapshotToolCatalog(sessionKey), + stream, + frames: stream.frames()[Symbol.asyncIterator](), + pending: new Map(), + displayToolCalls: new Map(), + editToolCalls: new Map(), + // Seed from the per-OpenCode-session copy: merges in this turn (and after + // checkpoint resumes/rebases, which rebuild the session here) expand + // against the last observed host list, not an empty one. + mirroredTodos: snapshotMirroredTodosBySession(lifecycle ? undefined : sessionKey), + nextBridgedExecId: 900_000, + blobs: new Map(), + toolDescriptors, + toolAliases: webToolAliases.aliases, + hostToolDialect, + subagentCatalog, + requestContext, + allowTools, + permittedToolNames: new Set(allowTools ? incomingTools.map((tool) => tool.name).filter((name) => !!name) : []), + usageEstimate, + pumpActive: false, + pumpOwner: null, + heartbeat: null, + heartbeatCancel: null, + hardDeadlineTimer: null, + semanticDeadlineCancel: null, + terminalUnsubscribe: null, + deferredTerminalReason: null, + policy: continuationPolicy, + createdAt: Date.now(), + lastInboundAt: Date.now(), + lastHeartbeatWriteAt: Date.now(), + semanticDeadlineAt: Date.now() + continuationPolicy.semanticIdleMs, + closeError: null, + closed: false, + }; + sessionManager.registerSession(session); + attachSessionHeartbeat(session); + const abortIfNeeded = (stream) => { + if (!session.closed && !callOptions.abortSignal?.aborted) + return; + try { + stream?.destroy(); + } + catch { /* already closed */ } + if (callOptions.abortSignal?.aborted) { + throw new CursorLocalCancellationError("Cursor progress-only continuation cancelled"); + } + throw new CursorProtocolError("Cannot reopen a closed Cursor session"); + }; + session.reopenWithUserMessage = async (text) => { + abortIfNeeded(); + const { resolveBearerToken } = await import("./auth.js"); + const freshToken = await resolveBearerToken({ + accessToken: options.accessToken, + apiKey: options.apiKey, + baseUrl: resolveApiBaseURL(options), + }); + abortIfNeeded(); + // Do not pass OpenCode's abortSignal into the h2 stream: a tool-calls abort + // must not tear down a Run we still need to pump. Check abort around open. + const next = await bidiRunStream(freshToken, { baseURL: agentBaseUrl, headers: options.headers }); + abortIfNeeded(next); + const conversationState = session.resumeCheckpoint ?? getCheckpoint(session.conversationId); + const reqBytes = buildRunRequest({ + text, + modelId: cursorModelId, + conversationId: session.conversationId, + conversationGroupId: session.cacheDiagnostics?.conversationGroupId ?? session.conversationId, + conversationState, + parameterValues, + maxMode, + toolDescriptors: session.toolDescriptors, + requestContext: session.requestContext, + action: "user", + }); + try { + await writeWithBackpressure(next, reqBytes, "progress-only continuation Run"); + } + catch (error) { + try { + next.destroy(); + } + catch { } + throw error; + } + abortIfNeeded(next); + session.heartbeatCancel?.(); + await waitForStreamWrites(session.stream); + abortIfNeeded(next); + sessionManager.replaceStream(session, next); + attachSessionHeartbeat(session); + }; + callOptions.abortSignal?.addEventListener("abort", () => { + // Abort after tool-calls is normal — preserve pending sessions. + trace("abortSignal aborted → closeUnlessPending"); + sessionManager.closeUnlessPending(session); + }, { once: true }); + return session; +} +/** + * OpenCode re-sends the full tool-result history on every continuation. Prefer + * the newest result that still has a live pending exec on its tagged session. + */ +export function findContinuationSession(toolResults) { + for (let i = toolResults.length - 1; i >= 0; i--) { + const r = toolResults[i]; + const s = sessionManager.findByExecIds(r.sessionId, [r.execId]); + if (s) + return s; + } + return undefined; +} +/** How long a fresh-turn drain may wait for Cursor `turn_ended` after bridged settle. */ +export const FRESH_TURN_DRAIN_TIMEOUT_MS = 8_000; +/** + * Error text written onto Cursor execs that the host abandoned by starting a + * new user turn before returning the tool result. Completing the held Run with + * this cancel (then draining to `turn_ended`) keeps the same conversation + * prefix instead of `superseded-by-new-run` mid-exec. + */ +export const FRESH_TURN_PENDING_CANCEL_REASON = "Host started a new user turn before this tool result was delivered"; +/** + * Before opening a new user-turn Run, finish the prior held-open Run cleanly: + * settle display-only bridged pendings, cancel any real execs still waiting on + * the host, then drain for `turn_ended`. That preserves the same + * conversation_id / checkpoint prefix instead of aborting mid-tool. + */ +export async function preparePriorSessionForFreshTurn(openCodeSessionId, opts) { + const prior = sessionManager.findOpenByOpenCodeSessionId(openCodeSessionId); + if (!prior) + return "none"; + const settledBridged = sessionManager.settleBridgedPending(prior); + if (settledBridged > 0) { + trace(`fresh turn: settled ${settledBridged} bridged pending on prior session ${prior.sessionId} ` + + `remainingPending=${prior.pending.size}`); + } + if (sessionManager.isActivelyPumping(prior)) { + // A live pull owns the stream; do not race cancel writes against it. + // Wait briefly for the pump to finish, then cancel + drain. + const waited = await waitUntilNotPumping(prior, { + timeoutMs: opts?.timeoutMs ?? FRESH_TURN_DRAIN_TIMEOUT_MS, + }); + if (!waited) { + trace(`fresh turn: prior session ${prior.sessionId} is still pumping after wait — ` + + `leaving that Run held; registerSession will not close it`); + return "busy"; + } + } + if (prior.closed) + return "none"; + const cancelled = cancelPendingExecsForFreshTurn(prior); + if (cancelled > 0) { + trace(`fresh turn: cancelled ${cancelled} pending exec(s) on prior session ${prior.sessionId} ` + + `remainingPending=${prior.pending.size}`); + } + if (prior.closed) { + // Cancel path closed the session (encode/write failure). Caller opens a + // fresh Run on the same conversation_id — better than mid-exec supersede + // with stranded pending, but turn_ended may be missing. + return cancelled > 0 || settledBridged > 0 ? "settled-only" : "none"; + } + if (prior.pending.size > 0) { + trace(`fresh turn: prior session ${prior.sessionId} still has ${prior.pending.size} ` + + `pending after cancel — leaving that Run held; registerSession will not close it`); + return "busy"; + } + const outcome = await drainSessionUntilTurnEnded(prior, { + timeoutMs: opts?.timeoutMs ?? FRESH_TURN_DRAIN_TIMEOUT_MS, + }); + trace(`fresh turn: prior session ${prior.sessionId} drain outcome=${outcome}`); + if (outcome === "turn-ended") + return "drained"; + if (cancelled > 0 || settledBridged > 0) + return "settled-only"; + return outcome === "timeout" || outcome === "interrupted" ? "settled-only" : "busy"; +} +/** + * Write error/reject results for every still-open non-bridged pending on the + * held Run so Cursor can finish the agent turn instead of being superseded. + * + * @returns number of pendings successfully cancelled + */ +export function cancelPendingExecsForFreshTurn(session) { + if (session.closed || session.pending.size === 0) + return 0; + const synthetic = []; + for (const [execId, pending] of session.pending.entries()) { + if (pending.bridged) + continue; + if (pending.state !== "pending") + continue; + synthetic.push({ + toolCallId: `cursor_${session.sessionId}_${execId}`, + sessionId: session.sessionId, + execId, + toolName: pending.toolName ?? "unknown", + output: FRESH_TURN_PENDING_CANCEL_REASON, + error: FRESH_TURN_PENDING_CANCEL_REASON, + }); + } + if (synthetic.length === 0) + return 0; + const before = session.pending.size; + const delivered = deliverContinuationResults(session, synthetic); + if (!delivered || delivered.closed) { + // deliverContinuationResults closes on encode/write failure after some + // claims may already have succeeded. + return Math.max(0, before - session.pending.size); + } + return Math.max(0, before - session.pending.size); +} +async function waitUntilNotPumping(session, opts) { + if (!sessionManager.isActivelyPumping(session)) + return true; + const deadlineAt = Date.now() + Math.max(1, opts.timeoutMs); + while (Date.now() < deadlineAt) { + if (session.closed) + return false; + if (!sessionManager.isActivelyPumping(session)) + return true; + await new Promise((resolve) => { + const timer = setTimeout(resolve, 25); + timer.unref?.(); + }); + } + return !sessionManager.isActivelyPumping(session); +} +function nextFrameWithTimeout(frames, timeoutMs) { + return new Promise((resolve, reject) => { + let settled = false; + const finish = (value, error) => { + if (settled) + return; + settled = true; + clearTimeout(timer); + if (error !== undefined) + reject(error); + else + resolve(value); + }; + const timer = setTimeout(() => finish({ done: true, timedOut: true }), timeoutMs); + timer.unref?.(); + void frames.next().then(value => finish(value), error => finish(undefined, error)); + }); +} +/** + * Read remaining frames on an idle held-open Run until `turn_ended`, a + * response-requiring request we cannot answer without the host, or timeout. + * Used only on the fresh-turn settle path — not a general pump substitute. + */ +export async function drainSessionUntilTurnEnded(session, opts) { + if (session.closed) + return "skipped"; + if (session.pending.size > 0 || sessionManager.isActivelyPumping(session)) + return "busy"; + const timeoutMs = Math.max(1, opts?.timeoutMs ?? FRESH_TURN_DRAIN_TIMEOUT_MS); + const owner = Symbol(`fresh-turn-drain:${session.sessionId}`); + sessionManager.beginPump(session, owner); + const deadlineAt = Date.now() + timeoutMs; + let outcome = "timeout"; + try { + while (Date.now() < deadlineAt) { + if (session.closed) { + outcome = "interrupted"; + break; + } + const remainingMs = Math.max(1, deadlineAt - Date.now()); + let next; + try { + next = await nextFrameWithTimeout(session.frames, remainingMs); + } + catch (error) { + trace(`fresh turn drain: frame wait failed sessionId=${session.sessionId} ` + + `err=${error.message}`); + outcome = "interrupted"; + break; + } + if ("timedOut" in next && next.timedOut) { + outcome = "timeout"; + break; + } + if (next.done) { + outcome = "interrupted"; + break; + } + const frame = next.value; + if (frame.flags & 0x02) { + outcome = "interrupted"; + break; + } + let payload; + try { + payload = decodeFramePayload(frame); + } + catch { + continue; + } + let asm; + try { + asm = decodeMessage("AgentServerMessage", payload); + } + catch { + continue; + } + const iu = asm.interaction_update; + const kv = asm.kv_server_message; + const esm = asm.exec_server_message; + const interactionQuery = asm.interaction_query; + const checkpointRaw = asm.conversation_checkpoint_update; + const requiredChannel = responseRequiredChannel(payload); + if (checkpointRaw != null) { + const bytes = normalizeCheckpointBytes(checkpointRaw); + if (bytes && bytes.length > 0) { + if (session.cacheDiagnostics) + session.cacheDiagnostics.checkpointUpdates++; + setCheckpoint(session.conversationId, bytes); + session.resumeCheckpoint = Uint8Array.from(bytes); + const tokenDetails = decodeConversationTokenDetails(bytes); + if (tokenDetails) { + if (session.cacheDiagnostics) + session.cacheDiagnostics.tokenDetailUpdates++; + session.tokenDetails = tokenDetails; + session.tokenDetailsFresh = true; + } + } + } + if (iu?.turn_ended) { + trace(`fresh turn drain: turn_ended raw wire fields: ${debugWalkTurnEnded(payload)}`); + if (session.openCodeSessionId) { + await persistConversationState(session.cacheDir ?? opencodeGlobalCacheDir(), { + sessionKey: session.openCodeSessionId, + conversationId: session.conversationId, + requestContext: session.requestContext, + toolCatalog: session.toolCatalog ?? [], + postCompactionRebase: session.postCompactionRebase, + hostAgent: session.hostAgent, + systemPromptHash: session.stableSystemPromptHash, + }).catch((error) => { + trace(`fresh turn drain: TurnEnded save failed ` + + `sessionKey=${session.openCodeSessionId}: ${String(error)}`); + }); + } + // Record TurnEnded cache counters on diagnostics for the abandoned Run + // even though no OpenCode stream consumer will see this finish. + const turnEnded = iu.turn_ended; + const counters = cursorUsageCountersFromTurnEnded(turnEnded); + if (session.cacheDiagnostics) { + trace(formatCursorCacheDiagnostics(counters, session.tokenDetails, session.cacheDiagnostics.priorTokenDetails, { + ...session.cacheDiagnostics, + conversationGroupId: session.cacheDiagnostics.conversationGroupId + ?? resolveConversationGroupId(session.openCodeSessionId, session.conversationId), + modelId: session.cacheDiagnostics.modelId, + })); + } + else { + trace(`fresh turn drain: turn_ended input=${counters.inputTokens} ` + + `cacheRead=${counters.cacheRead} output=${counters.outputTokens}`); + } + sessionManager.close(session, "turn-ended"); + outcome = "turn-ended"; + break; + } + if (kv) { + const handled = handleKvServerMessage(kv, session); + if (!handled) { + outcome = "busy"; + break; + } + try { + await writeWithBackpressure(session.stream, handled.reply, `fresh-turn-drain KV ${handled.kind}_blob reply id=${handled.id}`); + } + catch (error) { + trace(`fresh turn drain: KV reply failed sessionId=${session.sessionId} ` + + `err=${error.message}`); + outcome = "interrupted"; + break; + } + sessionManager.recordSemanticProgress(session); + continue; + } + // Text/thinking/heartbeat/partial display updates can be ignored while draining. + if (iu?.text_delta || iu?.thinking_delta || iu?.heartbeat || iu?.partial_tool_call || iu?.step_started || iu?.step_completed) { + sessionManager.recordSemanticProgress(session); + continue; + } + if (iu?.tool_call_started || iu?.tool_call_completed) { + // A new display tool needs host mediation — stop draining. + outcome = "busy"; + break; + } + if (esm || interactionQuery || requiredChannel === "exec" || requiredChannel === "interaction") { + outcome = "busy"; + break; + } + } + } + finally { + sessionManager.endPump(session, owner); + } + return outcome; +} +/** + * Turn an OpenCode `question` tool result into the frame Cursor is waiting for. + * + * A synchronous AskQuestion still has its InteractionQuery open, so the answer + * goes back as the InteractionResponse for that query id. A `run_async` one was + * already released with `async{}`, so — exactly as Cursor CLI does — the answer + * travels as a ConversationAction keyed by the originating tool call id, with + * the server's own AskQuestionArgs bytes echoed back untouched. + */ +function buildAskQuestionContinuationFrame(pending, result) { + const metadata = pending.resultMetadata ?? {}; + const args = metadata.askQuestionArgs; + if (!args) + throw new CursorProtocolError("Bridged AskQuestion lost its decoded arguments"); + const answer = askQuestionResultFromToolOutput(args, result.output, result.error !== undefined); + if (!args.runAsync) { + const interactionId = metadata.interactionId; + if (typeof interactionId !== "number") { + throw new CursorProtocolError("Bridged AskQuestion lost its interaction id"); + } + return buildAskQuestionInteractionReply(interactionId, answer); + } + const rawArgs = metadata.askQuestionRawArgs; + if (!(rawArgs instanceof Uint8Array)) { + throw new CursorProtocolError("Bridged async AskQuestion lost its original arguments"); + } + const toolCallId = metadata.askQuestionToolCallId; + return buildAsyncAskQuestionCompletion(typeof toolCallId === "string" ? toolCallId : "", rawArgs, answer); +} +/** + * Turn the host's plan-approval outcome into the CreatePlan reply Cursor is + * still blocking on. One contract across every channel: `success` means the + * user approved execution, `error` means the plan was written but not accepted, + * so the model keeps planning instead of starting work. + * + * A host plan-stage tool reports that outcome as tool success/failure. The + * emulated path asks through `question`, so only an explicit "Yes" approves. + * On approval the session leaves plan mode, which arms the agent-mode + * `` for the next Run. + */ +function buildCreatePlanContinuationFrame(pending, result, sessionKey, cursorSessionID) { + const metadata = pending.resultMetadata ?? {}; + const interactionId = metadata.interactionId; + if (typeof interactionId !== "number") { + throw new CursorProtocolError("Bridged CreatePlan lost its interaction id"); + } + const planUri = metadata.planUri; + if (typeof planUri !== "string" || !planUri.trim()) { + throw new CursorProtocolError("Bridged CreatePlan lost its plan URI"); + } + const approved = metadata.createPlanBridgeKind === "approve" + ? createPlanApproved(result.output, result.error !== undefined, typeof metadata.createPlanQuestion === "string" ? metadata.createPlanQuestion : "") + : result.error === undefined; + if (!approved) { + const reason = result.error?.trim(); + return buildCreatePlanInteractionReply(interactionId, { + error: { error: reason || CREATE_PLAN_NOT_APPROVED_REASON }, + plan_uri: "", + }); + } + // The question-emulated path can report execution approval only when a host + // kickoff exists. Otherwise keep planning mode active and return an error. + if (metadata.createPlanBridgeKind === "approve" && !hasPlanExecutionKickoff()) { + return buildCreatePlanInteractionReply(interactionId, { + error: { error: "The plan was approved, but this host cannot start its execution turn." }, + plan_uri: planUri, + }); + } + // Approved: planning is over for this plan, so drop the plan-mode reminder + // and hand the next Run the agent-mode contract, exactly as an approved + // plan_exit does. + setActiveCursorMode(sessionKey, "agent"); + // Only the question-emulated path needs the provider's synthetic build turn. + // A host stage tool owns both approval and execution handoff itself. + if (metadata.createPlanBridgeKind === "approve" && sessionKey) { + const absolute = typeof metadata.planPath === "string" && metadata.planPath.trim() + ? metadata.planPath.trim() + : planPathFromUri(planUri); + const workspaceRoot = typeof metadata.workspaceRoot === "string" ? metadata.workspaceRoot : undefined; + if (!queuePlanExecutionKickoff({ + sessionID: sessionKey, + planPath: formatPlanKickoffPath(absolute, workspaceRoot), + cursorSessionID, + })) { + return buildCreatePlanInteractionReply(interactionId, { + error: { error: "The plan was approved, but this host cannot start its execution turn." }, + plan_uri: planUri, + }); + } + } + return buildCreatePlanInteractionReply(interactionId, { + success: {}, + plan_uri: planUri, + }); +} +function buildSwitchModeContinuationFrame(pending, result) { + const metadata = pending.resultMetadata ?? {}; + const interactionId = metadata.interactionId; + if (typeof interactionId !== "number") { + throw new CursorProtocolError("Bridged SwitchMode lost its interaction id"); + } + // A question-emulated exit carries the user's Yes/No as prose, not a plan + // tool's success/failure, so it needs the answer parser rather than the + // error-shaped mapping. + const answer = metadata.switchModeBridgeKind === "question" + ? switchModeResultFromQuestionOutput(result.output, result.error !== undefined) + : switchModeResultFromToolOutput(result.output, result.error !== undefined); + const target = "approved" in answer && typeof metadata.switchModeTarget === "string" + ? metadata.switchModeTarget.trim() + : ""; + return { + frame: buildSwitchModeInteractionReply(interactionId, answer), + ...(target ? { approvedTarget: target, bridgeKind: metadata.switchModeBridgeKind } : {}), + }; +} +/** + * Deliver trailing tool results onto a live continuation session. + * Returns the same session when writes succeed (or only bridged results were + * cleared). Returns undefined after closing the session when a write fails, so + * the caller can rebase onto a fresh Run instead of pumping a dead stream. + */ +export function deliverContinuationResults(session, trailingToolResults) { + const pendingResults = trailingToolResults.filter((r) => r.sessionId === session.sessionId && session.pending.has(r.execId)); + trace(`continuation: ${trailingToolResults.length} trailing tool result(s), ` + + `${pendingResults.length} pending for sessionId=${session.sessionId} ` + + `pending={${[...session.pending.keys()].join(",")}}`); + for (const r of pendingResults) { + const claim = sessionManager.claim(session.sessionId, r.execId); + if ("kind" in claim) { + if (claim.kind === "deliverable") { + throw new CursorProtocolError("Cursor continuation claim remained unclaimed"); + } + if (claim.kind === "duplicate") { + trace(`continuation: skipped duplicate execId=${r.execId} reason=${claim.reason}`); + continue; + } + trace(`continuation: unavailable execId=${r.execId} reason=${claim.reason}`); + return undefined; + } + const pending = claim.pending; + let frames = []; + let deliveredSwitchMode; + if (pending.resultField === ASK_QUESTION_RESULT_FIELD) { + // A bridged Cursor AskQuestion. The host tool result carries the user's + // choices; translate them back into the Cursor result the interaction is + // waiting for (or into the deferred completion, when Cursor was already + // released with `async`). + try { + frames = [buildAskQuestionContinuationFrame(pending, r)]; + } + catch (error) { + trace(`continuation: ask_question encode FAILED execId=${r.execId} err=${error.message}`); + sessionManager.close(session, "result-write-failed"); + return undefined; + } + } + else if (pending.resultField === SWITCH_MODE_RESULT_FIELD) { + // Bridged SwitchMode: host plan_enter / plan_exit outcome becomes + // approved{} or rejected{reason} on the still-open InteractionQuery. + // Approval also arms the CLI-shaped mode reminder for the next Run. + try { + const built = buildSwitchModeContinuationFrame(pending, r); + frames = [built.frame]; + if (built.approvedTarget) { + deliveredSwitchMode = { + target: built.approvedTarget, + bridgeKind: built.bridgeKind, + }; + } + } + catch (error) { + trace(`continuation: switch_mode encode FAILED execId=${r.execId} err=${error.message}`); + sessionManager.close(session, "result-write-failed"); + return undefined; + } + } + else if (pending.resultField === CREATE_PLAN_RESULT_FIELD) { + try { + frames = [buildCreatePlanContinuationFrame(pending, r, session.openCodeSessionId, session.sessionId)]; + } + catch (error) { + trace(`continuation: create_plan encode FAILED execId=${r.execId} err=${error.message}`); + sessionManager.close(session, "result-write-failed"); + return undefined; + } + } + else if (!pending.bridged) { + try { + const shellResult = pending.resultField === "shell_stream" + || pending.resultField === "shell_result" + || pending.resultField === "background_shell_spawn_result" + ? consumeCursorShellResult(r.toolCallId, r.output) + : undefined; + const workspaceRoot = workspaceRootFromRequestContext(session.requestContext); + const correlatedEditCallId = pending.resultMetadata?.correlatedEditCallId; + const requestedPath = pending.resultMetadata?.path; + const correlatedEdit = !r.error + && pending.resultField === "read_result" + && pending.toolName === "read" + && typeof correlatedEditCallId === "string" + && typeof requestedPath === "string" + ? session.editToolCalls?.get(correlatedEditCallId) + : undefined; + if (correlatedEdit) { + const absolutePath = path.resolve(workspaceRoot, requestedPath); + if (absolutePath === path.resolve(workspaceRoot, correlatedEdit.path)) { + frames = buildCompleteEditReadMessages(r.execId, absolutePath, requestedPath) ?? []; + if (frames.length > 0) { + correlatedEdit.completeRead = true; + trace(`continuation: upgraded authorized correlated edit read execId=${r.execId} ` + + `path=${JSON.stringify(requestedPath)}`); + } + } + } + // A refused image commit is Cursor's own `permission_denied` variant, + // not a generic write error: its agent branches on that case. The + // marker is set by this provider's own tool, so matching it is a + // contract rather than a guess at someone else's message text. + if (pending.toolName === CURSOR_IMAGE_SAVE_TOOL + && pending.resultField === "write_result" + && r.error?.includes(IMAGE_PERMISSION_DENIED_PREFIX)) { + const deniedPath = typeof pending.resultMetadata?.path === "string" + ? pending.resultMetadata.path + : ""; + frames = [encodeMessage("AgentClientMessage", { + exec_client_message: { + id: r.execId, + write_result: { + permission_denied: { + path: deniedPath, + directory: deniedPath ? path.dirname(deniedPath) : "", + operation: "write", + error: r.error.replace(`${IMAGE_PERMISSION_DENIED_PREFIX} `, ""), + is_readonly: false, + }, + }, + }, + })]; + } + // A committed image reports the file Cursor asked about, not the tool's + // prose. `buildExecClientMessages` would otherwise put the message's + // character count in `WriteSuccess.file_size`. + if (frames.length === 0 + && !r.error + && pending.toolName === CURSOR_IMAGE_SAVE_TOOL + && pending.resultField === "write_result") { + frames = [encodeMessage("AgentClientMessage", { + exec_client_message: { + id: r.execId, + write_result: { + success: { + path: typeof pending.resultMetadata?.path === "string" + ? pending.resultMetadata.path + : "", + lines_created: 0, + file_size: typeof pending.resultMetadata?.imageByteLength === "number" + ? pending.resultMetadata.imageByteLength + : 0, + }, + }, + }, + })]; + } + if (frames.length === 0) { + frames = buildExecClientMessages({ + execId: r.execId, + resultField: pending.resultField, + output: shellResult?.output ?? r.output, + error: r.error, + toolName: pending.toolName ?? r.toolName, + resultMetadata: pending.resultMetadata, + shellOutcome: shellResult?.outcome, + workspaceRoot, + }); + } + } + catch (error) { + trace(`continuation: result encode FAILED execId=${r.execId} err=${error.message}`); + sessionManager.close(session, "result-write-failed"); + return undefined; + } + } + const outcome = sessionManager.deliverClaim(claim, frames); + if (outcome.kind !== "delivered") { + trace(`continuation: delivery stopped execId=${r.execId} reason=${outcome.reason}`); + if (outcome.kind === "duplicate") + continue; + return undefined; + } + if (deliveredSwitchMode) { + const normalized = deliveredSwitchMode.target.toLowerCase(); + setActiveCursorMode(session.openCodeSessionId, deliveredSwitchMode.target, { + bridgedPlanEntered: normalized === "plan" || normalized === "spec", + }); + // A question-emulated exit has no native host plan tool to perform the + // actual primary-agent switch. Queue only after Cursor received the + // approved response; a failed write must not mutate host mode. + if (deliveredSwitchMode.bridgeKind === "question" && session.openCodeSessionId) { + queueHostAgentModeSwitch({ + sessionID: session.openCodeSessionId, + targetModeID: deliveredSwitchMode.target, + cursorSessionID: session.sessionId, + }); + } + } + // A host `todoread` result is the authoritative list. Refresh the mirrored + // snapshot (direct reads and bridged native reads alike) so later merges + // apply onto host truth, not a stale write. + if (pending.toolName === "todoread" && r.error === undefined) { + const snapshot = snapshotMirroredTodosFromReadOutput(r.output); + if (snapshot !== undefined) { + storeMirroredTodos(session, snapshot); + trace(`continuation: mirrored todoread snapshot items=${snapshot.length}`); + } + } + session.usageEstimate.inputTokens += estimateTokens(r.output.length); + if (pending.bridged) { + trace(`continuation: completed bridged result execId=${r.execId} toolName=${pending.toolName ?? r.toolName} outLen=${r.output.length}`); + continue; + } + const resultKind = pending.resultField === ASK_QUESTION_RESULT_FIELD + ? "ask_question answer" + : pending.resultField === SWITCH_MODE_RESULT_FIELD + ? "switch_mode answer" + : pending.resultField === CREATE_PLAN_RESULT_FIELD + ? "create_plan answer" + : "exec result"; + trace(`continuation: wrote ${resultKind} execId=${r.execId} field=${pending.resultField} ` + + `frames=${outcome.framesWritten} outLen=${r.output.length}`); + } + return session; +} +async function loadAvailableModels() { + const cacheDir = opencodeGlobalCacheDir(); + try { + const filePath = cacheFilePath(cacheDir); + let mtime = 0; + try { + const stat = await fs.promises.stat(filePath); + mtime = stat.mtimeMs; + } + catch { + // file missing — fall through with mtime=0 + } + // Re-read when the file changed (discoverModels background refresh). + if (mtime !== _availableModelsMtimeMs) { + const cached = await readCache(cacheDir); + _availableModels = cached?.models; + _availableModelsMtimeMs = mtime; + } + } + catch { /* ignore */ } +} +function resolveApiBaseURL(options) { + return options.apiBaseURL ?? process.env.CURSOR_API_BASE_URL ?? `https://${CURSOR_API_HOST}`; +} +function resolveTelemetryEnabled(options) { + return options.telemetryEnabled ?? isTruthyEnv(process.env.CURSOR_GET_SERVER_CONFIG_TELEMETRY); +} +function resolveExplicitAgentBaseURL(options) { + const raw = options.agentBaseURL ?? options.baseURL; + if (!raw) + return undefined; + const normalized = normalizeAgentRunOrigin(raw); + if (!normalized) { + throw new CursorProtocolError("Invalid Cursor agent base URL override: expected https://*.cursor.sh"); + } + return normalized; +} +function isTruthyEnv(value) { + return value === "1" || value === "true"; +} +const streamWriteChains = new WeakMap(); +async function waitForStreamWrites(stream) { + const pending = streamWriteChains.get(stream); + if (pending) + await pending.catch(() => undefined); +} +/** + * Keep awaited protocol writes ordered per Run stream. In particular, a large + * KV get_blob reply must drain before another KV reply or heartbeat is queued; + * otherwise one heartbeat observes the backlog created by dozens of ignored + * false write() results and reports the wrong operation as the failure. + */ +async function writeWithBackpressure(stream, message, operation) { + const previous = streamWriteChains.get(stream); + const current = (previous ? previous.catch(() => undefined) : Promise.resolve()) + .then(() => writeWithBackpressureNow(stream, message, operation)); + streamWriteChains.set(stream, current); + try { + await current; + } + finally { + if (streamWriteChains.get(stream) === current) + streamWriteChains.delete(stream); + } +} +async function writeWithBackpressureNow(stream, message, operation) { + let accepted; + try { + accepted = stream.write(message); + } + catch (cause) { + throw toCursorProviderError(cause, { + replaySafe: false, + fallback: `Cursor ${operation} write failed`, + }); + } + if (accepted !== false) + return; + trace(`stream write backpressured: operation=${operation} bytes=${message.length}`); + if (!stream.waitForDrain) { + throw new CursorTransportError(`Cursor ${operation} write was backpressured`, { + transient: false, + replaySafe: false, + code: "CURSOR_WRITE_BACKPRESSURE", + }); + } + try { + await stream.waitForDrain(5_000); + trace(`stream write drained: operation=${operation} bytes=${message.length}`); + } + catch (cause) { + throw toCursorProviderError(cause, { + replaySafe: false, + fallback: `Cursor ${operation} backpressure drain failed`, + }); + } +} +async function nextFrameWithSemanticDeadline(session) { + const remainingMs = session.semanticDeadlineAt - Date.now(); + if (remainingMs <= 0) { + throw new CursorTransportError(`Cursor semantic-progress timeout after ${session.policy.semanticIdleMs}ms`, { transient: true, replaySafe: true, code: "CURSOR_SEMANTIC_IDLE_TIMEOUT" }); + } + let timer; + let rejectCancelled; + const deadline = new Promise((_, reject) => { + rejectCancelled = reject; + timer = setTimeout(() => { + reject(new CursorTransportError(`Cursor semantic-progress timeout after ${session.policy.semanticIdleMs}ms`, { transient: true, replaySafe: true, code: "CURSOR_SEMANTIC_IDLE_TIMEOUT" })); + }, remainingMs); + timer.unref?.(); + }); + session.semanticDeadlineCancel = () => { + rejectCancelled?.(session.closeError ?? new CursorTransportError("Cursor semantic wait cancelled locally", { + transient: false, + replaySafe: false, + })); + }; + try { + return await Promise.race([session.frames.next(), deadline]); + } + finally { + if (timer) + clearTimeout(timer); + session.semanticDeadlineCancel = null; + } +} +/** + * Read the held-open stream, emitting stream parts, until the turn boundary: + * - a tool call (exec_server_message) → emit tool-call, finish "tool-calls", + * and KEEP the session open for the result on the next doStream call; + * - turn_ended → finish "stop" and close the session; + * - transport EOF before turn_ended → throw for one fresh-Run recovery. + */ +export async function pump(session, controller, ids, abortSignal) { + sessionManager.registerSession(session); + const cacheDiagnostics = session.cacheDiagnostics ??= { + sessionKey: session.openCodeSessionId, + conversationId: session.conversationId, + priorTokenDetails: session.tokenDetails, + startedWithCheckpoint: !!session.tokenDetails, + requestContextReused: false, + requestContextHash: "unavailable", + checkpointUpdates: 0, + tokenDetailUpdates: 0, + pumpPasses: 0, + stepStarts: 0, + stepCompletes: 0, + displayToolCalls: 0, + execRequests: 0, + createPlanInTurn: false, + switchModeInTurn: false, + }; + cacheDiagnostics.pumpPasses++; + const { textId, reasoningId } = ids; + const advertisedToolNames = advertisedToolNamesFromDescriptors(session.toolDescriptors); + const advertisedToolNameSet = new Set(advertisedToolNames.map((name) => resolveCustomWebToolAlias(name, session.toolAliases))); + let textStarted = false; + let reasoningStarted = false; + let assistantText = ""; + let progressContinuationAttempts = 0; + let emittedHostTools = 0; + const replaySafety = new AttemptReplaySafety(session.sessionId); + const failRunProtocol = (message, code) => { + replaySafety.markBarrier("unknown-or-malformed-frame"); + const error = new CursorProtocolError(message, { code }); + sessionManager.close(session, "remote-error", error); + throw error; + }; + const rethrowTransportWriteFailure = (error) => { + if (error instanceof CursorProviderError && error.origin !== "protocol") + throw error; + }; + const writeExecFrames = async (frames, operation) => { + try { + for (const frame of frames) { + await writeWithBackpressure(session.stream, frame, operation); + } + return true; + } + catch (error) { + rethrowTransportWriteFailure(error); + const wrapped = new Error(`Failed to ${operation}: ${error.message}`); + trace(`exec: reply FAILED ${wrapped.message}`); + safeError(wrapped); + sessionManager.close(session); + return false; + } + }; + // OpenCode cancels the ReadableStream between turns (see the cancel handler + // in doStreamImpl). The frames iterator can still yield a final `done` after + // the cancel lands — controller.enqueue on a cancelled controller throws. + // safeEnqueue swallows that throw and tracks the close so we stop pumping. + let streamClosed = false; + const safeEnqueue = (part) => { + if (streamClosed) + return false; + try { + controller.enqueue(part); + return true; + } + catch (e) { + streamClosed = true; + trace(`pump: enqueue on closed controller (suppressing) err=${e.message}`); + return false; + } + }; + const safeError = (err) => { + if (streamClosed) + return; + try { + controller.error(err); + } + catch (e) { + trace(`pump: controller.error failed (suppressing) err=${e.message}`); + } + streamClosed = true; + }; + /** Reply on Cursor's correlated exec channel without exposing a host tool call. */ + const rejectExec = async (parsed, reason, label) => { + const groundedReason = appendWorkspaceRootGrounding(reason, workspaceRootFromRequestContext(session.requestContext)); + const ok = await writeExecFrames(buildExecClientMessages({ + execId: parsed.id, + resultField: parsed.resultField, + output: "", + error: groundedReason, + toolName: parsed.toolName, + resultMetadata: parsed.resultMetadata, + workspaceRoot: workspaceRootFromRequestContext(session.requestContext), + }), `reject ${label} id=${parsed.id}`); + if (ok) + trace(`exec: REFUSED ${label} toolName=${parsed.toolName} id=${parsed.id}`); + return ok; + }; + /** + * Cursor's streamed edit handshake needs the complete target before it sends + * the replacement through write_args. OpenCode's ordinary read tool caps at + * 50 KB, so forwarding this private prerequisite read teaches Cursor's editor + * that a large file ends at the cap and produces a destructive replacement. + * + * Answer a correlated workspace-file read directly up to Cursor's own 50 MB + * edit limit. Missing targets remain empty-file successes so creation can + * continue. External and otherwise ineligible reads retain OpenCode's normal + * permission-aware path. + */ + const recoverCorrelatedEditRead = (parsed, displayCallId) => { + if (!displayCallId || + parsed.resultField !== "read_result" || + parsed.toolName !== "read" || + // `apply_patch` is the substitute this host offers when it withholds + // `write` — the follow-up write_args is remapped onto it either way. + !(advertisedToolNameSet.has("write") || advertisedToolNameSet.has("apply_patch"))) + return false; + const stored = session.displayToolCalls.get(displayCallId); + const display = parseDisplayToolCall(displayCallId, stored); + if (display?.variant !== "edit_tool_call" || display.bridgeable === false) + return false; + const requestedPath = opencodePathArg(parsed.args) ?? ""; + const editPath = typeof display.args.path === "string" ? display.args.path : ""; + if (!requestedPath || !editPath) + return false; + // Prefer env.workspace_paths — project_folder / workspace_project_dir are + // Cursor metadata roots under /projects/, not the git tree. + const workspaceRoot = workspaceRootFromRequestContext(session.requestContext); + const resolvePath = (value) => path.resolve(workspaceRoot, value); + const absolutePath = resolvePath(requestedPath); + if (absolutePath !== resolvePath(editPath)) + return false; + let exists = true; + try { + fs.lstatSync(absolutePath); + } + catch (error) { + const code = error && typeof error === "object" && "code" in error + ? error.code + : undefined; + if (code !== "ENOENT") + return false; + exists = false; + } + if (exists) { + // Do not bypass OpenCode's external-directory permission boundary, even + // through a symlink rooted in the workspace. + try { + const realRoot = fs.realpathSync(workspaceRoot); + const realTarget = fs.realpathSync(absolutePath); + const relative = path.relative(realRoot, realTarget); + if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { + parsed.resultMetadata = { + ...parsed.resultMetadata, + correlatedEditCallId: displayCallId, + }; + return false; + } + const frames = buildCompleteEditReadMessages(parsed.id, absolutePath, requestedPath); + if (!frames) + return false; + for (const frame of frames) + session.stream.write(frame); + const editCall = session.editToolCalls?.get(displayCallId); + if (editCall) + editCall.completeRead = true; + trace(`exec: correlated edit read completed directly id=${parsed.id} ` + + `path=${JSON.stringify(requestedPath)}; awaiting write_args`); + return true; + } + catch { + parsed.resultMetadata = { + ...parsed.resultMetadata, + correlatedEditCallId: displayCallId, + }; + return false; + } + } + try { + for (const frame of buildExecClientMessages({ + execId: parsed.id, + resultField: parsed.resultField, + output: "", + toolName: parsed.toolName, + resultMetadata: { path: requestedPath }, + workspaceRoot, + })) { + session.stream.write(frame); + } + trace(`exec: missing edit target treated as empty file id=${parsed.id} ` + + `path=${JSON.stringify(requestedPath)}; awaiting write_args`); + return true; + } + catch (e) { + const error = new Error(`Failed to recover Cursor edit read: ${e.message}`); + trace(`exec: edit read recovery FAILED ${error.message}`); + safeError(error); + sessionManager.close(session); + return true; + } + }; + /** + * A read of a path that does not exist would otherwise reach OpenCode as a + * real tool call and raise a permission prompt for a file the user never had. + * Cursor's own LocalReadExecutor instead resolves the target against the + * workspace, stats it, and returns a typed ReadResult (file_not_found / + * invalid_file) before execution. Mirror that here: reply on the held-open + * exec channel with the exact typed case so the model receives a structured + * observation and the host surfaces no prompt. + * + * Read-only by construction — write/edit targets are never checked, so + * legitimate file creation (path resolves, file absent) is never denied. Runs + * after recoverCorrelatedEditRead, which has already converted the edit + * handshake read into an empty-file success. EACCES/EPERM and other stat + * errors fall through to OpenCode so a genuine permission decision stands. + */ + const rejectMissingReadTarget = (parsed) => { + if (parsed.toolName !== "read") + return false; + const requested = opencodePathArg(parsed.args) ?? ""; + if (!requested) + return false; + // A scheme-addressed target is the advertised host executor's to resolve, + // not a local file to stat. Rejecting it here would make any URI-backed + // capability permanently unreachable through this provider. + if (isUriReadTarget(requested)) { + trace(`exec: forwarding URI read target id=${parsed.id} target=${JSON.stringify(requested)}`); + return false; + } + const workspaceRoot = workspaceRootFromRequestContext(session.requestContext); + const absolutePath = resolveReadTargetPath(requested, workspaceRoot); + const readResult = classifyMissingReadTarget(absolutePath); + if (!readResult) + return false; + try { + for (const frame of buildReadRejectionMessages(parsed.id, readResult)) { + session.stream.write(frame); + } + const kind = Object.keys(readResult)[0]; + trace(`exec: rejected missing read target id=${parsed.id} kind=${kind} ` + + `path=${JSON.stringify(absolutePath)}`); + return true; + } + catch (e) { + const error = new Error(`Failed to reject Cursor read of a missing path: ${e.message}`); + trace(`exec: missing read rejection FAILED ${error.message}`); + safeError(error); + sessionManager.close(session); + return true; + } + }; + /** AI SDK V3 requires text-end / reasoning-end before finish or tool-call. */ + const closeOpenSpans = () => { + for (const part of spanEndParts({ textStarted, reasoningStarted, textId, reasoningId })) { + safeEnqueue(part); + } + reasoningStarted = false; + textStarted = false; + }; + const emitText = (text) => { + if (!text) + return; + assistantText += text; + replaySafety.markBarrier("visible-text"); + // Close reasoning before text (hosts expect reasoning-end before text-start). + if (reasoningStarted && !textStarted) { + safeEnqueue({ type: "reasoning-end", id: reasoningId }); + reasoningStarted = false; + } + if (!textStarted) { + safeEnqueue({ type: "text-start", id: textId }); + textStarted = true; + } + session.usageEstimate.outputTokens += estimateTokens(text.length); + safeEnqueue({ type: "text-delta", id: textId, delta: text }); + }; + const emitReasoning = (text) => { + if (!text) + return; + replaySafety.markBarrier("visible-reasoning"); + if (!reasoningStarted) { + safeEnqueue({ type: "reasoning-start", id: reasoningId }); + reasoningStarted = true; + } + session.usageEstimate.outputTokens += estimateTokens(text.length); + safeEnqueue({ type: "reasoning-delta", id: reasoningId, delta: text }); + }; + const emitFinish = (te, reason, settledUsage, settledSource) => { + closeOpenSpans(); + const est = session.usageEstimate; + // OpenCode TUI/GUI replace each assistant message's tokens (they do not + // sum occupancy) and the TUI footer requires tokens.output > 0. Cost is + // added per step-finish. Emit checkpoint occupancy snapshots at tool-call + // boundaries with a $0 Copilot cost override, then one more occupancy + // snapshot at TurnEnded/stop. Held-Run TurnEnded counters are cumulative + // across every tool step, but this finish only spans the last generation + // slice — putting output_tokens/reasoning there makes host tok/s + // (generated/stepElapsed) absurd. Keep exact request counters under + // providerMetadata.cursor.*Raw. Char/4 usageEstimate stays traces-only. + const tokenDetails = session.tokenDetails; + const occupancyDetails = tokenDetails && tokenDetails.usedTokens > 0 ? tokenDetails : undefined; + const contextSource = tokenDetails + ? session.tokenDetailsFresh + ? "checkpoint-current-run" + : "checkpoint-previous-turn" + : undefined; + const usage = settledUsage ?? (occupancyDetails + ? occupancyUsageFromTokenDetails(occupancyDetails, session.cacheDiagnostics?.priorTokenDetails) + : emptyLanguageModelV3Usage()); + const counters = te ? cursorUsageCountersFromTurnEnded(te) : undefined; + // TurnEnded stays a real (non-occupancyOnly) finish so hosts that collapse + // tool-boundary occupancy still keep one context snapshot for the sidebar. + // Copilot $0 avoids billing the occupancy-shaped counters as a new prompt. + const providerMetadata = te + ? { + ...OPENCODE_DISPLAY_ONLY_COST_METADATA, + ...cursorTurnEndedProviderMetadata(te, tokenDetails, contextSource), + } + : { + ...OPENCODE_DISPLAY_ONLY_COST_METADATA, + cursor: { + usageVersion: 3, + occupancyOnly: true, + ...(occupancyDetails && contextSource + ? { context: cursorContextUsageMetadata(occupancyDetails, contextSource) } + : {}), + }, + }; + const reasonLabel = typeof reason === "object" && reason && "unified" in reason + ? String(reason.unified ?? "unknown") + : String(reason); + const inTotal = usage.inputTokens?.total ?? 0; + const outTotal = usage.outputTokens?.total ?? 0; + const occupancySource = occupancyDetails + ? `occupancy-${contextSource ?? "unavailable"}` + : "intermediate-zero"; + // Occupancy finishes never see Cursor TurnEnded cache_read. usageEstimate.cacheRead + // stays 0 for the whole Run, so logging it as rawCacheRead falsely reports 0% on + // every tool-call step. Prefer the V3 occupancy partition (prior prefix → cacheRead) + // and label the estimate separately from billed TurnEnded counters. + const occupancyPrefixCache = occupancyDetails + ? (session.cacheDiagnostics?.priorTokenDetails?.usedTokens ?? 0) + : undefined; + const rawIn = te + ? turnEndedCounter(te, "input_tokens") + : occupancyDetails + ? occupancyDetails.usedTokens + : est.inputTokens; + const rawOut = te + ? turnEndedCounter(te, "output_tokens") + : occupancyDetails + ? 1 + : est.outputTokens; + const rawCacheRead = te + ? turnEndedCounter(te, "cache_read") + : occupancyPrefixCache ?? est.cacheRead; + const rawCacheWrite = te ? turnEndedCounter(te, "cache_write") : est.cacheWrite; + trace(`finish: reason=${reasonLabel} ` + + `v3In=${inTotal} v3Out=${outTotal} ` + + `v3CacheRead=${usage.inputTokens?.cacheRead ?? 0} v3CacheWrite=${usage.inputTokens?.cacheWrite ?? 0} ` + + `v3Reasoning=${usage.outputTokens?.reasoning ?? 0} ` + + `rawIn=${rawIn} rawOut=${rawOut} rawCacheRead=${rawCacheRead} rawCacheWrite=${rawCacheWrite} ` + + `${occupancyPrefixCache !== undefined ? `occupancyPrefixCache=${occupancyPrefixCache} ` : ""}` + + `source=${te + ? settledSource ?? (contextSource ?? "unavailable") + : occupancySource}`); + // Validate the usage we actually send. Occupancy finishes (tool-call and + // TurnEnded/stop) use prior-prefix cacheRead — never compare that against + // aggregate TurnEnded request cache ratios (false mismatch). Raw request + // counters stay on `finish:` and cache diagnosis only. + if (occupancyDetails) { + trace(formatTurnUsageValidation(occupancyValidationCounters(occupancyDetails, session.cacheDiagnostics?.priorTokenDetails), usage, occupancyDetails, contextSource)); + } + else if (counters) { + trace(formatTurnUsageValidation(counters, usage, tokenDetails, contextSource)); + } + if (counters) { + trace(formatCursorCacheDiagnostics(counters, tokenDetails, cacheDiagnostics.priorTokenDetails, cacheDiagnostics)); + } + safeEnqueue({ + type: "finish", + usage, + finishReason: reason, + ...(providerMetadata ? { providerMetadata } : {}), + }); + }; + while (true) { + // Consumer cancelled / closed the ReadableStream. Stop reading Cursor + // frames so a continuation doStream can resume the same iterator — + // keeping the loop alive would discard frames the next pump needs. + if (streamClosed) { + trace(`pump: stream closed (consumer cancelled) pending=${session.pending.size}`); + sessionManager.closeUnlessPending(session); + return; + } + if (abortSignal?.aborted) { + // Stop feeding this ReadableStream, but keep the Run session if we still + // owe Cursor an exec result (OpenCode aborts between tool-call turns). + trace(`pump: abortSignal aborted pending=${session.pending.size}`); + sessionManager.closeUnlessPending(session); + return; + } + let next; + try { + next = session.pending.size === 0 + ? await nextFrameWithSemanticDeadline(session) + : await session.frames.next(); + } + catch (error) { + closeOpenSpans(); + const failure = error instanceof CursorProviderError + ? error + : new CursorRunInterruptedError(`Cursor Run frame stream interrupted: ${error.message}`, { cause: error }); + throw replaySafety.applyTo(failure); + } + if (next.done) { + closeOpenSpans(); + trace("pump: frames iterator ended before turn_ended"); + const failure = new CursorRunInterruptedError(); + throw replaySafety.applyTo(failure); + } + const frame = next.value; + if (frame.flags & 0x02) { + // A successful agent turn has an explicit turn_ended update before the + // Connect envelope closes. Reaching end-stream here means the Run was + // interrupted, even if the HTTP status itself was 200. + let payload = ""; + if (frame.payload.length > 0) { + try { + payload = new TextDecoder().decode(decodeFramePayload(frame)); + } + catch { + replaySafety.markBarrier("unknown-or-malformed-frame"); + } + } + closeOpenSpans(); + const failure = payload + ? connectFrameError(payload) + : new CursorRunInterruptedError(); + throw replaySafety.applyTo(failure); + } + // decodeFramePayload can throw on a corrupt gzip payload (gunzipSync). + // Skip the frame rather than abort the whole turn. + let payload; + try { + payload = decodeFramePayload(frame); + } + catch (e) { + replaySafety.markBarrier("unknown-or-malformed-frame"); + trace(`gunzip FAILED (skipping frame): flags=0x${frame.flags.toString(16)} len=${frame.payload.length} err=${e.message}`); + continue; + } + let asm; + try { + asm = decodeMessage("AgentServerMessage", payload); + } + catch { + // A single malformed/truncated frame must not abort the whole turn + // (protobufjs throws "index out of range: …" on length overruns). Log it + // and keep pumping. + replaySafety.markBarrier("unknown-or-malformed-frame"); + const channel = responseRequiredChannel(payload); + if (channel) { + failRunProtocol(`Cursor ${channel} request could not be decoded`, RUN_REQUEST_DECODE_FAILED); + } + trace(`decode FAILED (skipping non-request frame): flags=0x${frame.flags.toString(16)} len=${payload.length}`); + continue; + } + const iu = asm.interaction_update; + const esm = asm.exec_server_message; + const kv = asm.kv_server_message; + const execControl = asm.exec_server_control_message; + const interactionQuery = asm.interaction_query; + const checkpointRaw = asm.conversation_checkpoint_update; + const topField = payload.length > 0 ? payload[0] >> 3 : 0; + const checkpointProgress = normalizeCheckpointBytes(checkpointRaw); + const requiredChannel = responseRequiredChannel(payload); + if (requiredChannel === "multiple" || + (requiredChannel === "exec" && !esm) || + (requiredChannel === "kv" && !kv) || + (requiredChannel === "interaction" && !interactionQuery)) { + failRunProtocol("Cursor response-requiring request could not be decoded", RUN_REQUEST_DECODE_FAILED); + } + const replayFrame = analyzeReplayFrame(payload, { + interactionUpdate: iu, + exec: esm, + kv, + execControl, + interactionQuery, + checkpointBytes: checkpointProgress, + }); + if (replayFrame.semanticProgress) { + sessionManager.recordSemanticProgress(session); + } + if (replayFrame.barrier) + replaySafety.markBarrier(replayFrame.barrier); + { + const iuKind = iu ? Object.keys(iu).find((k) => iu[k]) : undefined; + trace(`pump frame: topField=${topField} interaction_update=${iuKind ?? "-"} ` + + `exec=${esm ? "yes" : "no"} kv=${kv ? "yes" : "no"} ` + + `interaction_query=${interactionQuery ? "yes" : "no"} ` + + `checkpoint=${checkpointRaw ? "yes" : "no"}`); + } + try { + // CLI: conversationCheckpointUpdate → replace agentStore conversation state. + // Store opaque bytes keyed by conversation_id; next Run echoes them. + if (checkpointRaw != null) { + const bytes = normalizeCheckpointBytes(checkpointRaw); + if (bytes && bytes.length > 0) { + cacheDiagnostics.checkpointUpdates++; + setCheckpoint(session.conversationId, bytes); + session.resumeCheckpoint = Uint8Array.from(bytes); + const tokenDetails = decodeConversationTokenDetails(bytes); + if (tokenDetails) { + cacheDiagnostics.tokenDetailUpdates++; + session.tokenDetails = tokenDetails; + session.tokenDetailsFresh = true; + } + const context = tokenDetails + ? ` context=${tokenDetails.usedTokens}/${tokenDetails.maxTokens} ` + + `categories=${formatCursorTokenCategories(tokenDetails)}` + : " context=unavailable"; + trace(`checkpoint: stored ${bytes.length}B for conversationId=${session.conversationId}${context}`); + } + } + if (iu?.text_delta) { + emitText(iu.text_delta.text ?? ""); + } + else if (iu?.thinking_delta) { + emitReasoning(iu.thinking_delta.text ?? ""); + } + else if (iu?.turn_ended) { + trace(`turn_ended raw wire fields: ${debugWalkTurnEnded(payload)}`); + const turnEnded = iu.turn_ended; + if (session.openCodeSessionId) { + await persistConversationState(session.cacheDir ?? opencodeGlobalCacheDir(), { + sessionKey: session.openCodeSessionId, + conversationId: session.conversationId, + requestContext: session.requestContext, + toolCatalog: session.toolCatalog ?? [], + postCompactionRebase: session.postCompactionRebase, + hostAgent: session.hostAgent, + systemPromptHash: session.stableSystemPromptHash, + }).catch((error) => { + trace(`conversation persistence: TurnEnded save failed ` + + `sessionKey=${session.openCodeSessionId}: ${String(error)}`); + }); + } + const checkpoint = session.resumeCheckpoint ?? getCheckpoint(session.conversationId); + if (typeof session.reopenWithUserMessage === "function" + && checkpoint + && checkpoint.length > 0 + && shouldContinueProgressOnlyTurn({ + allowTools: session.allowTools, + advertisedToolCount: advertisedToolNames.length, + assistantText, + emittedHostTools, + continuationAttempts: progressContinuationAttempts, + pendingExecs: session.pending.size, + })) { + progressContinuationAttempts += 1; + trace("progress-only: continuing attempt=1"); + try { + await session.reopenWithUserMessage(progressOnlyContinuationPrompt(workspaceRootFromRequestContext(session.requestContext))); + assistantText = ""; + continue; + } + catch (error) { + // Cursor already completed this turn. A failed nudge must not discard + // the valid turn_ended the user already received as assistant text. + trace(`progress-only: continuation failed, finishing original turn: ${error.message}`); + } + } + emitFinish(turnEnded, { unified: "stop", raw: undefined }); + sessionManager.close(session); + return; + } + else if (iu?.tool_call_started) { + cacheDiagnostics.displayToolCalls++; + // Stash Cursor display ToolCall until exec claims it, or completed bridges it. + const started = iu.tool_call_started; + const callId = typeof started.call_id === "string" ? started.call_id : ""; + const toolCall = started.tool_call; + if (callId && toolCall) { + session.displayToolCalls.set(callId, toolCall); + const variant = Object.keys(toolCall).find((k) => k.endsWith("_tool_call")) ?? "?"; + const display = parseDisplayToolCall(callId, toolCall); + const editPath = display?.variant === "edit_tool_call" + && typeof display.args.path === "string" + ? display.args.path + : undefined; + if (editPath) { + const editToolCalls = session.editToolCalls ?? (session.editToolCalls = new Map()); + editToolCalls.set(callId, { path: editPath }); + } + const callIdLog = callId.replace(/\r?\n/g, "\\n"); + let wireFields = ""; + if (variant === "?") { + const toolBytes = extractProtobufSubmessage(payload, [1, 2, 2]); + if (toolBytes) { + wireFields = ` wireFields=[${listProtobufFieldNumbers(toolBytes).join(",")}]`; + } + } + trace(`display tool_call_started: callId=${callIdLog} variant=${variant}${wireFields}`); + } + } + else if (iu?.tool_call_completed) { + const completed = iu.tool_call_completed; + const callId = typeof completed.call_id === "string" ? completed.call_id : ""; + if (callId) + session.editToolCalls?.delete(callId); + // If exec already claimed this call_id, display map entry is gone — skip. + if (!callId || !session.displayToolCalls.has(callId)) { + if (callId) { + trace(`display tool_call_completed: ignore (exec-handled or unknown) callId=${callId}`); + } + } + else { + const stored = session.displayToolCalls.get(callId); + session.displayToolCalls.delete(callId); + const toolCall = completed.tool_call ?? stored; + if (!session.allowTools) { + trace(`display tool_call_completed: SKIPPED (allowTools=false) callId=${callId}`); + } + else { + const display = parseDisplayToolCall(callId, toolCall, session.mirroredTodos); + const advertised = advertisedToolNamesFromDescriptors(session.toolDescriptors); + const bridged = display + ? resolveBridgedOpenCodeToolCall(display, advertised, session.hostToolDialect) + : undefined; + if (!display) { + const callIdLog = callId.replace(/\r?\n/g, "\\n"); + // AgentServerMessage.interaction_update(1).tool_call_completed(3).tool_call(2) + const toolBytes = extractProtobufSubmessage(payload, [1, 3, 2]); + const wire = toolBytes + ? ` wireFields=[${listProtobufFieldNumbers(toolBytes).join(",")}]` + : ""; + trace(`display tool_call_completed: unparsed callId=${callIdLog} ` + + `keys=[${Object.keys(toolCall).join(",")}]${wire}`); + } + else if (!bridged) { + // GetMcpTools / GetDynamicTools is executed by Cursor after mcp_state; + // large catalogs then spill through write_args. Not a missing host tool. + if (isNativeDisplayToolCall(display.variant)) { + trace(`display tool_call_completed: native ${display.preferredToolName} callId=${callId} ` + + `(server-side catalog; spill uses write_args)`); + } + else { + trace(`display tool_call_completed: no advertised OpenCode tool ` + + `callId=${callId} variant=${display.variant} preferred=${display.preferredToolName} ` + + `advertised=[${advertised.join(",")}]`); + } + } + else { + // Snapshot only Cursor todo writes (not create_plan's synthetic + // "plan" prepend) so later merge patches apply onto a real list. + if (bridged.toolName === "todowrite" && bridged.variant === "update_todos_tool_call") { + const snapshot = snapshotMirroredTodos(bridged.args.todos); + if (snapshot !== undefined) + storeMirroredTodos(session, snapshot); + } + const execId = session.nextBridgedExecId++; + sessionManager.registerPending(execId, session, "bridged", bridged.toolName, true); + const toolCallId = `cursor_${session.sessionId}_${execId}`; + const input = JSON.stringify(bridged.args ?? {}); + trace(`display BRIDGED tool-call toolCallId=${toolCallId} toolName=${bridged.toolName} ` + + `variant=${bridged.variant} callId=${callId} inputLen=${input.length}`); + emittedHostTools++; + closeOpenSpans(); + safeEnqueue({ + type: "tool-call", + toolCallId, + toolName: bridged.toolName, + input, + }); + emitFinish(undefined, { unified: "tool-calls", raw: undefined }); + return; + } + } + } + } + else if (iu?.step_started) { + cacheDiagnostics.stepStarts++; + } + else if (iu?.step_completed) { + cacheDiagnostics.stepCompletes++; + } + else if (esm) { + cacheDiagnostics.execRequests++; + const esmId = esm.id ?? 0; + if (esm.request_context_args) { + // Server turn-setup probe (#10). Reply with full OpenCode-sourced context. + { + const rc = session.requestContext; + const skills = Array.isArray(rc.agent_skills) ? rc.agent_skills.length : 0; + const hooks = typeof rc.hooks_additional_context === "string" ? rc.hooks_additional_context : ""; + trace(`exec request_context: id=${esmId} — replying context ` + + `tools=${session.toolDescriptors.length} skills=${skills} ` + + `hooks=${hooks ? hooks.split("\n").length : 0}`); + if (hooks) + trace(`exec request_context hooks_additional_context: ${hooks}`); + } + try { + traceRequestContextPaths(`exec request_context reply id=${esmId}`, session.requestContext); + await writeWithBackpressure(session.stream, buildRequestContextResult(esmId, session.requestContext), `request-context reply id=${esmId}`); + trace(`exec request_context: replied`); + } + catch (error) { + rethrowTransportWriteFailure(error); + failRunProtocol("Cursor request-context reply failed", RUN_REPLY_FAILED); + } + } + else if (esm.mcp_state_exec_args) { + // MCP-backed writes/reads can be preceded by this control-plane probe. + // Confirm the virtual servers from the already-advertised context, then + // keep pumping until Cursor emits the actual mcp_args tool request. + const stateArgs = esm.mcp_state_exec_args; + const requested = Array.isArray(stateArgs.server_identifiers) + ? stateArgs.server_identifiers.join(",") + : ""; + try { + await writeWithBackpressure(session.stream, buildMcpStateResult(esmId, stateArgs, session.requestContext), `MCP-state reply id=${esmId}`); + trace(`exec mcp_state: replied id=${esmId} requested=[${requested}]`); + } + catch (error) { + rethrowTransportWriteFailure(error); + failRunProtocol("Cursor MCP-state reply failed", RUN_REPLY_FAILED); + } + } + else if (esm.list_mcp_resources_exec_args) { + // Native Cursor exec (agent.v1 field 17), no OpenCode route under + // Option B — see tasks/plans/fix-cursor-mcp-resource-exec.md. Answer + // with Cursor's own no-client-qualifies shape and keep pumping. + const args = esm.list_mcp_resources_exec_args; + const server = typeof args.server === "string" ? args.server : ""; + try { + await writeWithBackpressure(session.stream, buildListMcpResourcesFallback(esmId), `list-MCP-resources reply id=${esmId}`); + trace(`exec list_mcp_resources: replied id=${esmId} server=${server || "(all)"} success{resources:[]}`); + } + catch (error) { + rethrowTransportWriteFailure(error); + failRunProtocol("Cursor list_mcp_resources reply failed", RUN_REPLY_FAILED); + } + } + else if (esm.read_mcp_resource_exec_args) { + // Native Cursor exec (agent.v1 field 18) — same rationale as above. + const args = esm.read_mcp_resource_exec_args; + const server = typeof args.server === "string" ? args.server : ""; + const uri = typeof args.uri === "string" ? args.uri : ""; + try { + await writeWithBackpressure(session.stream, buildReadMcpResourceFallback(esmId, server, uri), `read-MCP-resource reply id=${esmId}`); + trace(`exec read_mcp_resource: replied id=${esmId} server=${server} uri=${uri} error{server not found}`); + } + catch (error) { + rethrowTransportWriteFailure(error); + failRunProtocol("Cursor read_mcp_resource reply failed", RUN_REPLY_FAILED); + } + } + else if (esm.git_diff_request) { + const request = (esm.git_diff_request ?? {}); + try { + const frames = await buildGitDiffExecMessages({ + execId: esmId, + request, + workspaceRoot: workspaceRootFromRequestContext(session.requestContext), + }); + for (const frame of frames) { + await writeWithBackpressure(session.stream, frame, `git_diff reply id=${esmId}`); + } + trace(`exec git_diff: replied id=${esmId}`); + } + catch (error) { + rethrowTransportWriteFailure(error); + failRunProtocol("Cursor git_diff reply failed", RUN_REPLY_FAILED); + } + } + else { + replaySafety.markBarrier("non-control-exec"); + const displayCallId = extractExecDisplayCallId(esm); + const parsed = parseExecServerMessage(esm, session.hostToolDialect); + if (parsed) { + const executableToolName = resolveCustomWebToolAlias(parsed.toolName, session.toolAliases); + if (executableToolName !== parsed.toolName) { + trace(`web tool alias resolved: ${parsed.toolName} -> ${executableToolName}`); + parsed.toolName = executableToolName; + } + remapNativeSubagentForCatalog(parsed, advertisedToolNameSet, session.subagentCatalog); + // SubagentArgs has no description; Cursor's real title lives on the + // correlated display TaskToolCall. Prefer that over the 5-word prompt slice. + if (displayCallId && (parsed.toolName === "task" || parsed.toolName === "subagent")) { + const stored = session.displayToolCalls.get(displayCallId); + const display = parseDisplayToolCall(displayCallId, stored); + if (display?.variant === "task_tool_call") { + const description = typeof display.args.description === "string" ? display.args.description : undefined; + preferCorrelatedTaskDescription(parsed, description); + if (description?.trim()) { + trace(`exec: preferred TaskToolCall description callId=${displayCallId} ` + + `description=${JSON.stringify(description.trim())}`); + } + } + } + const editCall = displayCallId + ? session.editToolCalls?.get(displayCallId) + : undefined; + // A complete correlated edit read is trusted provenance: unlike an + // ordinary capped read it cannot have appended our warning marker, + // and the source itself may legitimately contain the marker text. + if (!editCall?.completeRead) + rejectPartialReadMutation(parsed); + if (editCall) { + const remapped = remapCorrelatedEditWriteForCatalog(parsed, advertisedToolNameSet, editCall.path, workspaceRootFromRequestContext(session.requestContext)); + if (parsed.resultField === "write_result" && displayCallId) { + session.editToolCalls?.delete(displayCallId); + } + if (remapped) { + trace(`exec: preserved edit intent callId=${displayCallId} ` + + `path=${JSON.stringify(editCall.path)}`); + } + } + // Hosts that advertise `apply_patch` in place of `edit`/`write` (see + // protocol/apply-patch.ts) still receive Cursor's native write/edit + // exec requests. Translate before the unavailable-tool check below. + remapEditToolsForCatalog(parsed, advertisedToolNameSet, workspaceRootFromRequestContext(session.requestContext)); + } + trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`); + if (parsed) { + if (parsed.localError) { + if (!await rejectExec(parsed, parsed.localError, "invalid mapping")) + return; + continue; + } + // OpenCode throws "Tool call not allowed while generating summary" + // when assistantMessage.summary is set. Compaction/summary turns + // advertise no tools — refuse on the Cursor channel and keep + // pumping for text / turn_ended instead of emitting tool-call. + if (!session.allowTools) { + const reason = "Tool calls are not available during this turn (summary/compaction)."; + if (!await rejectExec(parsed, reason, "allowTools=false")) + return; + continue; + } + // Cursor writes a generated image with an ordinary write exec whose + // `file_bytes` are binary (Cursor CLI's agent does exactly this, then + // reads the client's WriteResult back). OpenCode's `write` takes a + // string, so those bytes are staged and committed by the host tool + // that can raise the `edit` permission before anything hits disk. + const binaryWrite = binaryWritePayload(parsed); + if (binaryWrite) { + if (!advertisedToolNameSet.has(CURSOR_IMAGE_SAVE_TOOL)) { + const reason = "This OpenCode agent cannot write binary file content. " + + "Do not retry this write with the same bytes."; + if (!await rejectExec(parsed, reason, "binary write unsupported")) + return; + continue; + } + const workspaceRoot = workspaceRootFromRequestContext(session.requestContext); + const projectDir = ensureOpencodeProjectDir(workspaceRoot); + const target = remapCursorImageWritePath(binaryWrite.path, { + workspaceRoot, + projectDir, + }); + let imageId; + try { + imageId = stageCursorImage({ + path: target, + projectDir, + mime: imageMimeForPath(target), + data: binaryWrite.data, + sessionId: session.openCodeSessionId, + }); + } + catch (error) { + if (!await rejectExec(parsed, error.message, "binary write too large")) + return; + continue; + } + if (displayCallId) + session.displayToolCalls.delete(displayCallId); + sessionManager.registerPending(parsed.id, session, parsed.resultField, CURSOR_IMAGE_SAVE_TOOL, false, { + ...parsed.resultMetadata, + path: target, + binaryWriteBytes: undefined, + imageByteLength: binaryWrite.data.length, + }); + const toolCallId = `cursor_${session.sessionId}_${parsed.id}`; + trace(`exec: binary write STAGED toolCallId=${toolCallId} ` + + `requested=${JSON.stringify(binaryWrite.path)} target=${JSON.stringify(target)} ` + + `bytes=${binaryWrite.data.length}`); + emittedHostTools++; + closeOpenSpans(); + safeEnqueue({ + type: "tool-call", + toolCallId, + toolName: CURSOR_IMAGE_SAVE_TOOL, + input: JSON.stringify({ image_id: imageId }), + }); + emitFinish(undefined, { unified: "tool-calls", raw: undefined }); + return; + } + // Cursor has native capabilities (Task, filesystem, shell, etc.) in + // addition to the MCP descriptors sent by this provider. The model + // can request one even when the current OpenCode agent omitted its + // corresponding host tool. Emitting that request makes OpenCode + // manufacture an `invalid` tool result. Refuse it on the held-open + // Cursor exec channel instead, using the request's exact typed result. + if (!advertisedToolNameSet.has(parsed.toolName)) { + const available = advertisedToolNames.length > 0 + ? advertisedToolNames.join(", ") + : "none"; + const reason = `OpenCode tool '${parsed.toolName}' is unavailable for the current agent. ` + + `Available tools: ${available}. Continue using only available tools; do not retry ` + + `'${parsed.toolName}'.`; + trace(`exec: unavailable catalog target toolName=${parsed.toolName} ` + + `advertised=[${advertisedToolNames.join(",")}]`); + if (!await rejectExec(parsed, reason, "unavailable tool")) + return; + continue; + } + // Epoch advertisement may keep tools the host filtered this turn + // (plan denies edit). Refuse those here — do not emit to OpenCode. + const permittedToolNames = session.permittedToolNames; + if (permittedToolNames + && permittedToolNames.size > 0 + && !permittedToolNames.has(parsed.toolName)) { + const permitted = [...permittedToolNames].sort().join(", "); + const reason = `OpenCode tool '${parsed.toolName}' is not permitted for the current agent this turn. ` + + `Permitted tools: ${permitted || "none"}. Continue using only permitted tools; do not retry ` + + `'${parsed.toolName}'.`; + trace(`exec: not permitted this turn toolName=${parsed.toolName} ` + + `permitted=[${[...permittedToolNames].sort().join(",")}]`); + if (!await rejectExec(parsed, reason, "not permitted this turn")) + return; + continue; + } + if (recoverCorrelatedEditRead(parsed, displayCallId)) + continue; + if (rejectMissingReadTarget(parsed)) { + if (displayCallId) + session.displayToolCalls.delete(displayCallId); + continue; + } + if (displayCallId) { + session.displayToolCalls.delete(displayCallId); + trace(`exec: claimed display callId=${displayCallId}`); + } + const tc = buildToolCallPart(parsed, session.sessionId); + if (parsed.resultField === "shell_stream" + || parsed.resultField === "shell_result" + || parsed.resultField === "background_shell_spawn_result") { + registerCursorShellCall(tc.toolCallId, parsed.resultMetadata); + } + // Keep the stream open; the result arrives on the next doStream call. + sessionManager.registerPending(parsed.id, session, parsed.resultField, parsed.toolName, false, parsed.resultMetadata); + // A direct host `todowrite` is a replace-all snapshot: it is the new + // truth for later Cursor merge patches, which otherwise apply onto a + // stale (or empty) mirrored list. + if (parsed.toolName === "todowrite") { + const snapshot = snapshotMirroredTodos(parsed.args.todos); + if (snapshot !== undefined) { + storeMirroredTodos(session, snapshot); + trace(`exec: mirrored todowrite snapshot items=${snapshot.length}`); + } + } + // tc.input is already a JSON string (LanguageModelV3ToolCall.input). + trace(`exec: EMITTED tool-call toolCallId=${tc.toolCallId} toolName=${tc.toolName} inputLen=${tc.input.length}`); + emittedHostTools++; + // Close open text/reasoning spans before tool-call (required by AI SDK V3). + closeOpenSpans(); + safeEnqueue({ + type: "tool-call", + toolCallId: tc.toolCallId, + toolName: tc.toolName, + input: tc.input, + }); + emitFinish(undefined, { unified: "tool-calls", raw: undefined }); + return; + } + // Known Cursor-native exec variants with no safe OpenCode bridge are + // soft-denied with a populated typed result or throw, so the turn + // continues with remaining tools. Unknown fields still hard-fail. + const variantField = detectExecVariantField(payload); + const variant = variantField !== undefined + ? cursorExecVariantByRequestField(variantField) + : undefined; + if (variant?.handling === "unsupported") { + const advertised = advertisedToolNames.length > 0 ? advertisedToolNames.join(", ") : "none"; + const rawReason = `Cursor-native '${variant.requestName}' is not available on this host. Continue with listed tools: ${advertised}; do not retry '${variant.requestName}'.`; + const grounded = appendWorkspaceRootGrounding(rawReason, workspaceRootFromRequestContext(session.requestContext)); + // Allowlist/status denies have no string channel; buildUnsupportedExecDeny + // ignores `reason` for those shapes and still selects the typed oneof. + const frames = buildUnsupportedExecDeny({ execId: esmId, variant, reason: grounded }); + try { + for (const frame of frames) { + await writeWithBackpressure(session.stream, frame, `unsupported deny ${variant.requestName} id=${esmId}`); + } + } + catch (error) { + rethrowTransportWriteFailure(error); + failRunProtocol("Cursor unsupported exec deny reply failed", RUN_REPLY_FAILED); + } + trace(`exec: SOFT-DENIED ${variant.requestName} id=${esmId}`); + continue; + } + // Never guess a response type for an unknown exec variant. Request and + // result field numbers are not universally identical; a structurally + // wrong reply recreates the heartbeat-only deadlock. Fail promptly so + // schema drift is actionable. + const variantDescription = describeCursorExecVariant(variantField); + const hex = Array.from(payload.subarray(0, 48)) + .map((x) => x.toString(16).padStart(2, "0")) + .join(""); + trace(`exec UNMAPPED: id=${esmId} variant=${variantDescription} keys=[${Object.keys(esm).join(",")}] hex=${hex}`); + failRunProtocol(`Unsupported Cursor exec variant ${variantDescription} (id=${esmId})`, RUN_REQUEST_UNSUPPORTED); + } + } + else if (interactionQuery) { + // InteractionQuery is a must-reply channel, just like exec and KV. AI + // SDK has no Cursor-specific UI callback, so answer immediately with the + // policy from protocol/interactions.ts (AskQuestion bridge, CreatePlan + // write under OpenCode defaults, GenerateImage approve when saveable). + const handled = (() => { + try { + return handleInteractionQuery(interactionQuery, payload, { + canBridgeAskQuestion: session.allowTools && advertisedToolNameSet.has("question"), + allowTools: session.allowTools, + advertisedTools: advertisedToolNameSet, + canSaveGeneratedImage: session.allowTools && advertisedToolNameSet.has(CURSOR_IMAGE_SAVE_TOOL), + canBridgeCreatePlan: session.allowTools + && isBridgedCursorPlanModeActive(session.openCodeSessionId) + && advertisedToolNameSet.has(CURSOR_PLAN_STAGE_TOOL) + && advertisedToolNameSet.has("write"), + planModeActive: isCursorPlanModeActive(session.openCodeSessionId), + ...(getActiveCursorMode(session.openCodeSessionId) + ? { activeCursorModeId: getActiveCursorMode(session.openCodeSessionId) } + : {}), + workspaceRoot: workspaceRootFromRequestContext(session.requestContext), + }); + } + catch { + return failRunProtocol("Cursor interaction request could not be handled", RUN_REQUEST_UNSUPPORTED); + } + })(); + try { + if (handled.outcome === "acknowledged") { + replaySafety.markBarrier("stateful-interaction"); + } + if (handled.reply) { + await writeWithBackpressure(session.stream, handled.reply, `interaction reply id=${handled.id}`); + } + trace(`interaction_query: replied id=${handled.id} variant=${handled.variantName} ` + + `field=${handled.variantField} outcome=${handled.outcome}` + + (handled.reply ? "" : " (deferred to host tool result)")); + // Tag the Run for cache diagnosis: a first CreatePlan/SwitchMode can + // coincide with a one-time upstream tools-category expansion. Set on + // any outcome (acknowledged/approved/bridged) via presence, not kind. + if (handled.createPlan && session.cacheDiagnostics) { + session.cacheDiagnostics.createPlanInTurn = true; + } + if (handled.switchMode && session.cacheDiagnostics) { + session.cacheDiagnostics.switchModeInTurn = true; + } + if (handled.generateImage) { + trace(`interaction_query: approved generate_image target=` + + `${JSON.stringify(handled.generateImage.filePath || "(unspecified)")} ` + + `refs=${handled.generateImage.referenceImagePaths.length} ` + + `cursorToolCallId=${handled.generateImage.toolCallId || "(none)"}`); + } + } + catch (error) { + rethrowTransportWriteFailure(error); + failRunProtocol("Cursor interaction reply failed", RUN_REPLY_FAILED); + } + if (handled.outcome === "bridged" && handled.askQuestion) { + // Cursor raised AskQuestion; OpenCode owns the tool loop, so the prompt + // leaves as a `question` tool call and this doStream ends. The Run stays + // open on the pending entry, and the answer is written back on the next + // continuation (InteractionResponse when Cursor is blocking on it, + // ConversationAction when it was already acknowledged with `async`). + const ask = handled.askQuestion; + const execId = session.nextBridgedExecId++; + sessionManager.registerPending(execId, session, ASK_QUESTION_RESULT_FIELD, "question", false, { + interactionId: handled.id, + askQuestionArgs: ask.args, + askQuestionRawArgs: ask.rawArgs, + askQuestionToolCallId: ask.toolCallId, + }); + const toolCallId = `cursor_${session.sessionId}_${execId}`; + const input = JSON.stringify(askQuestionToolInput(ask.args)); + trace(`interaction_query: BRIDGED ask_question id=${handled.id} toolCallId=${toolCallId} ` + + `questions=${ask.args.questions.length} runAsync=${ask.args.runAsync} ` + + `cursorToolCallId=${ask.toolCallId || "(none)"}`); + emittedHostTools++; + closeOpenSpans(); + safeEnqueue({ + type: "tool-call", + toolCallId, + toolName: "question", + input, + }); + emitFinish(undefined, { unified: "tool-calls", raw: undefined }); + return; + } + if (handled.outcome === "approved" && handled.switchMode) { + // Entering plan mode without a host plan tool: Cursor was already + // released with approved{} above, and the behavioural contract travels + // as the injected on subsequent Runs. Nothing is + // pending, so keep pumping this Run rather than ending doStream. + const sw = handled.switchMode; + setActiveCursorMode(session.openCodeSessionId, sw.args.targetModeId, { + bridgedPlanEntered: false, + }); + const hostAgentSwitchQueued = session.openCodeSessionId + ? queueHostAgentModeSwitch({ + sessionID: session.openCodeSessionId, + targetModeID: sw.args.targetModeId, + cursorSessionID: session.sessionId, + }) + : false; + trace(`interaction_query: APPROVED switch_mode id=${handled.id} ` + + `target=${JSON.stringify(sw.args.targetModeId)} (no host plan tool; ` + + `${hostAgentSwitchQueued ? "native host-agent switch queued" : "provider-owned fallback"}) ` + + `cursorToolCallId=${sw.toolCallId || "(none)"}`); + continue; + } + if (handled.outcome === "bridged" && handled.switchMode) { + // Cursor raised SwitchMode; the host owns the outcome, so the switch + // leaves as a tool call (native plan_enter / plan_exit, or the emulated + // `question` approval prompt) and this doStream ends. The Run stays open + // on the pending entry; approved/rejected is written back on the next + // continuation while Cursor is still blocking on the query. + const sw = handled.switchMode; + const bridgeKind = sw.bridge.kind; + const toolName = sw.toolName ?? "plan_exit"; + const execId = session.nextBridgedExecId++; + sessionManager.registerPending(execId, session, SWITCH_MODE_RESULT_FIELD, toolName, false, { + interactionId: handled.id, + switchModeTarget: sw.args.targetModeId, + switchModeToolCallId: sw.toolCallId, + switchModeBridgeKind: bridgeKind, + }); + const toolCallId = `cursor_${session.sessionId}_${execId}`; + const input = JSON.stringify(sw.bridge.kind === "question" ? sw.bridge.input : switchModeToolInput()); + trace(`interaction_query: BRIDGED switch_mode id=${handled.id} toolCallId=${toolCallId} ` + + `bridge=${bridgeKind} hostTool=${toolName} ` + + `target=${JSON.stringify(sw.args.targetModeId)} ` + + `cursorToolCallId=${sw.toolCallId || "(none)"}`); + emittedHostTools++; + closeOpenSpans(); + safeEnqueue({ + type: "tool-call", + toolCallId, + toolName, + input, + }); + emitFinish(undefined, { unified: "tool-calls", raw: undefined }); + return; + } + if (handled.outcome === "bridged" && handled.createPlan) { + // Either a host plan-stage tool (writes the plan and runs its own + // approval UI) or the emulated prompt after the provider already wrote + // the plan. Both hold Cursor's query open until the user decides. + const plan = handled.createPlan; + const staged = plan.bridge.kind === "stage" ? createPlanStageInput(plan.args) : undefined; + const planUri = staged?.plan_uri ?? plan.planUri ?? ""; + const input = staged ?? plan.questionInput ?? {}; + const stageExecId = session.nextBridgedExecId++; + sessionManager.registerPending(stageExecId, session, CREATE_PLAN_RESULT_FIELD, plan.toolName, false, { + interactionId: handled.id, + createPlanToolCallId: plan.toolCallId, + createPlanBridgeKind: plan.bridge.kind, + // The host echoes the prompt verbatim; it anchors the answer parse. + createPlanQuestion: plan.questionInput?.questions[0]?.question ?? "", + planUri, + // Absolute path for the post-Yes OpenCode kickoff (plan_exit shape). + ...(typeof plan.planPath === "string" && plan.planPath.trim() + ? { planPath: plan.planPath.trim() } + : {}), + workspaceRoot: workspaceRootFromRequestContext(session.requestContext), + }); + const stageToolCallId = `cursor_${session.sessionId}_${stageExecId}`; + trace(`interaction_query: BRIDGED create_plan id=${handled.id} bridge=${plan.bridge.kind} ` + + `hostTool=${plan.toolName} stageToolCallId=${stageToolCallId} ` + + `planUri=${planUri || "(none)"} cursorToolCallId=${plan.toolCallId || "(none)"}`); + // Cursor sends the plan body through the interaction query, not the + // text stream, so nothing has shown it yet. Put it in the transcript + // before asking — approving a plan you cannot read is not approval. + if (plan.planReview) + emitText(plan.planReview); + emittedHostTools++; + closeOpenSpans(); + safeEnqueue({ + type: "tool-call", + toolCallId: stageToolCallId, + toolName: plan.toolName, + input: JSON.stringify(input), + }); + emitFinish(undefined, { unified: "tool-calls", raw: undefined }); + return; + } + } + else if (kv) { + // KV blob channel: ack set_blob / answer get_blob, then keep pumping. + // Not replying hangs the turn — see protocol/kv.ts. + trace(`kv frame raw: gunzippedLen=${payload.length} id=${kv.id ?? "?"} ` + + `get=${!!kv.get_blob_args} set=${!!kv.set_blob_args} ` + + `getBlobIdLen=${kv.get_blob_args?.blob_id?.length ?? "-"} ` + + `setBlobIdLen=${kv.set_blob_args?.blob_id?.length ?? "-"} ` + + `setDataLen=${kv.set_blob_args?.blob_data?.length ?? "-"}`); + const handled = handleKvServerMessage(kv, session); + if (handled) { + try { + await writeWithBackpressure(session.stream, handled.reply, `KV ${handled.kind}_blob reply id=${handled.id}`); + trace(`kv replied: kind=${handled.kind} id=${handled.id} blobId=${handled.blobIdHex.slice(0, 16)}… ` + + `found=${handled.found} echoed=${!!handled.echoed} ` + + `replyBytes=${handled.reply.length} replyBlobBytes=${handled.replyBlobBytes} ` + + `sessionBlobs=${session.blobs.size} convBlobs=${conversationBlobCount(session.conversationId)}`); + } + catch (error) { + rethrowTransportWriteFailure(error); + failRunProtocol("Cursor KV reply failed", RUN_REPLY_FAILED); + } + } + else { + failRunProtocol("Cursor KV request could not be handled", RUN_REQUEST_UNSUPPORTED); + } + } + } + catch (e) { + if (e instanceof CursorProviderError) + throw e; + if (requiredChannel) { + failRunProtocol(`Cursor ${requiredChannel} request could not be handled`, RUN_REQUEST_UNSUPPORTED); + } + // Any per-frame dispatch throw (e.g. protobufjs length overrun in + // exec/args decode) must not abort the whole turn — log and skip. + trace(`frame dispatch FAILED (skipping): topField=${topField}`); + } + // heartbeat / step / partial_tool_call → ignore (partial args are + // display-only; the exec channel is authoritative. Checkpoints and + // interaction queries are handled above.) + } +} +/** Normalize protobufjs bytes / Buffer / number[] into a Uint8Array. */ +function normalizeCheckpointBytes(raw) { + if (raw instanceof Uint8Array) + return raw; + if (Buffer.isBuffer(raw)) + return new Uint8Array(raw); + if (Array.isArray(raw)) + return Uint8Array.from(raw); + if (raw && typeof raw === "object" && "type" in raw && "data" in raw) { + // protobufjs sometimes yields { type: "Buffer", data: number[] } + const data = raw.data; + if (Array.isArray(data)) + return Uint8Array.from(data); + } + return undefined; +} +function extractToolResults(prompt) { + const out = []; + for (const msg of prompt) { + if (msg.role !== "tool" || !Array.isArray(msg.content)) + continue; + for (const part of msg.content) { + const p = part; + if (p.type !== "tool-result") + continue; + const toolCallId = p.toolCallId ?? ""; + const parsed = parseExecIdFromToolCallId(toolCallId); + if (!parsed) + continue; + const { text, isError } = toolResultOutputToText(p.output); + out.push({ + toolCallId, + sessionId: parsed.sessionId, + execId: parsed.execId, + toolName: p.toolName ?? "mcp", + output: text, + error: isError ? text : undefined, + }); + } + } + return out; +} +/** + * Tool results that form a live continuation: only the trailing run of `tool` + * messages after the last non-tool message. Mid-prompt historical tool results + * are ignored — they are conversation history, not replies for a held-open Run. + */ +export function extractTrailingToolResults(prompt) { + if (prompt.length === 0) + return []; + let i = prompt.length - 1; + while (i >= 0 && prompt[i].role === "tool") + i--; + // Continuations end with tool messages. Anything else (user/assistant/system) + // means this is a fresh model call that merely carries tools in history. + if (i === prompt.length - 1) + return []; + return extractToolResults(prompt.slice(i + 1)); +} +function toolResultOutputToText(output) { + if (output == null) + return { text: "", isError: false }; + if (typeof output === "string") + return { text: output, isError: false }; + const o = output; + // LanguageModelV3 tool-result output: { type: "text"|"json"|"error-text"|..., value } + const isError = typeof o.type === "string" && o.type.startsWith("error"); + if (o.type === "text" || o.type === "error-text") { + return { text: String(o.value ?? ""), isError }; + } + if (o.type === "json" || o.type === "error-json") { + return { text: JSON.stringify(o.value ?? null), isError }; + } + if (o.type === "content" && Array.isArray(o.value)) { + // Held-open continuation exec frames are text-only. Media is intentionally + // omitted here and harvested from the full prompt when a fresh Run opens. + const text = o.value + .map((c) => { + const cp = c; + return cp.type === "text" ? String(cp.text ?? "") : ""; + }) + .join(""); + return { text, isError }; + } + return { text: JSON.stringify(output), isError }; +} +function extractSystemPrompt(prompt) { + const parts = []; + for (const m of prompt) { + if (m.role === "system" && typeof m.content === "string") + parts.push(m.content); + } + return parts.length > 0 ? parts.join("\n\n") : undefined; +} +/** + * Checkpointed Runs do not resend the system prompt. Keep the workspace root + * on the live user message, and require absolute `path` arguments when that is + * the host's file-tool dialect. + */ +export function groundCheckpointTurnText(userText, checkpoint, workspaceRoot, tools) { + if (!checkpoint) + return userText; + return appendCheckpointUserGrounding(userText, workspaceRoot, { + requireAbsolutePathArg: hostToolDialectFromTools(tools).filePathKey === "path", + }); +} +/** + * Cursor's native UI interactions cannot be surfaced through the AI SDK. + * Redirect only to OpenCode tools that are genuinely advertised this turn; + * compaction keeps its dedicated summary prompt unchanged. + */ +export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceRoot) { + if (isCompaction) + return undefined; + const names = new Set(tools.map((tool) => tool.name)); + if (names.size === 0) + return undefined; + const instructions = []; + const subagents = extractHostSubagentCatalog(tools); + if (names.has("question")) { + // Cursor-native AskQuestion is translated into this tool (see + // protocol/ask-question.ts), so both routes reach the user. Saying so stops + // the model from treating a question as impossible when it reaches for its + // native interaction first. + instructions.push("- When user input is required, call the OpenCode `question` tool. Cursor-native AskQuestion requests are also accepted and answered through it."); + } + // CreatePlan is a Cursor InteractionQuery (#7), not an OpenCode/MCP catalog + // tool. Always say so when we advertise any tools — otherwise the model sees + // CreatePlan in Cursor's function definitions, notices it is absent from the + // OpenCode list, and narrates "No CreatePlan MCP tool is available" even + // though the provider bridges the native interaction. + instructions.push(names.has("cursor_plan_stage") + ? "- Cursor-native CreatePlan is accepted as a Cursor interaction (not an OpenCode or MCP catalog tool). Raise it normally. The host stage tool waits for the host plan review and does not return until the user accepts or declines. Do not call `plan_exit` to submit or skip that review, and do not implement until that tool returns success. Do not narrate that CreatePlan is missing, unavailable, or not an MCP tool. When the task will need a user-approved plan, record the first version as soon as its shape is clear, then keep investigating: refining afterward is expected and follow-up plans cost nothing extra. Do not defer the first plan until investigation is complete." + : "- Cursor-native CreatePlan is accepted as a Cursor interaction (not an OpenCode or MCP catalog tool). Raise it normally; the provider writes the plan under the host's calculated plans directory and handles execution approval. Do not narrate that CreatePlan is missing, unavailable, or not an MCP tool. When the task will need a user-approved plan, record the first version as soon as its shape is clear, then keep investigating: refining afterward is expected and follow-up plans cost nothing extra. Do not defer the first plan until investigation is complete."); + if (names.has("plan_enter")) { + instructions.push("- To enter plan mode, call the OpenCode `plan_enter` tool. Cursor-native SwitchMode requests for plan/spec are also accepted and answered through it."); + } + if (names.has("plan_exit")) { + instructions.push("- To leave plan mode, call the OpenCode `plan_exit` tool. Cursor-native SwitchMode for any non-plan target (agent, build, chat, debug, edit, background, multitask, triage, project, …) is also accepted and answered through it; the provider then injects the Cursor CLI-shaped mode reminder for that target."); + } + // Prefer host todos whenever advertised. Keep guidance terse — spelling out + // Cursor TodoWrite vs TodoRead / MCP alias collisions teaches the model to + // inventory catalogs instead of just calling the host tools. + if (names.has("todowrite") || names.has("todoread")) { + const write = names.has("todowrite") ? "`todowrite`" : undefined; + const read = names.has("todoread") ? "`todoread`" : undefined; + const tools = write && read ? `${write} / ${read}` : (write ?? read); + instructions.push(`- For task-list create/update/complete/cancel${read ? "/read" : ""}, call OpenCode ${tools}; do not use Cursor TodoWrite, and do not narrate Cursor-vs-OpenCode todo-tool differences.`); + } + if (names.has(CUSTOM_WEBSEARCH_TOOL)) { + instructions.push(`- For web searches, call \`${CUSTOM_WEBSEARCH_TOOL}\`; do not use Cursor's native WebSearch interaction.`); + } + if (names.has(CUSTOM_WEBFETCH_TOOL)) { + instructions.push(`- To fetch a known URL, call \`${CUSTOM_WEBFETCH_TOOL}\`; do not use Cursor's native WebFetch interaction.`); + } + if (names.has(CUSTOM_LIST_MCP_RESOURCES_TOOL)) { + instructions.push(`- To list MCP resources, call \`${CUSTOM_LIST_MCP_RESOURCES_TOOL}\`; do not use Cursor's native resource-listing interaction.`); + } + if (names.has(CUSTOM_READ_MCP_RESOURCE_TOOL)) { + instructions.push(`- To read an MCP resource, call \`${CUSTOM_READ_MCP_RESOURCE_TOOL}\`; do not use Cursor's native resource-reading interaction.`); + } + if (names.has("execute")) { + const shell = names.has("shell") ? "`shell`" : names.has("bash") ? "`bash`" : undefined; + instructions.push(shell + ? `- OpenCode \`execute\` is Code Mode JavaScript (\`code\`); it is not a shell. For OS commands, call OpenCode ${shell}. Do not pass \`command\` to \`execute\`.` + : "- OpenCode `execute` is Code Mode JavaScript (`code`); it is not a shell. Do not pass `command` to `execute`."); + instructions.push("- When the host's Code Mode catalog lists additional tools, including MCP server tools, call them inside `execute` through `tools`. Use only the exact paths and signatures in that catalog or returned by its `search` function. Call `execute` with `{ code }` to run them; do not request a Code Mode tool as a direct OpenCode tool call."); + } + if (names.has("task") || names.has("subagent")) { + const target = names.has("task") ? "`task`" : "`subagent`"; + const available = subagents.agents.map((agent) => `\`${agent.name}\``).join(", "); + instructions.push(`- Native Cursor Task/subagent requests are executed through OpenCode ${target}. ` + + "Advertised custom subagent names are used exactly; otherwise `unspecified` and `generalPurpose` select host `general`, " + + "`bugbot`, `security-review`, and `explore` select host `explore` (then `general`), and other specialized Cursor types fall back to `general`." + + (available ? ` Spawnable host agents this turn: ${available}.` : "")); + if (subagents.agents.some((agent) => agent.name === "scout")) { + instructions.push("- Host `scout` is available for external documentation and dependency-source research. Use Cursor `cursor-guide` for that use case; local repository discovery still uses `bugbot`/`explore`."); + } + } + if (names.has("write")) { + instructions.push(names.has("edit") + ? "- For file changes, use OpenCode `edit` for targeted changes to existing files and `write` to create files or intentionally replace complete contents; do not use shell, Python, or heredocs to change file content while these tools are available." + : "- Use OpenCode `write` for file-content changes; do not use shell, Python, or heredocs to change file content while it is available."); + } + else if (names.has("apply_patch")) { + // This host withheld `edit`/`write` and offers `apply_patch` instead. Native + // Cursor write/edit requests are translated into it, so say so rather than + // leaving the turn with no file-editing guidance at all. + instructions.push("- Use OpenCode `apply_patch` for file-content changes; do not use shell, Python, or heredocs to change file content while it is available. Cursor-native write and edit requests are accepted and converted to `apply_patch` automatically."); + } + if (hostToolDialectFromTools(tools).filePathKey === "path") { + instructions.push("- OpenCode file tools take `path` as an absolute path under the workspace root above. Do not pass a project-relative path, and do not invent a different absolute prefix."); + } + if (names.has("edit") || names.has("write") || names.has("apply_patch")) { + instructions.push("- Never use a read result as complete file content when it says the output is capped, partial, or requires another offset. Read the remaining ranges first, or make a targeted edit/patch from complete context; do not pass a partial read back as a whole-file replacement."); + } + return [ + `OpenCode exposes these direct tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`, + `Workspace root: ${JSON.stringify(workspaceRoot)}. Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them.`, + subagents.executor + ? "Call only tools in that direct OpenCode list for ordinary host execution. Cursor-native Task/subagent requests are permitted because a compatible host executor is listed. Bridged Cursor interactions named below (AskQuestion, SwitchMode, CreatePlan, …) are not OpenCode/MCP catalog tools — raise them normally and do not narrate that they are missing." + : "Call only tools in that direct OpenCode list for ordinary host execution. Bridged Cursor interactions named below (AskQuestion, SwitchMode, CreatePlan, …) are not OpenCode/MCP catalog tools — raise them normally and do not narrate that they are missing. Other unlisted Cursor-native tools are not bridged; complete the work with the listed tools or explain the limitation without claiming a missing MCP tool.", + ...(instructions.length > 0 + ? ["Use these OpenCode tools instead of equivalent Cursor-native UI interactions:"] + : []), + ...instructions, + "Emit the actual tool call and wait for its result; never merely claim or summarize that a tool was used.", + "A progress update such as \"Checking…\", \"Inspecting…\", or \"Let me look…\" is not a final answer.", + "After progress narration, call a listed tool in the same turn; if no tool is needed, provide the complete user-facing answer before finishing.", + "Never end a turn with progress narration alone.", + ].join("\n"); +} +/** Rough char→token estimate for mid-turn usage before TurnEnded arrives. */ +export function estimateTokens(chars) { + if (!Number.isFinite(chars) || chars <= 0) + return 0; + return Math.ceil(chars / 4); +} +/** Preserve exact request-local Cursor counters as diagnostics. */ +export function cursorTurnEndedProviderMetadata(te, tokenDetails, contextSource) { + return { + cursor: { + usageVersion: 3, + inputTokensRaw: turnEndedCounter(te, "input_tokens"), + outputTokensRaw: turnEndedCounter(te, "output_tokens"), + cacheReadRaw: turnEndedCounter(te, "cache_read"), + cacheWriteRaw: turnEndedCounter(te, "cache_write"), + reasoningTokensRaw: turnEndedCounter(te, "reasoning_tokens"), + ...(tokenDetails + ? { context: cursorContextUsageMetadata(tokenDetails, contextSource) } + : {}), + }, + }; +} +/** + * Prior prompt turns for a seed ConversationStateStructure. Tool results must + * never be replayed as assistant-authored prose: that teaches the model to + * counterfeit `Tool result (...)` text instead of emitting a real tool call. + * Normal rebases omit old results; compaction can retain all results and + * interrupted continuations retain only the trailing live result suffix as + * explicit OpenCode-host observations. + */ +export function extractPromptHistory(prompt, options) { + const out = []; + const toolResults = options?.toolResults ?? "omit"; + let trailingToolStart = prompt.length; + if (toolResults === "trailing") { + while (trailingToolStart > 0 && prompt[trailingToolStart - 1]?.role === "tool") { + trailingToolStart--; + } + } + for (let messageIndex = 0; messageIndex < prompt.length; messageIndex++) { + const m = prompt[messageIndex]; + if (m.role === "system") { + if (typeof m.content === "string" && m.content.length > 0) { + out.push({ role: "system", content: m.content }); + } + continue; + } + if (m.role === "user") { + const text = extractUserText(m); + if (text && text !== ".") + out.push({ role: "user", content: text }); + continue; + } + if (m.role === "assistant") { + const text = extractAssistantHistoryText(m); + if (text) + appendSeedHistory(out, "assistant", text); + continue; + } + if (m.role === "tool" && Array.isArray(m.content)) { + if (toolResults === "omit" || + (toolResults === "trailing" && messageIndex < trailingToolStart)) + continue; + const results = []; + for (const part of m.content) { + const p = part; + if (p.type !== "tool-result") + continue; + const toolName = typeof p.toolName === "string" && p.toolName ? p.toolName : "tool"; + const toolCallId = typeof p.toolCallId === "string" ? p.toolCallId : ""; + const result = toolResultOutputToText(p.output); + results.push(formatSeedToolObservation({ + toolName, + toolCallId, + output: result.text, + isError: result.isError, + })); + } + if (results.length > 0) + appendSeedHistory(out, "user", results.join("\n\n")); + } + } + // Live user message is the Run action, not seed history. + if (!options?.preserveTrailingUser && out.length > 0 && out[out.length - 1].role === "user") { + out.pop(); + } + return out; +} +function formatSeedToolObservation(input) { + const metadata = JSON.stringify({ + source: "opencode-tool", + tool: input.toolName, + callId: input.toolCallId, + status: input.isError ? "error" : "completed", + }); + return `OpenCode host observation ${metadata}:\n${input.output}`; +} +function extractAssistantHistoryText(msg) { + const content = msg.content; + if (typeof content === "string") + return content; + if (!Array.isArray(content)) + return ""; + const texts = []; + for (const part of content) { + const p = part; + if (p.type === "text" && typeof p.text === "string" && p.text.length > 0) { + texts.push(p.text); + } + } + return texts.join("\n"); +} +function appendSeedHistory(out, role, content) { + if (!content) + return; + const last = out[out.length - 1]; + if (last?.role === role) { + last.content += `\n\n${content}`; + return; + } + out.push({ role, content }); +} +/** OpenCode session id header, if present. */ +export function opencodeSessionKey(callOptions) { + const h = callOptions.headers ?? {}; + const raw = h["x-session-id"] ?? + h["X-Session-Id"] ?? + h["x-session-affinity"] ?? + h["x-opencode-session"]; + if (typeof raw === "string" && raw.trim().length > 0) + return raw.trim(); + return undefined; +} +/** + * Map OpenCode's session id header to the active Cursor conversation_id. + * Compaction resets remint via bindConversationId; otherwise the binding is + * sticky for the OpenCode session. Falls back to a random UUID with no header. + */ +export function resolveConversationId(callOptions) { + return bindConversationId(opencodeSessionKey(callOptions)).conversationId; +} +export { sessionIdToUuid } from "./protocol/conversation-bind.js"; +function extractTools(callOptions) { + const tools = callOptions.tools; + if (!tools || tools.length === 0) { + trace("extractTools: callOptions.tools empty/missing"); + return []; + } + const out = []; + for (const t of tools) { + // LanguageModelV3FunctionTool always has type:"function". Be defensive in + // case a middleware strips it or passes schema as parameters/schema. + const any = t; + const schema = any.inputSchema ?? any.parameters ?? any.schema; + if (any.type === "function" || (any.name && schema !== undefined)) { + if (!any.name) + continue; + out.push({ name: any.name, description: any.description, inputSchema: schema }); + } + } + trace(`extractTools: ${tools.length} incoming → ${out.length} advertised [${out.map((t) => t.name).join(",")}]`); + return out; +} +/** Exported for tests — AI SDK V3 span ends that must precede finish / tool-call. */ +export function spanEndParts(opts) { + const out = []; + if (opts.reasoningStarted) + out.push({ type: "reasoning-end", id: opts.reasoningId }); + if (opts.textStarted) + out.push({ type: "text-end", id: opts.textId }); + return out; +} +/** UTF-16 name order for the first catalog freeze and for newcomers only. */ +function toolsInFixedOrder(tools) { + return tools + .map((tool) => ({ ...tool })) + .sort((left, right) => { + const a = left.name ?? ""; + const b = right.name ?? ""; + return a < b ? -1 : a > b ? 1 : 0; + }); +} +/** Exported for tests — false for compaction/summary (no tools) and toolChoice none. */ +export function computeAllowTools(toolCount, toolChoice) { + return toolCount > 0 && toolChoice?.type !== "none"; +} +export async function resolveTurnToolState(input) { + const { sessionKey, incomingTools, isCompaction } = input; + // Advertisement and permission are deliberately independent. + // + // Advertisement must stay byte-stable across every Run of a conversation or + // the RequestContext changes shape and Cursor's prompt cache goes cold. + // + // OpenCode V1 *does* shrink the catalog on plan (edit denied → tools dropped + // in request.ts resolveTools). Copying that into Cursor RequestContext costs + // the whole tools prefix. Prefer: keep the epoch's fullest catalog for + // advertisement; compute allowTools from what actually arrived this turn. + // + // A zero-tool call is never a smaller catalog — it is a lifecycle turn + // (compaction, title generation) that re-advertises the last real catalog. + // New tool names (MCP connect) append at the tail without rewriting + // descriptors already frozen. Equal name-sets and host shrinks keep the + // frozen advertisement and its order — schema/description churn must not + // retokenize tools, and inserting a name that sorts earlier than `z` must + // not reshuffle the prefix. + // + // On cold start the lifecycle Run may arrive before any catalog exists. For a + // valid session key, wait until a sibling doStream publishes the first real + // catalog; cancellation is the only escape. + let advertisedTools; + if (incomingTools.length > 0) { + if (sessionKey) { + const cached = toolCatalogBySession.get(sessionKey); + if (cached && cached.length > 0) { + const cachedNames = new Set(cached.map((tool) => tool.name)); + const incomingNames = new Set(incomingTools.map((tool) => tool.name)); + const hasNew = [...incomingNames].some((name) => !cachedNames.has(name)); + if (hasNew) { + const newcomers = toolsInFixedOrder(incomingTools.filter((tool) => !cachedNames.has(tool.name))); + const merged = [...cached, ...newcomers]; + rememberToolCatalog(sessionKey, merged); + advertisedTools = merged; + } + else { + rememberToolCatalog(sessionKey, cached); + advertisedTools = cached; + } + } + else { + const initial = toolsInFixedOrder(incomingTools); + rememberToolCatalog(sessionKey, initial); + advertisedTools = initial; + } + } + else { + advertisedTools = toolsInFixedOrder(incomingTools); + } + } + else if (sessionKey) { + const cached = toolCatalogBySession.get(sessionKey) + ?? await waitForSiblingToolCatalog(sessionKey, input.abortSignal); + advertisedTools = cached; + } + else { + advertisedTools = []; + } + return { + advertisedTools, + allowTools: !isCompaction && computeAllowTools(incomingTools.length, input.toolChoice), + }; +} +export function resolveTurnConversationReset(input) { + const { sessionKey, isCompaction } = input; + if (isCompaction) { + if (sessionKey) + rememberPostCompactionRebase(sessionKey); + return { reset: true, reason: "compaction" }; + } + if (input.historyRewrite) { + if (sessionKey) + postCompactionRebaseBySession.delete(sessionKey); + return { reset: true, reason: "history-rewrite" }; + } + // Diagnostics only: accept promptIdentity for remember without reminting. + if (sessionKey && input.promptIdentity) { + const previous = promptIdentityBySession.get(sessionKey); + rememberPromptIdentity(sessionKey, { + ...previous, + ...normalizePromptIdentity(input.promptIdentity), + }); + } + if (sessionKey && postCompactionRebaseBySession.delete(sessionKey)) { + return { reset: true, reason: "post-compaction-rebase" }; + } + return { reset: false }; +} +export function resetTurnStateForTests() { + for (const waiters of toolCatalogWaitersBySession.values()) { + for (const waiter of waiters) + waiter.cancel(); + } + toolCatalogWaitersBySession.clear(); + toolCatalogBySession.clear(); + postCompactionRebaseBySession.clear(); + promptIdentityBySession.clear(); + mirroredTodosBySession.clear(); + resetContextEpochsForTests(); +} +function extractUserText(lastUser) { + if (!lastUser) + return "."; + const content = lastUser.content; + if (typeof content === "string") + return content; + if (Array.isArray(content)) { + const texts = []; + for (const part of content) { + const p = part; + if (p.type === "text" && typeof p.text === "string") + texts.push(p.text); + } + if (texts.length > 0) + return texts.join("\n"); + } + return "."; +} +function foldStreamParts(parts) { + let text = ""; + let reasoning = ""; + const content = []; + let finishReason = { unified: "stop", raw: undefined }; + let providerMetadata; + let usage = { + inputTokens: { total: undefined, noCache: undefined, cacheRead: undefined, cacheWrite: undefined }, + outputTokens: { total: undefined, text: undefined, reasoning: undefined }, + }; + for (const part of parts) { + if (part.type === "text-delta") + text += part.delta; + else if (part.type === "reasoning-delta") + reasoning += part.delta; + else if (part.type === "tool-call") { + content.push({ + type: "tool-call", + toolCallId: part.toolCallId, + toolName: part.toolName, + input: part.input, + }); + } + else if (part.type === "finish") { + finishReason = part.finishReason; + usage = part.usage; + providerMetadata = part.providerMetadata; + } + } + if (reasoning) + content.unshift({ type: "reasoning", text: reasoning }); + if (text) + content.unshift({ type: "text", text }); + return { + content, + finishReason, + usage, + warnings: [], + ...(providerMetadata ? { providerMetadata } : {}), + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/model-config.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/model-config.d.ts new file mode 100644 index 00000000..6f3c557e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/model-config.d.ts @@ -0,0 +1,17 @@ +import { type ModelInfo, type ModelVariant } from "./models.js"; +/** + * Display names shared by both a thinking and a non-thinking model. A thinking + * model with such a name needs a "Thinking" tag to disambiguate it from its + * non-thinking twin (Cursor's Claude/Fable/Sonnet families). Models whose names + * are already unique — including Cursor's GPT family, where the reasoning tier + * ("None"/"Low"/"High"…) is baked into the name — are excluded, so they aren't + * tagged redundantly. + */ +export declare function thinkingSuffixBaseNames(models: ModelInfo[]): Set; +export declare function modelInfoToConfig(mi: ModelInfo, options?: { + thinkingSuffix?: boolean; + contextTier?: "base" | "long"; + variants?: ModelVariant[]; + fast?: boolean; +}): Record; +export declare function modelsToConfig(models: ModelInfo[]): Record; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/model-config.js b/references/upstream-plugins/cursor-opencode-provider/dist/model-config.js new file mode 100644 index 00000000..b8f8fbdc --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/model-config.js @@ -0,0 +1,284 @@ +import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, parseCursorContextLimit, } from "./models.js"; +import { applyCursorModelCost, hasCursorFastPricing } from "./pricing.js"; +import { getDocumentedCursorModelContext, resolveCursorModelSupportsImages, } from "./model-metadata.js"; +/** + * Cursor `ModelInfo` → OpenCode model-config mapping. + * + * Host-neutral on purpose: both the classic/1.18 plugins and the OpenCode 2.0 + * plugin derive their catalogs from here, so model naming, thinking suffixes, + * and long-context tiering stay identical across every surface. Do not import + * a host plugin API into this module. + */ +/** + * Strip characters and markup that break rendering in the OpenCode TUI/GUI from + * a model or variant display name: + * • HTML tags (e.g. `Medium`) — the IDE colours variant + * suffixes with a CSS var that doesn't exist in OpenCode, so the raw markup + * would show as literal text. We drop the tags and keep the inner text + * ("Medium"). + * • HTML/markup chars (`< > & " ' \``), parentheses. + * • Tabs/newlines collapse to single spaces. + * Dots and unicode letters are preserved so names stay readable. + * Fixes https://github.com/oakimov/cursor-opencode-provider/issues/2. + */ +function stripMarkupTags(value) { + const chunks = []; + let cursor = 0; + while (cursor < value.length) { + const start = value.indexOf("<", cursor); + if (start === -1) { + chunks.push(value.slice(cursor)); + break; + } + chunks.push(value.slice(cursor, start)); + const end = value.indexOf(">", start + 1); + if (end === -1) { + chunks.push(value.slice(start)); + break; + } + cursor = end + 1; + } + return chunks.join(""); +} +function safeLabel(value) { + return (stripMarkupTags(value) + .replace(/[()<>&"'`]/g, "") + .replace(/\s+/g, " ") + .trim() || "default"); +} +function baseName(mi) { + return safeLabel(mi.displayName ?? mi.id); +} +function modelInfoVariants(mi, variants) { + if (variants.length === 0) + return undefined; + const entries = {}; + const usedKeys = new Set(); + const baseName = safeLabel(mi.displayName ?? mi.id); + // Each variant's key is `safeLabel(displayName)` (the IDE's own label, e.g. + // "Opus 4.8 1M High Fast Thinking") so the picker matches what the user + // sees in Cursor. Two variants can sanitize to the same name when the IDE + // wraps a differentiator in `…` (e.g. Composer's "Fast" + // suffix collapses to the bare model name after stripping). To guarantee + // every variant stays pickable: + // 1. If the sanitized displayName matches the model name itself, suffix + // it with distinguishing params (or "default") so it never collides + // with the model entry in the variant panel. + // 2. If two variants still collide, tag the later one with its params. + // Suffixes must stay free of `()` / markup chars — same constraint as + // safeLabel (issue #2); use spaced tokens, not parenthetical tags. + const tagDims = (p) => { + const labels = []; + for (const d of p) { + if (d.id === "fast" && d.value === "true") + labels.push("Fast"); + else if (d.id === "thinking" && d.value === "true") + labels.push("Thinking"); + else if (d.id === "context") + labels.push(d.value); + } + if (labels.length > 0) + return ` ${labels.join(" ")}`; + // No params at all — still disambiguate from the model name itself. + if (p.length === 0) + return ""; + return " default"; + }; + for (const v of variants) { + const sanitized = safeLabel(v.displayName || v.key || "default"); + let key = sanitized; + // Never let a variant key equal the model name — that would make the + // variant entry indistinguishable from the model entry in pickers that + // collapse them. + if (key === baseName && !usedKeys.has(key)) { + key = `${baseName}${tagDims(v.parameterValues)}` || `${baseName} default`; + } + else if (usedKeys.has(key)) { + key = `${sanitized}${tagDims(v.parameterValues)}`; + } + let n = 2; + while (usedKeys.has(key)) + key = `${sanitized}${tagDims(v.parameterValues)} ${n++}`; + usedKeys.add(key); + entries[key] = { + [CURSOR_VARIANT_PARAMETERS_KEY]: v.parameterValues.map((p) => ({ ...p })), + }; + } + return entries; +} +function isLongContextVariant(v) { + return v.parameterValues.some((p) => p.id === "context" && parseCursorContextLimit(p.value) === 1_000_000); +} +function isFastVariant(v) { + return v.parameterValues.some((p) => p.id === "fast" && p.value === "true"); +} +function variantsForTier(mi, tier) { + return mi.variants.filter((v) => isLongContextVariant(v) === (tier === "long")); +} +/** + * Display names shared by both a thinking and a non-thinking model. A thinking + * model with such a name needs a "Thinking" tag to disambiguate it from its + * non-thinking twin (Cursor's Claude/Fable/Sonnet families). Models whose names + * are already unique — including Cursor's GPT family, where the reasoning tier + * ("None"/"Low"/"High"…) is baked into the name — are excluded, so they aren't + * tagged redundantly. + */ +export function thinkingSuffixBaseNames(models) { + const flags = new Map(); + for (const m of models) { + const base = baseName(m); + const entry = flags.get(base) ?? { hasThinking: false, hasNonThinking: false }; + if (m.supportsThinking) + entry.hasThinking = true; + else + entry.hasNonThinking = true; + flags.set(base, entry); + } + const ambiguous = new Set(); + for (const [base, f] of flags) + if (f.hasThinking && f.hasNonThinking) + ambiguous.add(base); + return ambiguous; +} +export function modelInfoToConfig(mi, options = {}) { + const contextTier = options.contextTier ?? "base"; + const variants = options.variants ?? variantsForTier(mi, contextTier); + let name = baseName(mi); + if (options.thinkingSuffix) + name += " Thinking"; + if (options.fast) + name += " Fast"; + if (contextTier === "long") + name += " 1M"; + // OpenCode's context limit is static per model entry, while Cursor's context + // tier is a variant parameter. Long-context choices are therefore emitted as + // separate OpenCode entries by modelsToConfig. + const documentedContext = getDocumentedCursorModelContext(mi.id); + // Cursor Auto (`default`) has no static maxContext on the model list, but + // live checkpoints report 256k for the base tier — the previous 200k + // fallback made the host context meter disagree with Cursor. + const fallbackContext = mi.id === "default" ? 256_000 : 200_000; + const context = contextTier === "long" + ? (mi.maxContextForMaxMode ?? documentedContext?.maxContextForMaxMode ?? 1_000_000) + : (mi.maxContext ?? documentedContext?.maxContext ?? fallbackContext); + // OpenCode's overflow/compaction/UI use limit.context; generation and + // thinking budgets use limit.output. models.dev 1M peers advertise + // 64k–128k output — a tiny cap makes long-context sessions feel broken + // even when the 1M input window is correct. + const output = contextTier === "long" ? 128_000 : 32_000; + const supportsImages = resolveCursorModelSupportsImages(mi.id, mi.supportsImages); + const config = { + name, + attachment: supportsImages, + reasoning: mi.supportsThinking ?? false, + tool_call: mi.supportsAgent ?? true, + temperature: false, + modalities: { + input: supportsImages ? ["text", "image"] : ["text"], + output: ["text"], + }, + limit: { + context, + output, + }, + }; + const variantConfig = modelInfoVariants(mi, variants); + if (variantConfig) + config.variants = variantConfig; + if (contextTier === "long" || options.fast) { + const defaultVariant = contextTier === "long" + ? (variants.find((v) => v.isDefaultMax) ?? variants[0]) + : (variants.find((v) => v.isDefaultNonMax) ?? variants[0]); + if (defaultVariant) { + config.options = { + [CURSOR_WIRE_MODEL_ID_KEY]: mi.id, + [CURSOR_VARIANT_PARAMETERS_KEY]: defaultVariant.parameterValues.map((p) => ({ ...p })), + }; + } + } + return config; +} +function speedGroups(variants, splitFast) { + if (!splitFast) + return variants.length > 0 ? [{ fast: false, variants }] : []; + const slow = variants.filter((variant) => !isFastVariant(variant)); + const fast = variants.filter((variant) => isFastVariant(variant)); + const groups = []; + if (slow.length > 0) + groups.push({ fast: false, variants: slow }); + if (fast.length > 0) + groups.push({ fast: true, variants: fast }); + return groups; +} +function uniqueCatalogId(usedIds, modelId, suffix) { + let id = `${modelId}${suffix}`; + if (!usedIds.has(id)) { + usedIds.add(id); + return id; + } + let n = 2; + while (true) { + const candidate = suffix === "-fast" + ? `${modelId}-fast-${n}` + : suffix === "-1m-fast" + ? `${modelId}-1m-${n}-fast` + : `${modelId}-1m-${n}`; + if (!usedIds.has(candidate)) { + usedIds.add(candidate); + return candidate; + } + n++; + } +} +export function modelsToConfig(models) { + const ambiguous = thinkingSuffixBaseNames(models); + const out = {}; + const usedIds = new Set(models.map((m) => m.id)); + for (const m of models) { + const thinkingSuffix = !!m.supportsThinking && ambiguous.has(baseName(m)); + const splitFast = hasCursorFastPricing(m.id); + const groups = []; + for (const contextTier of ["base", "long"]) { + const suffixFor = (fast) => contextTier === "long" ? (fast ? "-1m-fast" : "-1m") : (fast ? "-fast" : ""); + for (const group of speedGroups(variantsForTier(m, contextTier), splitFast)) { + groups.push({ + contextTier, + fast: group.fast, + variants: group.variants, + suffix: suffixFor(group.fast), + }); + } + } + if (groups.length === 0) { + out[m.id] = applyCursorModelCost(m.id, modelInfoToConfig(m, { thinkingSuffix, contextTier: "base" })); + continue; + } + const primaryIndex = groups.findIndex((group) => group.suffix === ""); + const primary = primaryIndex >= 0 ? primaryIndex : 0; + for (const [index, group] of groups.entries()) { + const catalogId = index === primary + ? m.id + : uniqueCatalogId(usedIds, m.id, group.suffix || "-fast"); + const pricingId = group.fast ? `${m.id}-fast` : (group.contextTier === "long" ? `${m.id}-1m` : m.id); + const config = modelInfoToConfig(m, { + thinkingSuffix, + contextTier: group.contextTier, + variants: group.variants, + fast: group.fast, + }); + if (catalogId !== m.id && !config.options) { + const defaultVariant = (group.contextTier === "long" + ? group.variants.find((v) => v.isDefaultMax) + : group.variants.find((v) => v.isDefaultNonMax)) ?? group.variants[0]; + if (defaultVariant) { + config.options = { + [CURSOR_WIRE_MODEL_ID_KEY]: m.id, + [CURSOR_VARIANT_PARAMETERS_KEY]: defaultVariant.parameterValues.map((p) => ({ ...p })), + }; + } + } + out[catalogId] = applyCursorModelCost(pricingId, config); + } + } + return out; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/model-metadata.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/model-metadata.d.ts new file mode 100644 index 00000000..457b15f5 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/model-metadata.d.ts @@ -0,0 +1,10 @@ +export type CursorModelContext = { + maxContext?: number; + maxContextForMaxMode?: number; +}; +export type CursorModelCapabilities = { + supportsImages: boolean; +}; +export declare function getDocumentedCursorModelContext(modelId: string): CursorModelContext | undefined; +export declare function getDocumentedCursorModelCapabilities(modelId: string): CursorModelCapabilities | undefined; +export declare function resolveCursorModelSupportsImages(modelId: string, availableModelsValue?: boolean): boolean; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/model-metadata.js b/references/upstream-plugins/cursor-opencode-provider/dist/model-metadata.js new file mode 100644 index 00000000..d0fe97b6 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/model-metadata.js @@ -0,0 +1,12 @@ +import { CURSOR_MODEL_CAPABILITIES, CURSOR_MODEL_CONTEXTS } from "./pricing-data.js"; +export function getDocumentedCursorModelContext(modelId) { + const context = CURSOR_MODEL_CONTEXTS[modelId]; + return context ? { ...context } : undefined; +} +export function getDocumentedCursorModelCapabilities(modelId) { + const capabilities = CURSOR_MODEL_CAPABILITIES[modelId]; + return capabilities ? { ...capabilities } : undefined; +} +export function resolveCursorModelSupportsImages(modelId, availableModelsValue) { + return availableModelsValue ?? getDocumentedCursorModelCapabilities(modelId)?.supportsImages ?? false; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/models.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/models.d.ts new file mode 100644 index 00000000..145feb43 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/models.d.ts @@ -0,0 +1,103 @@ +export type ModelParameterValue = { + id: string; + value: string; +}; +/** Dedicated OpenCode provider-option key for a selected Cursor variant. */ +export declare const CURSOR_VARIANT_PARAMETERS_KEY = "cursorVariantParameters"; +/** Real Cursor model id used when an OpenCode entry has a synthetic id. */ +export declare const CURSOR_WIRE_MODEL_ID_KEY = "cursorModelId"; +export type ModelVariant = { + key: string; + parameterValues: ModelParameterValue[]; + displayName: string; + isDefaultNonMax: boolean; + isDefaultMax: boolean; +}; +export type ModelInfo = { + id: string; + displayName?: string; + family?: string; + supportsThinking?: boolean; + supportsAgent?: boolean; + supportsImages?: boolean; + maxContext?: number; + /** Context window when max-mode is on (proto field 16). */ + maxContextForMaxMode?: number; + supportsMaxMode?: boolean; + variants: ModelVariant[]; +}; +export type ModelCache = { + models: ModelInfo[]; + fetchedAt: number; + /** Absent or mismatched → treat cache as stale (forces AvailableModels refetch). */ + schemaVersion?: number; +}; +export declare class CursorVariantSelectionError extends Error { + constructor(message: string); +} +export declare function normalizeModelParameterValues(value: unknown): ModelParameterValue[] | null; +export declare function normalizeModelCache(value: unknown): ModelCache | null; +/** + * Read only the dedicated variant payload generated by the plugin. OpenCode + * merges model, agent, and variant options into one providerOptions namespace; + * treating that whole object as Cursor parameters would leak unrelated options + * onto the wire. + */ +export declare function extractCursorVariantParameters(providerOptions: Record | undefined): ModelParameterValue[] | undefined; +export declare function resolveCursorWireModelId(providerOptions: Record | undefined, fallback: string): string; +/** True when resolved params select the long-context (1m) tier. */ +export declare function paramsImplyMaxMode(params: ModelParameterValue[]): boolean; +/** + * Resolve the request-level max_mode bit. An explicit variant pick owns the + * context tier; otherwise a standalone maxMode hint may select max mode. + */ +export declare function resolveVariantMaxMode(params: ModelParameterValue[], opts?: { + picked?: ModelParameterValue[]; + maxMode?: boolean; +}): boolean; +export declare function parseCursorContextLimit(value: unknown): number | undefined; +export declare function isCacheFresh(cache: ModelCache, ttlMs?: number): boolean; +export declare function cacheFilePath(cacheDir: string): string; +export declare function readCache(cacheDir: string): Promise; +export declare function writeCache(cacheDir: string, cache: ModelCache): Promise; +export declare function mapAvailableModelsResponse(raw: Record): ModelInfo[]; +/** + * Resolve the parameter values to send in `requested_model.parameters` for a + * given model + the variant opencode selected. Each variant already carries + * the full `{id,value}` set (per-model vocabulary — effort, fast, thinking, + * context, …), so we pick the matching variant and return its parameters + * verbatim — the client never constructs these by hand. + * + * `picked` is the user-selected variant paramMap (opencode sends it under + * `providerOptions.cursor`); when present it wins over effort/maxMode hints + * so every param the user chose (context, fast, …) is forwarded to Cursor. + * Hints (`reasoningEffort`, `maxMode`) live beside the dedicated picked array, + * so the picked values are already isolated and can be matched verbatim. + */ +export declare function resolveVariantParameters(model: ModelInfo | undefined, opts?: { + reasoningEffort?: string; + maxMode?: boolean; + picked?: ModelParameterValue[]; +}): ModelParameterValue[]; +export declare function fetchModels(token: string, options?: { + baseURL?: string; + headers?: Record; + timeoutMs?: number; +}): Promise; +export declare function refreshModelCache(cacheDir: string, fetcher: () => Promise, options?: { + forceAfterInflight?: boolean; +}): Promise; +export declare function discoverModels(token: string, cacheDir: string, options?: { + baseURL?: string; + headers?: Record; + timeoutMs?: number; + /** + * Bypass even a fresh cache and require an authoritative response. + * + * Model availability is account-scoped. Callers use this after a + * credential switch so a fresh cache produced by the previous account is + * never rebound to the new connection. A forced refresh rejects on fetch + * failure; the caller can keep the already-published inventory unchanged. + */ + forceRefresh?: boolean; +}): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/models.js b/references/upstream-plugins/cursor-opencode-provider/dist/models.js new file mode 100644 index 00000000..c496f8e9 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/models.js @@ -0,0 +1,455 @@ +import { MODEL_CACHE_FILE, MODEL_CACHE_SCHEMA_VERSION, MODEL_CACHE_TTL_MS } from "./shared.js"; +import { unaryAvailableModels } from "./transport/connect.js"; +import { buildRequestedModelParams } from "./protocol/thinking.js"; +import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; +import path from "node:path"; +/** Dedicated OpenCode provider-option key for a selected Cursor variant. */ +export const CURSOR_VARIANT_PARAMETERS_KEY = "cursorVariantParameters"; +/** Real Cursor model id used when an OpenCode entry has a synthetic id. */ +export const CURSOR_WIRE_MODEL_ID_KEY = "cursorModelId"; +export class CursorVariantSelectionError extends Error { + constructor(message) { + super(`Cursor variant selection ${message}`); + this.name = "CursorVariantSelectionError"; + } +} +function isPlainRecord(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) + return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} +function isNonEmptyString(value) { + return typeof value === "string" && value.trim().length > 0; +} +function optionalString(record, names) { + for (const name of names) { + if (!Object.hasOwn(record, name)) + continue; + if (record[name] === undefined) + return undefined; + return typeof record[name] === "string" ? record[name] : null; + } + return undefined; +} +function optionalBoolean(record, names) { + for (const name of names) { + if (!Object.hasOwn(record, name)) + continue; + if (record[name] === undefined) + return undefined; + return typeof record[name] === "boolean" ? record[name] : null; + } + return undefined; +} +function optionalPositiveNumber(record, names) { + for (const name of names) { + if (!Object.hasOwn(record, name)) + continue; + const value = record[name]; + if (value === undefined) + return undefined; + return typeof value === "number" && Number.isFinite(value) && value > 0 + ? value + : null; + } + return undefined; +} +export function normalizeModelParameterValues(value) { + if (!Array.isArray(value)) + return null; + const parameters = []; + for (const parameter of value) { + if (!isPlainRecord(parameter)) + return null; + if (!isNonEmptyString(parameter.id) || typeof parameter.value !== "string") + return null; + parameters.push({ id: parameter.id, value: parameter.value }); + } + return parameters; +} +function normalizeVariant(value, fallbackName) { + if (!isPlainRecord(value)) + return null; + const parameters = normalizeModelParameterValues(value.parameterValues ?? value.parameter_values ?? []); + if (!parameters) + return null; + const key = optionalString(value, ["key"]); + const displayName = optionalString(value, ["displayName", "display_name"]); + const isDefaultNonMax = optionalBoolean(value, ["isDefaultNonMax", "isDefaultNonMaxConfig", "is_default_non_max_config"]); + const isDefaultMax = optionalBoolean(value, ["isDefaultMax", "isDefaultMaxConfig", "is_default_max_config"]); + if (key === null || + displayName === null || + isDefaultNonMax === null || + isDefaultMax === null) { + return null; + } + if ((key !== undefined && !isNonEmptyString(key)) || + (displayName !== undefined && !isNonEmptyString(displayName))) { + return null; + } + return { + key: key ?? fallbackName, + displayName: displayName ?? fallbackName, + parameterValues: parameters, + isDefaultNonMax: isDefaultNonMax ?? false, + isDefaultMax: isDefaultMax ?? false, + }; +} +function normalizeModelInfo(value) { + if (!isPlainRecord(value) || !isNonEmptyString(value.id)) + return null; + const rawVariants = value.variants ?? []; + if (!Array.isArray(rawVariants)) + return null; + const variants = rawVariants.map((variant) => normalizeVariant(variant, value.id)); + if (variants.some((variant) => variant === null)) + return null; + const displayName = optionalString(value, ["displayName", "display_name"]); + const family = optionalString(value, ["family"]); + const supportsThinking = optionalBoolean(value, ["supportsThinking", "supports_thinking"]); + const supportsAgent = optionalBoolean(value, ["supportsAgent", "supports_agent"]); + const supportsImages = optionalBoolean(value, ["supportsImages", "supports_images"]); + const supportsMaxMode = optionalBoolean(value, ["supportsMaxMode", "supports_max_mode"]); + const maxContext = optionalPositiveNumber(value, ["maxContext", "contextTokenLimit", "context_token_limit"]); + const maxContextForMaxMode = optionalPositiveNumber(value, ["maxContextForMaxMode", "contextTokenLimitForMaxMode", "context_token_limit_for_max_mode"]); + if (displayName === null || + family === null || + supportsThinking === null || + supportsAgent === null || + supportsImages === null || + supportsMaxMode === null || + maxContext === null || + maxContextForMaxMode === null) { + return null; + } + return { + id: value.id, + ...(displayName === undefined ? {} : { displayName }), + ...(family === undefined ? {} : { family }), + ...(supportsThinking === undefined ? {} : { supportsThinking }), + ...(supportsAgent === undefined ? {} : { supportsAgent }), + ...(supportsImages === undefined ? {} : { supportsImages }), + ...(maxContext === undefined ? {} : { maxContext }), + ...(maxContextForMaxMode === undefined ? {} : { maxContextForMaxMode }), + ...(supportsMaxMode === undefined ? {} : { supportsMaxMode }), + variants: variants, + }; +} +export function normalizeModelCache(value) { + if (!isPlainRecord(value) || !Array.isArray(value.models)) + return null; + if (typeof value.fetchedAt !== "number" || !Number.isFinite(value.fetchedAt)) + return null; + if (value.schemaVersion !== undefined && + (!Number.isSafeInteger(value.schemaVersion) || value.schemaVersion < 0)) { + return null; + } + const models = value.models.map(normalizeModelInfo); + if (models.some((model) => model === null)) + return null; + return { + models: models, + fetchedAt: value.fetchedAt, + ...(value.schemaVersion === undefined + ? {} + : { schemaVersion: value.schemaVersion }), + }; +} +/** + * Read only the dedicated variant payload generated by the plugin. OpenCode + * merges model, agent, and variant options into one providerOptions namespace; + * treating that whole object as Cursor parameters would leak unrelated options + * onto the wire. + */ +export function extractCursorVariantParameters(providerOptions) { + if (!providerOptions || !Object.hasOwn(providerOptions, CURSOR_VARIANT_PARAMETERS_KEY)) { + return undefined; + } + const params = normalizeModelParameterValues(providerOptions[CURSOR_VARIANT_PARAMETERS_KEY]); + if (!params) { + throw new CursorVariantSelectionError("is malformed: cursorVariantParameters must be a parameter array"); + } + return params; +} +export function resolveCursorWireModelId(providerOptions, fallback) { + const value = providerOptions?.[CURSOR_WIRE_MODEL_ID_KEY]; + return typeof value === "string" && value.trim() ? value : fallback; +} +/** True when resolved params select the long-context (1m) tier. */ +export function paramsImplyMaxMode(params) { + return params.some((parameter) => parameter.id === "context" && + parseCursorContextLimit(parameter.value) === 1_000_000); +} +/** + * Resolve the request-level max_mode bit. An explicit variant pick owns the + * context tier; otherwise a standalone maxMode hint may select max mode. + */ +export function resolveVariantMaxMode(params, opts = {}) { + return paramsImplyMaxMode(params) || (opts.picked === undefined && opts.maxMode === true); +} +// Cursor encodes a model's context window as a variant parameter `id: "context"` +// whose value is a tier string — the same 200k / 272k / 300k / 1m the IDE's +// picker shows. The base tier rides on the default non-max variant; the 1M +// tier (when the model supports max mode) rides on the default max variant. +// `context_token_limit` (#15) / `context_token_limit_for_max_mode` (#16) are +// also defined on AvailableModel but the server often leaves them empty — the +// variant param is the primary source (request flags may still populate them). +export function parseCursorContextLimit(value) { + if (typeof value !== "string") + return undefined; + const text = value.trim(); + const match = /^(\d+(?:\.\d+)?)\s*([km])$/i.exec(text); + if (match) { + const multiplier = match[2].toLowerCase() === "k" ? 1_000 : 1_000_000; + const limit = Number(match[1]) * multiplier; + return Number.isSafeInteger(limit) && limit > 0 ? limit : undefined; + } + const numeric = Number(text); + return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : undefined; +} +function variantContextTokens(v) { + const raw = v?.parameterValues.find((p) => p.id === "context")?.value; + return parseCursorContextLimit(raw); +} +function isLongContextVariant(v) { + return variantContextTokens(v) === 1_000_000; +} +function positiveNumber(value) { + return typeof value === "number" && Number.isFinite(value) && value > 0 + ? value + : undefined; +} +// ── Cache helpers ── +export function isCacheFresh(cache, ttlMs = MODEL_CACHE_TTL_MS) { + if (cache.schemaVersion !== MODEL_CACHE_SCHEMA_VERSION) + return false; + return Date.now() - cache.fetchedAt < ttlMs; +} +export function cacheFilePath(cacheDir) { + return path.join(cacheDir, MODEL_CACHE_FILE); +} +export async function readCache(cacheDir) { + const filePath = cacheFilePath(cacheDir); + try { + const data = await readFile(filePath, "utf-8"); + return normalizeModelCache(JSON.parse(data)); + } + catch { + return null; + } +} +export async function writeCache(cacheDir, cache) { + const normalized = normalizeModelCache(cache); + if (!normalized) + throw new Error("Refusing to write an invalid Cursor model cache"); + const filePath = cacheFilePath(cacheDir); + const directory = path.dirname(filePath); + const tempPath = path.join(directory, `.${MODEL_CACHE_FILE}.${process.pid}.${crypto.randomUUID()}.tmp`); + await mkdir(directory, { recursive: true }); + try { + await writeFile(tempPath, JSON.stringify(normalized, null, 2), "utf-8"); + await rename(tempPath, filePath); + } + finally { + await unlink(tempPath).catch(() => { }); + } +} +// ── Map live API response to ModelInfo[] ── +function apiBoolean(record, names) { + const value = optionalBoolean(record, names); + if (value === null) + throw new Error(`AvailableModels returned a non-boolean ${names[0]}`); + return value ?? false; +} +export function mapAvailableModelsResponse(raw) { + const entries = raw.models ?? []; + const models = []; + for (const entry of entries) { + if (!isPlainRecord(entry)) + continue; + const e = entry; + const name = e.name; + if (!isNonEmptyString(name)) + continue; + const variants = []; + const rawVariants = e.variants ?? []; + if (!Array.isArray(rawVariants)) { + throw new Error(`AvailableModels returned invalid variants for ${name}`); + } + for (const v of rawVariants) { + if (!isPlainRecord(v)) { + throw new Error(`AvailableModels returned an invalid variant for ${name}`); + } + const rawParams = (v.parameterValues ?? v.parameter_values ?? []); + const parameterValues = normalizeModelParameterValues(rawParams); + if (!parameterValues) { + throw new Error(`AvailableModels returned invalid parameter values for ${name}`); + } + variants.push({ + key: name, + parameterValues, + displayName: (v.displayName ?? v.display_name ?? name), + isDefaultNonMax: apiBoolean(v, ["isDefaultNonMaxConfig", "is_default_non_max_config"]), + isDefaultMax: apiBoolean(v, ["isDefaultMaxConfig", "is_default_max_config"]), + }); + } + // Derive the context window from the variant `context` param (primary + // source). Base tier ← default non-max variant; max tier ← default max + // variant. Fall back to the (often empty) proto fields, then undefined. + // Prefer a non-1m context when isDefaultNonMax is missing so a leading + // max-mode variant cannot inflate the base window. + const variantBaseContext = variantContextTokens(variants.find((v) => v.isDefaultNonMax)) ?? + variantContextTokens(variants.find((v) => variantContextTokens(v) !== 1_000_000)) ?? + variantContextTokens(variants.find((v) => v.parameterValues.some((p) => p.id === "context"))); + const variantMaxContext = variantContextTokens(variants.find((v) => v.isDefaultMax)); + const supportsImages = optionalBoolean(e, ["supportsImages", "supports_images"]); + if (supportsImages === null) { + throw new Error(`AvailableModels returned invalid supportsImages for ${name}`); + } + models.push({ + id: name, + displayName: (e.clientDisplayName ?? e.client_display_name ?? name), + supportsThinking: apiBoolean(e, ["supportsThinking", "supports_thinking"]), + supportsAgent: apiBoolean(e, ["supportsAgent", "supports_agent"]), + ...(supportsImages === undefined ? {} : { supportsImages }), + maxContext: variantBaseContext ?? positiveNumber(e.contextTokenLimit ?? e.context_token_limit), + maxContextForMaxMode: variantMaxContext ?? + positiveNumber(e.contextTokenLimitForMaxMode ?? e.context_token_limit_for_max_mode), + supportsMaxMode: apiBoolean(e, ["supportsMaxMode", "supports_max_mode"]), + variants, + }); + } + return models; +} +// ── Variant resolution ── +/** + * Resolve the parameter values to send in `requested_model.parameters` for a + * given model + the variant opencode selected. Each variant already carries + * the full `{id,value}` set (per-model vocabulary — effort, fast, thinking, + * context, …), so we pick the matching variant and return its parameters + * verbatim — the client never constructs these by hand. + * + * `picked` is the user-selected variant paramMap (opencode sends it under + * `providerOptions.cursor`); when present it wins over effort/maxMode hints + * so every param the user chose (context, fast, …) is forwarded to Cursor. + * Hints (`reasoningEffort`, `maxMode`) live beside the dedicated picked array, + * so the picked values are already isolated and can be matched verbatim. + */ +export function resolveVariantParameters(model, opts = {}) { + const picked = opts.picked?.map((p) => ({ ...p })); + if (!model || model.variants.length === 0) { + if (picked !== undefined) { + throw new CursorVariantSelectionError(`is stale: model ${JSON.stringify(model?.id ?? "unknown")} has no cached variants`); + } + return opts.reasoningEffort ? [{ id: "effort", value: opts.reasoningEffort }] : []; + } + const effortOf = (v) => v.parameterValues.find((p) => p.id === "effort" || p.id === "reasoning")?.value; + const isFast = (v) => v.parameterValues.find((p) => p.id === "fast")?.value === "true"; + const isMaxVariant = (v) => v.isDefaultMax || isLongContextVariant(v); + const wantMax = opts.maxMode ?? false; + // Non-fast pool for hint-based resolution only. Exact `picked` matching + // searches all variants so Fast selections are not silently dropped. + const pool = model.variants.filter((v) => !isFast(v)); + const scoped = pool.length > 0 ? pool : model.variants; + // 1. Variant explicitly picked by opencode (verbatim — preserves context, fast, …). + if (picked !== undefined) { + const pickedById = new Map(picked.map((parameter) => [parameter.id, parameter.value])); + const exact = model.variants.find((v) => v.parameterValues.length === picked.length && + pickedById.size === picked.length && + v.parameterValues.every((parameter) => pickedById.get(parameter.id) === parameter.value)); + if (exact) { + return buildRequestedModelParams(exact.parameterValues); + } + throw new CursorVariantSelectionError(`is stale for model ${JSON.stringify(model.id)}: its exact parameter tuple is unavailable`); + } + // When maxMode is requested, prefer max-tier variants for effort matching + // so "high + max" lands on 1m high rather than the first 300k high. + const maxScoped = wantMax ? scoped.filter(isMaxVariant) : []; + const effortPool = maxScoped.length > 0 ? maxScoped : scoped; + // 2. Effort hint (e.g. user passed reasoningEffort through the CLI). + if (opts.reasoningEffort) { + const match = effortPool.find((v) => effortOf(v) === opts.reasoningEffort); + if (match) { + return buildRequestedModelParams(match.parameterValues, { + reasoningEffort: opts.reasoningEffort, + maxMode: wantMax, + }); + } + } + // 3. Max-mode hint → prefer the default max variant (1m context). + if (wantMax) { + const max = scoped.find((v) => v.isDefaultMax) ?? scoped.find(isLongContextVariant); + if (max) + return buildRequestedModelParams(max.parameterValues, { maxMode: true }); + } + // 4. Default non-max variant. + const byDefault = scoped.find((v) => v.isDefaultNonMax) ?? scoped[0]; + return buildRequestedModelParams(byDefault.parameterValues, { + reasoningEffort: opts.reasoningEffort, + maxMode: false, + }); +} +// ── Fetch + cache orchestration ── +export async function fetchModels(token, options = {}) { + const raw = await unaryAvailableModels(token, options); + return mapAvailableModelsResponse(raw); +} +const refreshesByDirectory = new Map(); +export async function refreshModelCache(cacheDir, fetcher, options = {}) { + const key = path.resolve(cacheDir); + const existing = refreshesByDirectory.get(key); + if (existing) { + if (!options.forceAfterInflight) + return existing; + // An account switch must not join a background refresh authenticated as + // the previous account. Wait for that write to finish, then run this + // caller's fetch so its account inventory is authoritative on disk. + await existing.catch(() => { }); + return refreshModelCache(cacheDir, fetcher, options); + } + const refresh = (async () => { + const models = await fetcher(); + await writeCache(cacheDir, { + models, + fetchedAt: Date.now(), + schemaVersion: MODEL_CACHE_SCHEMA_VERSION, + }); + return models; + })(); + refreshesByDirectory.set(key, refresh); + try { + return await refresh; + } + finally { + if (refreshesByDirectory.get(key) === refresh) + refreshesByDirectory.delete(key); + } +} +export async function discoverModels(token, cacheDir, options = {}) { + const { forceRefresh = false, ...fetchOptions } = options; + const cached = await readCache(cacheDir); + const refresh = () => refreshModelCache(cacheDir, () => fetchModels(token, fetchOptions), { forceAfterInflight: forceRefresh }); + // Cache is fresh → return it; refresh in background + if (!forceRefresh && cached && isCacheFresh(cached)) { + // Background refresh (fire and forget) + void refresh() + .catch(() => { + /* background refresh failure is non-fatal */ + }); + return cached.models; + } + // Cache exists but expired → try fetch, serve stale on failure + if (cached) { + try { + return await refresh(); + } + catch (error) { + if (forceRefresh) + throw error; + return cached.models; + } + } + // No cache → must fetch + return refresh(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/catalog.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/catalog.d.ts new file mode 100644 index 00000000..932e38f0 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/catalog.d.ts @@ -0,0 +1,68 @@ +import { type ModelInfo } from "../models.js"; +import { type OpenCode2ModelCost } from "../pricing.js"; +import type { ConnectionInfo, ModelVariantInfo, ProviderEditor } from "./types.js"; +/** + * In-memory provider registration for the OpenCode 2.0 plugin — the replacement + * for the classic plugin's `config` hook. + * + * Model naming, thinking suffixes, and long-context tiering are NOT reimplemented + * here: we run the shared `modelsToConfig` and translate its output into the 2.0 + * `Model.Info` shape, so every surface exposes an identical model list. + */ +/** Integration id owning Cursor credentials. Matches the provider id. */ +export declare const CURSOR_INTEGRATION_ID = "cursor"; +/** + * `aisdk:` selects OpenCode 2.0's AI SDK path, which is what surfaces the + * `aisdk.hook("sdk")` / `("language")` extension points we supply the provider + * through. The suffix is this package's npm name so the host's built-in + * fallback can still resolve it if our own hook is ever bypassed — that + * fallback runs `npm.add(pkg)` against the *published* registry into + * `/packages//node_modules/`, ignoring any local + * `file://` plugin path this process was loaded from. + * + * `CURSOR_OPENCODE2_DEV_ENTRY` overrides the suffix with an `aisdk:file://…` + * spec instead, pointed at a local built entry file (e.g. `dist/index.js`, + * which exports `createCursor`). The host's fallback recognizes `file://` + * specs and imports them directly, skipping `npm.add` — the only way to + * exercise a local build through that fallback path short of publishing. + * Unset in production; only meant for local `opencode2 run` testing. + */ +export declare const CURSOR_AISDK_PACKAGE: string; +/** + * Plain-object `Model.Info` equivalent used by `ctx.provider.transform`. + */ +export type CatalogModelInfo = { + id: string; + modelID: string; + providerID: string; + name: string; + capabilities: { + tools: boolean; + input: string[]; + output: string[]; + }; + limit: { + context: number; + output: number; + }; + variants: ModelVariantInfo[]; + status: "active"; + enabled: true; + time: { + released: number; + }; + cost: OpenCode2ModelCost[]; + settings?: Record; +}; +/** Translate one `modelsToConfig` entry into the 2.0 `Model.Info` shape. */ +export declare function modelConfigEntryToInfo(id: string, entry: Record): CatalogModelInfo; +/** Full model map for the in-memory provider inventory. */ +export declare function modelsToCatalogModelMap(models: ModelInfo[]): Record; +/** + * Publish discovered Cursor models into the live provider inventory. + * + * Skip while empty (keeps the last successful inventory through a no-op + * transform on first register), replace the definition with `editor.add`, + * then `ctx.provider.reload()`. + */ +export declare function applyCursorProviderInventory(editor: ProviderEditor, models: ModelInfo[], sourceConnection?: ConnectionInfo): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/catalog.js b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/catalog.js new file mode 100644 index 00000000..05ad461c --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/catalog.js @@ -0,0 +1,106 @@ +import { pathToFileURL } from "node:url"; +import { CURSOR_PROVIDER_ID } from "../shared.js"; +import { CURSOR_WIRE_MODEL_ID_KEY } from "../models.js"; +import { modelsToConfig } from "../model-config.js"; +import { toOpenCode2Costs } from "../pricing.js"; +/** + * In-memory provider registration for the OpenCode 2.0 plugin — the replacement + * for the classic plugin's `config` hook. + * + * Model naming, thinking suffixes, and long-context tiering are NOT reimplemented + * here: we run the shared `modelsToConfig` and translate its output into the 2.0 + * `Model.Info` shape, so every surface exposes an identical model list. + */ +/** Integration id owning Cursor credentials. Matches the provider id. */ +export const CURSOR_INTEGRATION_ID = CURSOR_PROVIDER_ID; +/** + * `aisdk:` selects OpenCode 2.0's AI SDK path, which is what surfaces the + * `aisdk.hook("sdk")` / `("language")` extension points we supply the provider + * through. The suffix is this package's npm name so the host's built-in + * fallback can still resolve it if our own hook is ever bypassed — that + * fallback runs `npm.add(pkg)` against the *published* registry into + * `/packages//node_modules/`, ignoring any local + * `file://` plugin path this process was loaded from. + * + * `CURSOR_OPENCODE2_DEV_ENTRY` overrides the suffix with an `aisdk:file://…` + * spec instead, pointed at a local built entry file (e.g. `dist/index.js`, + * which exports `createCursor`). The host's fallback recognizes `file://` + * specs and imports them directly, skipping `npm.add` — the only way to + * exercise a local build through that fallback path short of publishing. + * Unset in production; only meant for local `opencode2 run` testing. + */ +export const CURSOR_AISDK_PACKAGE = process.env.CURSOR_OPENCODE2_DEV_ENTRY + ? `aisdk:${pathToFileURL(process.env.CURSOR_OPENCODE2_DEV_ENTRY).href}` + : "aisdk:cursor-opencode-provider"; +/** Translate one `modelsToConfig` entry into the 2.0 `Model.Info` shape. */ +export function modelConfigEntryToInfo(id, entry) { + const options = entry.options; + // Long-context and Fast entries get synthetic OpenCode ids (`-1m`, + // `-fast`, `-1m-fast`) while still addressing the same Cursor model + // on the wire. V1 smuggled that through provider options; 2.0 has a + // first-class `modelID` for exactly this. + const wireId = typeof options?.[CURSOR_WIRE_MODEL_ID_KEY] === "string" + ? options[CURSOR_WIRE_MODEL_ID_KEY] + : id; + const variants = Object.entries((entry.variants ?? {})).map(([variantId, settings]) => ({ id: variantId, settings: { ...settings } })); + const inputModalities = Array.isArray(entry.modalities?.input) + ? entry.modalities.input.filter((modality) => typeof modality === "string") + : ["text"]; + const outputModalities = Array.isArray(entry.modalities?.output) + ? entry.modalities.output.filter((modality) => typeof modality === "string") + : ["text"]; + const info = { + id, + modelID: wireId, + providerID: CURSOR_PROVIDER_ID, + name: entry.name ?? id, + capabilities: { + tools: entry.tool_call !== false, + input: inputModalities, + output: outputModalities, + }, + limit: { + context: entry.limit?.context ?? 200_000, + output: entry.limit?.output ?? 8192, + }, + variants, + status: "active", + enabled: true, + time: { released: 0 }, + cost: toOpenCode2Costs(entry.cost), + }; + if (options) + info.settings = { ...options }; + return info; +} +/** Full model map for the in-memory provider inventory. */ +export function modelsToCatalogModelMap(models) { + const config = modelsToConfig(models); + const out = {}; + for (const [id, entry] of Object.entries(config)) { + out[id] = modelConfigEntryToInfo(id, entry); + } + return out; +} +/** + * Publish discovered Cursor models into the live provider inventory. + * + * Skip while empty (keeps the last successful inventory through a no-op + * transform on first register), replace the definition with `editor.add`, + * then `ctx.provider.reload()`. + */ +export function applyCursorProviderInventory(editor, models, sourceConnection) { + if (models.length === 0) + return; + editor.add({ + info: { + id: CURSOR_PROVIDER_ID, + name: "Cursor", + activation: "enabled", + package: CURSOR_AISDK_PACKAGE, + integrationID: CURSOR_INTEGRATION_ID, + }, + models: Object.values(modelsToCatalogModelMap(models)), + ...(sourceConnection ? { sourceConnection } : {}), + }); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/integration.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/integration.d.ts new file mode 100644 index 00000000..e47ee5f8 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/integration.d.ts @@ -0,0 +1,23 @@ +import type { CredentialValue, IntegrationDomain, IntegrationDraft } from "./types.js"; +/** + * Integration registration for the OpenCode 2.0 plugin — the replacement for the + * classic plugin's `auth` hook (`methods` + `loader`). + * + * Unlike the 1.18 v2 API (where OAuth registration is Effect-valued), 2.0 takes + * plain Promises, so the PKCE flow from `src/auth.ts` ports across unchanged. + */ +export declare const CURSOR_OAUTH_METHOD_ID = "oauth"; +/** Env vars that can supply a Cursor API key without running /connect. */ +export declare const CURSOR_ENV_NAMES: string[]; +/** Register the Cursor integration and its three connection methods. */ +export declare function applyCursorIntegration(draft: IntegrationDraft): void; +/** + * Turn a stored credential into a Cursor access token. + * + * OAuth credentials already hold a JWT (the host refreshes them via `refresh` + * above). A `key` credential is the raw `crsr_…` API key, which Cursor requires us + * to exchange for a short-lived JWT — mirroring the classic plugin's behavior. + */ +export declare function accessTokenFromCredential(credential: CredentialValue | undefined): Promise; +/** Resolve the active Cursor connection into an access token, if any. */ +export declare function resolveCursorAccessToken(integration: IntegrationDomain): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/integration.js b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/integration.js new file mode 100644 index 00000000..83be3764 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/integration.js @@ -0,0 +1,124 @@ +import { CURSOR_WEBSITE_HOST } from "../shared.js"; +import { cursorApiBaseURL } from "../plugin-core.js"; +import { buildLoginUrl, decodeJwtExpiryMs, exchangeApiKey, generatePkceChallenge, generatePkceParams, isExpiringSoon, pollForTokens, refreshAccessToken, } from "../auth.js"; +import { CURSOR_INTEGRATION_ID } from "./catalog.js"; +/** + * Integration registration for the OpenCode 2.0 plugin — the replacement for the + * classic plugin's `auth` hook (`methods` + `loader`). + * + * Unlike the 1.18 v2 API (where OAuth registration is Effect-valued), 2.0 takes + * plain Promises, so the PKCE flow from `src/auth.ts` ports across unchanged. + */ +export const CURSOR_OAUTH_METHOD_ID = "oauth"; +/** Env vars that can supply a Cursor API key without running /connect. */ +export const CURSOR_ENV_NAMES = ["CURSOR_API_KEY"]; +function websiteURL() { + return process.env.CURSOR_WEBSITE_URL ?? `https://${CURSOR_WEBSITE_HOST}`; +} +/** Browser (PKCE) login: open URL, then poll until Cursor hands back tokens. */ +async function authorizeOAuth() { + const params = generatePkceParams(); + const challenge = await generatePkceChallenge(params.verifier); + const apiBaseURL = cursorApiBaseURL(); + const url = buildLoginUrl(challenge, params.uuid, websiteURL()); + return { + url, + instructions: "Open this URL in a browser to sign in to Cursor", + mode: "auto", + callback: pollForTokens(params.uuid, params.verifier, apiBaseURL).then((result) => ({ + type: "oauth", + methodID: CURSOR_OAUTH_METHOD_ID, + access: result.accessToken, + refresh: result.refreshToken, + expires: decodeJwtExpiryMs(result.accessToken) ?? Date.now(), + })), + }; +} +/** + * Renew an expiring Cursor JWT. The host calls this lazily when the stored + * credential is close to expiry, so it must not assume it runs on every request. + */ +async function refreshOAuth(credential) { + const tokens = await refreshAccessToken(credential.refresh, cursorApiBaseURL()); + return { + type: "oauth", + methodID: credential.methodID || CURSOR_OAUTH_METHOD_ID, + access: tokens.accessToken, + refresh: tokens.refreshToken, + expires: decodeJwtExpiryMs(tokens.accessToken) ?? Date.now(), + ...(credential.metadata === undefined ? {} : { metadata: credential.metadata }), + }; +} +/** Register the Cursor integration and its three connection methods. */ +export function applyCursorIntegration(draft) { + draft.update(CURSOR_INTEGRATION_ID, (integration) => { + integration.id = CURSOR_INTEGRATION_ID; + integration.name = "Cursor"; + }); + draft.method.update({ + integrationID: CURSOR_INTEGRATION_ID, + method: { + id: CURSOR_OAUTH_METHOD_ID, + type: "oauth", + label: "Cursor account (browser login)", + }, + authorize: authorizeOAuth, + refresh: refreshOAuth, + }); + draft.method.update({ + integrationID: CURSOR_INTEGRATION_ID, + method: { type: "key", label: "API key (cursor.com/settings)" }, + }); + draft.method.update({ + integrationID: CURSOR_INTEGRATION_ID, + method: { type: "env", names: CURSOR_ENV_NAMES }, + }); +} +/** + * Turn a stored credential into a Cursor access token. + * + * OAuth credentials already hold a JWT (the host refreshes them via `refresh` + * above). A `key` credential is the raw `crsr_…` API key, which Cursor requires us + * to exchange for a short-lived JWT — mirroring the classic plugin's behavior. + */ +export async function accessTokenFromCredential(credential) { + if (!credential) + return undefined; + if (credential.type === "oauth") { + if (credential.access && !isExpiringSoon(credential.access)) + return credential.access; + if (!credential.refresh) + return credential.access || undefined; + try { + return (await refreshOAuth(credential)).access; + } + catch { + // Fall back to the existing token; the call may still succeed. + return credential.access || undefined; + } + } + if (credential.type === "key") { + // Already a JWT (e.g. exchanged by an earlier run) — use it directly. + if (!credential.key.startsWith("crsr_")) + return credential.key; + try { + return (await exchangeApiKey(credential.key, cursorApiBaseURL())).accessToken; + } + catch { + return undefined; + } + } + return undefined; +} +/** Resolve the active Cursor connection into an access token, if any. */ +export async function resolveCursorAccessToken(integration) { + try { + const connection = await integration.connection.active(CURSOR_INTEGRATION_ID); + if (!connection) + return undefined; + return await accessTokenFromCredential(await integration.connection.resolve(connection)); + } + catch { + return undefined; + } +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/todo-tools.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/todo-tools.d.ts new file mode 100644 index 00000000..7b041da1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/todo-tools.d.ts @@ -0,0 +1,64 @@ +import type { ToolDraft } from "./types.js"; +/** + * OpenCode 2 snapshot: only `codemode === false` tools join the AI SDK catalog. + * Everything else is Code Mode-only (`packages/core/src/tool.ts` direct vs + * `codeModeTools`). Plugin-owned todo tools must opt into the direct catalog so + * Cursor can execute them without going through Code Mode. + */ +export declare const OPENCODE2_DIRECT_TOOL_OPTIONS: { + readonly codemode: false; +}; +/** + * Force-enable plugin-owned `todowrite` / `todoread` on OpenCode 2.0. + * Default is off: the host has no TUI/desktop checklist, so advertising + * these in-memory tools is opt-in. OpenCode 1.x is unaffected (host builtin). + * Same truthy rule as `CURSOR_PROVIDER_DEBUG` (`1` or `true`). + */ +export declare const CURSOR_OPENCODE2_TODOS_ENV = "CURSOR_OPENCODE2_TODOS"; +export declare function isOpenCode2TodosEnabled(env?: NodeJS.ProcessEnv): boolean; +/** + * 1.x `todowrite` description, shortened. OpenCode 2 has no builtin todo + * tools; this is the catalog text the model sees when the 2.0 gate is on. + */ +export declare const TODOWRITE_DESCRIPTION = "Create and maintain a structured task list for the current coding session. Tracks progress, organizes multi-step work, and surfaces status to the user.\n\nUse proactively when the work is 3+ distinct steps, non-trivial, or the user lists multiple tasks. Skip single straightforward edits and purely informational questions.\n\nStates: pending, in_progress (exactly one at a time), completed, cancelled.\nUpdate status as you go. Mark completed only after the work \u2014 including verification \u2014 is actually done."; +export declare const TODOREAD_DESCRIPTION = "Read the current session todo list. Takes no arguments. Returns the full list as JSON."; +export declare const TODO_OUTPUT_SCHEMA: { + readonly type: "object"; + readonly additionalProperties: false; + readonly properties: { + readonly todos: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly additionalProperties: false; + readonly properties: { + readonly id: { + readonly type: "string"; + }; + readonly content: { + readonly type: "string"; + }; + readonly status: { + readonly type: "string"; + readonly enum: readonly ["pending", "in_progress", "completed", "cancelled"]; + }; + readonly priority: { + readonly type: "string"; + readonly enum: readonly ["high", "medium", "low"]; + }; + }; + readonly required: readonly ["id", "content", "status", "priority"]; + }; + }; + }; + readonly required: readonly ["todos"]; +}; +/** True when the host editor already owns this tool id. */ +export declare function hostHasTool(draft: ToolDraft, name: string): boolean; +/** + * Register canonical `todowrite` / `todoread` when the 2.0 force-enable gate + * is on and the host catalog does not already advertise them. No-op when the + * gate is off (default), on OpenCode 1.x (this function is not called), and + * on any 2.x host that restores the tools. + */ +export declare function registerTodoTools(draft: ToolDraft): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/todo-tools.js b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/todo-tools.js new file mode 100644 index 00000000..e3922992 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/todo-tools.js @@ -0,0 +1,165 @@ +import { TODOREAD_TOOL, TODOWRITE_TOOL, getSessionTodos, setSessionTodos, } from "../todo-store.js"; +/** + * OpenCode 2 snapshot: only `codemode === false` tools join the AI SDK catalog. + * Everything else is Code Mode-only (`packages/core/src/tool.ts` direct vs + * `codeModeTools`). Plugin-owned todo tools must opt into the direct catalog so + * Cursor can execute them without going through Code Mode. + */ +export const OPENCODE2_DIRECT_TOOL_OPTIONS = { codemode: false }; +/** + * Force-enable plugin-owned `todowrite` / `todoread` on OpenCode 2.0. + * Default is off: the host has no TUI/desktop checklist, so advertising + * these in-memory tools is opt-in. OpenCode 1.x is unaffected (host builtin). + * Same truthy rule as `CURSOR_PROVIDER_DEBUG` (`1` or `true`). + */ +export const CURSOR_OPENCODE2_TODOS_ENV = "CURSOR_OPENCODE2_TODOS"; +export function isOpenCode2TodosEnabled(env = process.env) { + const value = env[CURSOR_OPENCODE2_TODOS_ENV]?.toLowerCase(); + return value === "1" || value === "true"; +} +/** + * 1.x `todowrite` description, shortened. OpenCode 2 has no builtin todo + * tools; this is the catalog text the model sees when the 2.0 gate is on. + */ +export const TODOWRITE_DESCRIPTION = `Create and maintain a structured task list for the current coding session. Tracks progress, organizes multi-step work, and surfaces status to the user. + +Use proactively when the work is 3+ distinct steps, non-trivial, or the user lists multiple tasks. Skip single straightforward edits and purely informational questions. + +States: pending, in_progress (exactly one at a time), completed, cancelled. +Update status as you go. Mark completed only after the work — including verification — is actually done.`; +export const TODOREAD_DESCRIPTION = "Read the current session todo list. Takes no arguments. Returns the full list as JSON."; +const TODO_ITEM_SCHEMA = { + type: "object", + additionalProperties: false, + properties: { + id: { type: "string", description: "Optional stable item id" }, + content: { type: "string", description: "Brief description of the task" }, + status: { + type: "string", + description: "pending, in_progress, completed, or cancelled", + enum: ["pending", "in_progress", "completed", "cancelled"], + }, + priority: { + type: "string", + description: "high, medium, or low", + enum: ["high", "medium", "low"], + }, + }, + required: ["content"], +}; +const TODO_OUTPUT_ITEM_SCHEMA = { + type: "object", + additionalProperties: false, + properties: { + id: { type: "string" }, + content: { type: "string" }, + status: { + type: "string", + enum: ["pending", "in_progress", "completed", "cancelled"], + }, + priority: { + type: "string", + enum: ["high", "medium", "low"], + }, + }, + required: ["id", "content", "status", "priority"], +}; +export const TODO_OUTPUT_SCHEMA = { + type: "object", + additionalProperties: false, + properties: { + todos: { type: "array", items: TODO_OUTPUT_ITEM_SCHEMA }, + }, + required: ["todos"], +}; +function todoResult(todos) { + const content = JSON.stringify(todos, null, 2); + return { + output: { todos }, + content, + metadata: { todos }, + }; +} +function sessionIDFromContext(context) { + if (!context || typeof context !== "object") { + throw new Error("OpenCode 2.0 todo tool context did not provide a sessionID"); + } + const id = context.sessionID; + if (typeof id !== "string" || !id) { + throw new Error("OpenCode 2.0 todo tool context did not provide a sessionID"); + } + return id; +} +/** True when the host editor already owns this tool id. */ +export function hostHasTool(draft, name) { + if (typeof draft.get === "function") { + try { + if (draft.get(name)) + return true; + } + catch { + // Host get() may throw for unknown ids; fall through to list(). + } + } + if (typeof draft.list === "function") { + try { + return draft.list().some((tool) => tool.id === name || tool.name === name); + } + catch { + return false; + } + } + return false; +} +/** + * Register canonical `todowrite` / `todoread` when the 2.0 force-enable gate + * is on and the host catalog does not already advertise them. No-op when the + * gate is off (default), on OpenCode 1.x (this function is not called), and + * on any 2.x host that restores the tools. + */ +export function registerTodoTools(draft) { + if (!isOpenCode2TodosEnabled()) + return; + if (!hostHasTool(draft, TODOWRITE_TOOL)) { + draft.add({ + name: TODOWRITE_TOOL, + description: TODOWRITE_DESCRIPTION, + input: { + type: "object", + additionalProperties: false, + properties: { + todos: { + type: "array", + description: "The updated todo list (replace-all snapshot)", + items: TODO_ITEM_SCHEMA, + }, + }, + required: ["todos"], + }, + output: TODO_OUTPUT_SCHEMA, + options: OPENCODE2_DIRECT_TOOL_OPTIONS, + execute: async (input, context) => { + const sessionID = sessionIDFromContext(context); + const todos = setSessionTodos(sessionID, input?.todos); + return todoResult(todos); + }, + }); + } + if (!hostHasTool(draft, TODOREAD_TOOL)) { + draft.add({ + name: TODOREAD_TOOL, + description: TODOREAD_DESCRIPTION, + input: { + type: "object", + additionalProperties: false, + properties: {}, + }, + output: TODO_OUTPUT_SCHEMA, + options: OPENCODE2_DIRECT_TOOL_OPTIONS, + execute: async (_input, context) => { + const sessionID = sessionIDFromContext(context); + return todoResult(getSessionTodos(sessionID)); + }, + }); + } +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/types.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/types.d.ts new file mode 100644 index 00000000..ad3da4be --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/types.d.ts @@ -0,0 +1,389 @@ +/** + * Runtime duck-type boundary for the OpenCode 2.0 plugin — not host conformance. + * + * Only the methods and fields this plugin calls or publishes. Extra host + * fields are ignored at runtime. Do not import `@opencode/plugin`: it is not a + * standalone types surface we can pin next to `@opencode-ai/plugin`, and a host + * dependency would force a plugin bump on every OpenCode release. + * + * Compile-time checks that our *calls and payloads* still fit the fuller host + * editor live in `test/opencode2-conformance.types.ts` against + * `test/opencode2-host-contract.ts`. + * + * Effect `Schema` brands (Provider.ID, Model.ID, …) are modelled as plain + * `string`; brands are compile-time only and erase at runtime. + */ +export type Registration = { + readonly dispose: () => Promise; +}; +export type Hooks = (name: Name, callback: (input: Spec[Name]) => Promise | void) => Promise; +export type Transform = (callback: (input: Input) => void) => Promise; +export type ProviderInfo = { + id: string; + name: string; + /** `"aisdk:"` selects the AI SDK path; a bare specifier selects native. */ + package: string; + activation: "auto" | "enabled" | "disabled"; + integrationID?: string; +}; +export type ModelVariantInfo = { + id: string; + settings?: Record; +}; +export type ModelInfo2 = { + id: string; + /** Wire id sent to the provider. Lets one Cursor model back several entries. */ + modelID: string; + providerID: string; + name: string; + capabilities: { + tools: boolean; + input: readonly string[]; + output: readonly string[]; + }; + variants: readonly ModelVariantInfo[]; + time: { + released: number; + }; + /** OpenCode 2.0 tiered $/1M rates; empty when Cursor does not publish a price. */ + cost: readonly { + tier?: { + type: "context"; + size: number; + }; + input: number; + output: number; + cache: { + read: number; + write: number; + }; + }[]; + status: "active"; + enabled: boolean; + limit: { + context: number; + input?: number; + output: number; + }; + settings?: Record; +}; +export type CredentialOAuth = { + type: "oauth"; + methodID: string; + refresh: string; + access: string; + expires: number; + metadata?: Record; +}; +export type CredentialKey = { + type: "key"; + key: string; + metadata?: Record; +}; +export type CredentialValue = CredentialOAuth | CredentialKey; +export type ConnectionInfo = { + type: "credential"; + id: string; + label: string; +} | { + type: "env"; + name: string; +}; +/** + * Inventory writer this plugin uses (`editor.add` only). + * The host editor is a superset; unused methods are not part of this boundary. + */ +export type ProviderEditor = { + add(input: { + info: ProviderInfo; + models: readonly ModelInfo2[]; + sourceConnection?: ConnectionInfo; + }): void; +}; +export type ProviderDomain = { + readonly transform: Transform; + readonly reload: () => Promise; +}; +export type IntegrationTextPrompt = { + type: "text"; + key: string; + message: string; + placeholder?: string; +}; +export type IntegrationOAuthMethod = { + id: string; + type: "oauth"; + label: string; + form?: unknown; + prompts?: IntegrationTextPrompt[]; +}; +export type IntegrationKeyMethod = { + type: "key"; + label?: string; +}; +export type IntegrationEnvMethod = { + type: "env"; + names: string[]; +}; +export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod; +export type IntegrationOAuthAuthorization = { + readonly url: string; + readonly instructions: string; + readonly expiresAt?: number; +} & ({ + readonly mode: "auto"; + readonly callback: Promise; +} | { + readonly mode: "code"; + readonly callback: (code: string) => Promise; +}); +export type IntegrationOAuthMethodRegistration = { + readonly integrationID: string; + readonly method: IntegrationOAuthMethod; + readonly authorize: (answer: any) => Promise; + readonly refresh?: (credential: CredentialOAuth) => Promise; + readonly label?: (credential: CredentialOAuth) => string | undefined; +}; +export type IntegrationMethodRegistration = IntegrationOAuthMethodRegistration | { + readonly integrationID: string; + readonly method: IntegrationKeyMethod; +} | { + readonly integrationID: string; + readonly method: IntegrationEnvMethod; +}; +export type IntegrationRef = { + id: string; + name: string; +}; +export type IntegrationDraft = { + update(id: string, update: (integration: IntegrationRef) => void): void; + readonly method: { + update(input: IntegrationMethodRegistration): void; + }; +}; +export type IntegrationDomain = { + readonly transform: Transform; + readonly reload: () => Promise; + readonly connection: { + readonly active: (integrationID: string) => Promise; + readonly resolve: (connection: ConnectionInfo) => Promise; + }; +}; +/** Fields this plugin reads from the host model object. */ +export type HostModelRef = { + readonly id: string; + readonly modelID: string; + readonly providerID: string; +}; +export type AISDKHooks = { + sdk: { + readonly model: HostModelRef; + readonly package: string; + readonly options: Record; + sdk?: any; + }; + language: { + readonly model: HostModelRef; + readonly sdk: any; + readonly options: Record; + language?: unknown; + }; +}; +export type AISDKDomain = { + readonly hook: Hooks; +}; +export type ToolOptions = { + readonly namespace?: string; + readonly permission?: string; + readonly codemode?: boolean; + readonly pinned?: boolean; +}; +/** Public OpenCode 2 tool context. It deliberately has no permission `ask`. */ +export type ToolExecutionContext = { + readonly sessionID: string; + readonly agent: string; + readonly messageID: string; + readonly id: string; + readonly progress: (update: Record) => Promise; +}; +export type ToolDefinition = { + readonly name: string; + readonly description: string; + readonly input: unknown; + readonly output?: unknown; + readonly options?: ToolOptions; + readonly execute: (input: any, context: ToolExecutionContext) => Promise; +}; +export type ToolDraft = { + add(tool: ToolDefinition): void; + /** Host ToolEditor.get — used to skip registering tools the host already owns. */ + get?(id: string): (ToolDefinition & { + readonly id?: string; + }) | undefined; + list?(): readonly (ToolDefinition & { + readonly id: string; + })[]; +}; +export type ToolHookBaseFields = { + readonly tool: string; + readonly sessionID: string; + readonly agent: string; + readonly messageID: string; +}; +export type ToolHookBase = ToolHookBaseFields & ({ + readonly id: string; + readonly callID?: string; +} | { + readonly id?: string; + readonly callID: string; +}); +export type ToolHooks = { + readonly "execute.before": ToolHookBase & { + input: unknown; + }; + readonly "execute.after": ToolHookBase & { + readonly input: unknown; + } & ({ + readonly status: "completed"; + result: any; + } | { + readonly status: "error"; + error: any; + }); +}; +export type ToolDomain = { + readonly transform: Transform; + readonly reload: () => Promise; + readonly hook: Hooks; +}; +export type SessionContext = { + readonly sessionID: string; + readonly agent: string; + readonly model: { + providerID: string; + id: string; + variant?: string; + }; + system: Array; + messages: Array; + tools: Record; + /** Mutable generation / provider options. OpenCode 2.0 SessionRequest.options. */ + options?: Record; +}; +export type SessionCompactionResult = { + summary: string; + providerState?: unknown; + metadata?: Record; + tokens?: unknown; +}; +export type SessionCompaction = SessionContext & { + result?: SessionCompactionResult; +}; +export type SessionGenerate = SessionContext; +export type SessionTitle = { + readonly sessionID: string; + readonly model: { + providerID: string; + id: string; + variant?: string; + }; + system: Array; + messages: Array; + options?: Record; + result?: string; +}; +export type SessionHooks = { + readonly context: SessionContext; + readonly compaction: SessionCompaction; + readonly generate: SessionGenerate; + readonly title: SessionTitle; +}; +/** Only the `location.directory` field we actually read. */ +export type SessionInfo = { + readonly id: string; + readonly location: { + readonly directory: string; + }; +}; +export type SessionDomain = { + readonly hook: Hooks; + readonly get: (input: { + sessionID: string; + }) => Promise; + readonly switchAgent?: (input: { + sessionID: string; + agent: string; + }) => Promise; + readonly synthetic?: (input: { + sessionID: string; + text: string; + description?: string; + metadata?: Record; + delivery?: "steer" | "queue"; + resume?: boolean; + }) => Promise; + readonly prompt?: (input: { + sessionID: string; + text: string; + }) => Promise; +}; +export type EventDomain = { + readonly subscribe: (...args: any[]) => any; +}; +export type ShellCreateBefore = { + command: string; + cwd: string; + timeout: number; + shell: string; + env: Record; +}; +export type ShellDomain = { + readonly hook: Hooks<{ + readonly "create.before": ShellCreateBefore; + }>; +}; +export type WebSearchResult = { + url: string; + title?: string; + content?: string; + time: { + published?: number; + }; +}; +export type WebSearchEditor = { + add(definition: { + readonly id: string; + readonly name: string; + readonly execute: (input: { + query: string; + }, context: { + readonly signal: AbortSignal; + }) => Promise; + }): void; +}; +export type WebSearchDomain = { + readonly transform: Transform; + readonly reload: () => Promise; +}; +export type PluginLocation = { + readonly directory: string; +}; +export type PluginContext = { + readonly aisdk: AISDKDomain; + readonly event: EventDomain; + readonly integration: IntegrationDomain; + readonly provider: ProviderDomain; + readonly session: SessionDomain; + readonly tool: ToolDomain; + readonly location?: PluginLocation; + readonly shell?: ShellDomain; + readonly websearch?: WebSearchDomain; +}; +export type Cleanup = () => Promise | void; +export type Plugin2 = { + readonly id: string; + readonly setup: (context: PluginContext) => Promise | Cleanup | void; +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/types.js b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/types.js new file mode 100644 index 00000000..26f1379e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/opencode2/types.js @@ -0,0 +1,16 @@ +/** + * Runtime duck-type boundary for the OpenCode 2.0 plugin — not host conformance. + * + * Only the methods and fields this plugin calls or publishes. Extra host + * fields are ignored at runtime. Do not import `@opencode/plugin`: it is not a + * standalone types surface we can pin next to `@opencode-ai/plugin`, and a host + * dependency would force a plugin bump on every OpenCode release. + * + * Compile-time checks that our *calls and payloads* still fit the fuller host + * editor live in `test/opencode2-conformance.types.ts` against + * `test/opencode2-host-contract.ts`. + * + * Effect `Schema` brands (Provider.ID, Model.ID, …) are modelled as plain + * `string`; brands are compile-time only and erase at runtime. + */ +export {}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plan-execution-kickoff.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/plan-execution-kickoff.d.ts new file mode 100644 index 00000000..8b612310 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plan-execution-kickoff.d.ts @@ -0,0 +1,57 @@ +/** + * Queue OpenCode's post-plan-exit kickoff after CreatePlan approval. + * + * Native `plan_exit` asks the user, then injects a synthetic user message with + * `agent: "build"` so a new turn starts implementing. Our CreatePlan → question + * path already asks; this module is the missing second half. A host plugin + * installs the prompt handler; the language model records a request after Yes, + * and the outer stream wrapper flushes it only after the current `doStream` + * result stream has settled. This prevents a second generation from starting + * while the continuation Run is still live. + */ +export type PlanExecutionKickoffInput = { + sessionID: string; + /** Filesystem path (or relative display path) of the approved plan. */ + planPath: string; + /** Concrete Cursor Run that owns the approval. Guards recovery/supersession. */ + cursorSessionID?: string; +}; +export type PlanExecutionKickoffFn = (input: PlanExecutionKickoffInput) => void | Promise; +/** Upstream `PlanExitTool` wording — keep verbatim so behaviour matches OpenCode. */ +export declare function createPlanExecutionKickoffText(planPath: string): string; +/** + * Prefer a worktree-relative label when the plan sits under `workspaceRoot`, + * matching OpenCode's `path.relative(instance.worktree, Session.plan(...))`. + * Otherwise keep the absolute path (global data `plans/` is outside the tree). + */ +export declare function formatPlanKickoffPath(planPath: string, workspaceRoot?: string): string; +/** Decode a `file://` plan URI to a filesystem path; pass other strings through. */ +export declare function planPathFromUri(planUri: string): string; +export type PlanExecutionKickoffState = PlanExecutionKickoffInput & { + status: "pending" | "failed"; + attempts: number; + lastError?: string; +}; +/** Install (or clear) the host kickoff. */ +export declare function setPlanExecutionKickoff(fn: PlanExecutionKickoffFn | undefined): void; +export declare function hasPlanExecutionKickoff(): boolean; +/** Record the approved plan; execution is flushed after the stream settles. */ +export declare function queuePlanExecutionKickoff(input: PlanExecutionKickoffInput): boolean; +/** Drop any approval state owned by a deleted host session. */ +export declare function cancelPlanExecutionKickoff(sessionID: string | undefined): void; +/** + * Run one pending kickoff only after the owning Cursor Run reached a terminal, + * idle state. Returns true only after successful host handoff. + */ +export declare function flushPlanExecutionKickoff(sessionID: string | undefined, options?: { + cursorSessionID?: string; + terminal?: boolean; + pumpActive?: boolean; + pendingExecs?: number; +}): Promise; +/** Consume a user-visible warning on the next provider turn. */ +export declare function takePlanExecutionKickoffWarning(sessionID: string | undefined): string | undefined; +/** Retry state remains explicit; no timer or automatic loop fires it. */ +export declare function planExecutionKickoffState(sessionID: string | undefined): PlanExecutionKickoffState | undefined; +/** Test helper: clear the registered kickoff and pending approvals. */ +export declare function resetPlanExecutionKickoffForTests(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plan-execution-kickoff.js b/references/upstream-plugins/cursor-opencode-provider/dist/plan-execution-kickoff.js new file mode 100644 index 00000000..e1d04ba5 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plan-execution-kickoff.js @@ -0,0 +1,186 @@ +/** + * Queue OpenCode's post-plan-exit kickoff after CreatePlan approval. + * + * Native `plan_exit` asks the user, then injects a synthetic user message with + * `agent: "build"` so a new turn starts implementing. Our CreatePlan → question + * path already asks; this module is the missing second half. A host plugin + * installs the prompt handler; the language model records a request after Yes, + * and the outer stream wrapper flushes it only after the current `doStream` + * result stream has settled. This prevents a second generation from starting + * while the continuation Run is still live. + */ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { trace } from "./debug.js"; +/** Upstream `PlanExitTool` wording — keep verbatim so behaviour matches OpenCode. */ +export function createPlanExecutionKickoffText(planPath) { + const where = planPath.trim() || "the plan"; + return `The plan at ${where} has been approved, you can now edit files. Execute the plan`; +} +/** + * Prefer a worktree-relative label when the plan sits under `workspaceRoot`, + * matching OpenCode's `path.relative(instance.worktree, Session.plan(...))`. + * Otherwise keep the absolute path (global data `plans/` is outside the tree). + */ +export function formatPlanKickoffPath(planPath, workspaceRoot) { + const absolute = planPath.trim(); + if (!absolute) + return absolute; + const root = workspaceRoot?.trim(); + if (!root) + return absolute; + const relative = path.relative(root, absolute); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return absolute; + } + return relative; +} +/** Decode a `file://` plan URI to a filesystem path; pass other strings through. */ +export function planPathFromUri(planUri) { + const raw = planUri.trim(); + if (!raw) + return ""; + if (raw.startsWith("file:")) { + try { + return fileURLToPath(raw); + } + catch { + return raw; + } + } + return raw; +} +let kickoff; +const pending = new Map(); +const warnings = new Map(); +const MAX_PENDING_PLAN_KICKOFFS = 256; +/** Install (or clear) the host kickoff. */ +export function setPlanExecutionKickoff(fn) { + kickoff = fn; + if (!fn) { + pending.clear(); + warnings.clear(); + } +} +export function hasPlanExecutionKickoff() { + return kickoff !== undefined; +} +/** Record the approved plan; execution is flushed after the stream settles. */ +export function queuePlanExecutionKickoff(input) { + const sessionID = input.sessionID.trim(); + const planPath = input.planPath.trim(); + if (!sessionID || !planPath || !kickoff) { + trace(`plan-execution-kickoff: not queued sessionID=${JSON.stringify(sessionID)} ` + + `planPath=${JSON.stringify(planPath)} handler=${Boolean(kickoff)}`); + return false; + } + pending.set(sessionID, { + sessionID, + planPath, + ...(input.cursorSessionID ? { cursorSessionID: input.cursorSessionID } : {}), + status: "pending", + attempts: 0, + }); + warnings.delete(sessionID); + while (pending.size > MAX_PENDING_PLAN_KICKOFFS) { + const oldest = pending.keys().next().value; + if (!oldest) + break; + pending.delete(oldest); + warnings.delete(oldest); + } + trace(`plan-execution-kickoff: pending sessionID=${sessionID} ` + + `cursorSessionID=${input.cursorSessionID ?? ""} planPath=${planPath}`); + return true; +} +/** Drop any approval state owned by a deleted host session. */ +export function cancelPlanExecutionKickoff(sessionID) { + const key = sessionID?.trim(); + if (!key) + return; + pending.delete(key); + warnings.delete(key); +} +/** + * Run one pending kickoff only after the owning Cursor Run reached a terminal, + * idle state. Returns true only after successful host handoff. + */ +export async function flushPlanExecutionKickoff(sessionID, options = {}) { + const key = sessionID?.trim(); + if (!key) + return false; + const state = pending.get(key); + if (!state) + return false; + if (options.terminal !== true || options.pumpActive || (options.pendingExecs ?? 0) > 0) { + trace(`plan-execution-kickoff: deferred sessionID=${key} terminal=${Boolean(options.terminal)} ` + + `pumpActive=${Boolean(options.pumpActive)} pending=${options.pendingExecs ?? 0}`); + return false; + } + if (state.cursorSessionID + && options.cursorSessionID + && state.cursorSessionID !== options.cursorSessionID) { + // The owning Run was superseded before the handoff could happen. Applying + // it from a later Run would execute a stale approval in the wrong turn. + pending.delete(key); + warnings.delete(key); + trace(`plan-execution-kickoff: discarded stale Run sessionID=${key} ` + + `owner=${state.cursorSessionID} terminal=${options.cursorSessionID}`); + return false; + } + const run = kickoff; + if (!run) { + const message = "The plan was approved, but this host cannot start its execution turn."; + state.status = "failed"; + state.lastError = message; + warnings.set(key, message); + return false; + } + state.attempts += 1; + try { + await run({ + sessionID: state.sessionID, + planPath: state.planPath, + ...(state.cursorSessionID ? { cursorSessionID: state.cursorSessionID } : {}), + }); + pending.delete(key); + warnings.delete(key); + trace(`plan-execution-kickoff: queued sessionID=${key} planPath=${state.planPath}`); + return true; + } + catch (error) { + const detail = error instanceof Error ? error.message : String(error); + const message = `The approved plan could not start execution: ${detail}. ` + + "The plan remains active and can be retried."; + state.status = "failed"; + state.lastError = detail; + // A later explicit provider turn may retry after its own terminal boundary; + // do not keep pinning the retry to the now-finished Cursor Run. + delete state.cursorSessionID; + warnings.set(key, message); + trace(`plan-execution-kickoff: FAILED sessionID=${key} attempts=${state.attempts} err=${detail}`); + return false; + } +} +/** Consume a user-visible warning on the next provider turn. */ +export function takePlanExecutionKickoffWarning(sessionID) { + const key = sessionID?.trim(); + if (!key) + return undefined; + const message = warnings.get(key); + if (message) + warnings.delete(key); + return message; +} +/** Retry state remains explicit; no timer or automatic loop fires it. */ +export function planExecutionKickoffState(sessionID) { + const key = sessionID?.trim(); + const state = key ? pending.get(key) : undefined; + return state ? { ...state } : undefined; +} +/** Test helper: clear the registered kickoff and pending approvals. */ +export function resetPlanExecutionKickoffForTests() { + kickoff = undefined; + pending.clear(); + warnings.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plugin-core.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-core.d.ts new file mode 100644 index 00000000..152d3c66 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-core.d.ts @@ -0,0 +1,27 @@ +import { createCursorLanguageModel } from "./language-model.js"; +import type { CreateCursorOptions } from "./index.js"; +/** + * Runtime-agnostic pieces shared by every plugin surface: + * • classic Hooks plugin (`plugin.ts`) + * • OpenCode 1.18 v2 plugin (`plugin-v2.ts`) + * • OpenCode 2.0 beta plugin (`plugin-opencode2.ts`) + * + * Nothing here may import a host plugin API — the 1.18 and 2.0 APIs are + * source-incompatible, so anything host-shaped belongs in the entrypoint. + */ +/** Minimal AI SDK provider surface OpenCode expects from an `aisdk` package. */ +export type CursorSdk = { + languageModel(modelId: string): ReturnType; +}; +export declare function createSdk(options: CreateCursorOptions): CursorSdk; +/** + * Whether an OpenCode `aisdk` package string refers to this provider. + * + * Matches on the provider id first (the common case once the catalog entry is + * ours), then on the package specifier so a bare `cursor-opencode-provider`, + * an `aisdk:`-prefixed form, or a resolved `dist/index.js` file URL all hit. + */ +export declare function isCursorPackage(pkg: string, providerID: string): boolean; +/** API base for auth, model discovery, and GetServerConfig. */ +export declare function cursorApiBaseURL(): string; +export declare function cursorGetServerConfigTelemetryEnabled(): boolean; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plugin-core.js b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-core.js new file mode 100644 index 00000000..a9e26947 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-core.js @@ -0,0 +1,31 @@ +import { CURSOR_API_HOST, CURSOR_PROVIDER_ID } from "./shared.js"; +import { createCursorLanguageModel } from "./language-model.js"; +export function createSdk(options) { + const providerId = options.name || CURSOR_PROVIDER_ID; + return { + languageModel(modelId) { + return createCursorLanguageModel(modelId, providerId, options); + }, + }; +} +/** + * Whether an OpenCode `aisdk` package string refers to this provider. + * + * Matches on the provider id first (the common case once the catalog entry is + * ours), then on the package specifier so a bare `cursor-opencode-provider`, + * an `aisdk:`-prefixed form, or a resolved `dist/index.js` file URL all hit. + */ +export function isCursorPackage(pkg, providerID) { + if (providerID === CURSOR_PROVIDER_ID) + return true; + return (pkg.includes("cursor-opencode-provider") || + /cursor-opencode-provider[/\\]dist[/\\]index\.js/.test(pkg)); +} +/** API base for auth, model discovery, and GetServerConfig. */ +export function cursorApiBaseURL() { + return process.env.CURSOR_API_BASE_URL ?? `https://${CURSOR_API_HOST}`; +} +export function cursorGetServerConfigTelemetryEnabled() { + return (process.env.CURSOR_GET_SERVER_CONFIG_TELEMETRY === "1" || + process.env.CURSOR_GET_SERVER_CONFIG_TELEMETRY === "true"); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plugin-opencode2.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-opencode2.d.ts new file mode 100644 index 00000000..1fa20934 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-opencode2.d.ts @@ -0,0 +1,6 @@ +import { CursorPlugin } from "./plugin.js"; +import type { Plugin2 } from "./opencode2/types.js"; +declare const plugin: Plugin2 & { + server: typeof CursorPlugin; +}; +export default plugin; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plugin-opencode2.js b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-opencode2.js new file mode 100644 index 00000000..b841b6c5 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-opencode2.js @@ -0,0 +1,537 @@ +import { CURSOR_PROVIDER_ID, CURSOR_COMPACTION_OPTION, CURSOR_HOST_AGENT_OPTION, } from "./shared.js"; +import { createSdk, cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled, isCursorPackage } from "./plugin-core.js"; +import { opencodeGlobalCacheDir } from "./context/paths.js"; +import { discoverModels, isCacheFresh, readCache } from "./models.js"; +import { resolveAgentUrl } from "./agent-url.js"; +import { sessionActivity } from "./activity.js"; +import { fetchOpenCodeWebSearchText, parseExaWebSearchResults, } from "./web-tools.js"; +import { captureCursorShellResult, cursorShellEnvForCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } from "./shell-timeout.js"; +import { applyCursorProviderInventory, CURSOR_INTEGRATION_ID } from "./opencode2/catalog.js"; +import { applyCursorIntegration, resolveCursorAccessToken } from "./opencode2/integration.js"; +import { registerTodoTools } from "./opencode2/todo-tools.js"; +import { OPENCODE_2_TOOL_DIALECT } from "./protocol/tools.js"; +import { clearSessionTodos } from "./todo-store.js"; +import { markCompactionSession } from "./compaction-marker.js"; +import { markSessionDirectory } from "./session-directory.js"; +import { cancelPlanExecutionKickoff, createPlanExecutionKickoffText, setPlanExecutionKickoff, } from "./plan-execution-kickoff.js"; +import { cancelHostAgentModeSwitch, setHostAgentModeSwitch, } from "./host-agent-mode.js"; +import { clearActiveCursorMode, getActiveCursorMode, normalizeSwitchModeId, setActiveCursorMode, } from "./protocol/switch-mode.js"; +import { CursorPlugin } from "./plugin.js"; +/** + * OpenCode 2.0 plugin. + * + * Separate from `plugin-v2.ts` on purpose: the OpenCode 1.18 `/v2/promise` API + * and the 2.0 API are source-incompatible (hook signatures, OAuth value type, + * provider schema), so they cannot share an entrypoint. Shared behavior lives in + * `plugin-core.ts`, `model-config.ts`, and `opencode2/*`. + * + * Models register in memory via `ctx.provider.transform` + `editor.add` + + * `reload()`. Nothing is written into `opencode.json`. + * + * Dual export: `{ id, setup, server: CursorPlugin }`. OpenCode 2.0 Host.resolve + * loads `./server` then `setup()`. OpenCode 1.18 also prefers `exports["./server"]` + * and then calls `server()` so classic 1.x hooks still run. + * + * Load with: { "plugin": ["cursor-opencode-provider/plugin/opencode2"] } + * or a local package directory under `$OPENCODE_CONFIG_DIR/plugins/` that + * re-exports `dist/plugin-opencode2.js` (OpenCode 2.0 requires a directory, + * not a .js path). + */ +async function loadModels(cacheDir, accessToken, forceRefresh = false) { + const cached = await readCache(cacheDir); + if (!forceRefresh && cached?.models.length && isCacheFresh(cached)) + return cached.models; + if (accessToken) { + try { + return await discoverModels(accessToken, cacheDir, { + baseURL: cursorApiBaseURL(), + forceRefresh, + }); + } + catch { + // A forced refresh follows a credential switch. Do not bind a cache + // produced by the prior account to the new connection on failure. + if (forceRefresh) + return []; + } + } + if (forceRefresh) + return []; + // Preserve offline / stale-cache behavior rather than emptying the picker. + return cached?.models ?? []; +} +function toolExecutionID(event) { + const id = event.id ?? event.callID; + if (!id) + throw new Error("OpenCode 2.0 tool hook did not provide an execution id"); + return id; +} +function isShellTool(name) { + return name === "bash" || name === "shell"; +} +function eventPayload(event) { + if (event?.data && typeof event.data === "object") + return event.data; + if (event?.properties && typeof event.properties === "object") + return event.properties; + return event; +} +function markCompactionAndOptions(event, isCompaction) { + markCompactionSession(event.sessionID, isCompaction); + // Keep the request-local flag authoritative. Explicit false prevents a + // concurrent title/generate/primary request for the same session from + // inheriting the process-wide fallback marker. + event.options ??= {}; + event.options[CURSOR_COMPACTION_OPTION] = isCompaction; +} +const plugin = { + id: "cursor.provider", + server: CursorPlugin, + setup: async (ctx) => { + const cacheDir = opencodeGlobalCacheDir(); + const workspaceRoot = ctx.location?.directory || process.cwd(); + const hasShellEnvHook = typeof ctx.shell?.hook === "function"; + const admitPlanKickoff = typeof ctx.session.synthetic === "function" + ? ctx.session.synthetic + : ctx.session.prompt; + if (typeof ctx.session.switchAgent === "function" && typeof admitPlanKickoff === "function") { + const switchAgent = ctx.session.switchAgent; + setPlanExecutionKickoff(async ({ sessionID, planPath }) => { + await switchAgent({ sessionID, agent: "build" }); + try { + await admitPlanKickoff({ + sessionID, + text: createPlanExecutionKickoffText(planPath), + }); + } + catch (error) { + // Switching and admitting input are separate public APIs. Restore the + // plan agent if admission fails so the shared retry state is honest: + // the plan remains active rather than silently leaving the session in + // build mode with no execution turn. + await switchAgent({ sessionID, agent: "plan" }).catch(() => { }); + throw error; + } + }); + } + else { + setPlanExecutionKickoff(undefined); + } + if (typeof ctx.session.switchAgent === "function") { + const switchAgent = ctx.session.switchAgent; + setHostAgentModeSwitch(async ({ sessionID, targetModeID }) => { + const mode = normalizeSwitchModeId(targetModeID); + await switchAgent({ + sessionID, + agent: mode === "plan" || mode === "spec" ? "plan" : "build", + }); + }); + } + else { + setHostAgentModeSwitch(undefined); + } + const registrations = []; + const track = async (p) => { + registrations.push(await p); + }; + let models = []; + let sourceConnection; + // ── Credentials ───────────────────────────────────────── + await track(ctx.integration.transform(applyCursorIntegration)); + let cachedToken; + let tokenInflight; + /** + * Cache only a *successful* resolution. + * + * On a fresh install `setup()` runs before the user has connected, so the + * first attempt necessarily returns nothing. Memoizing that would pin the + * plugin to "no credentials" for the whole process and models would never + * load, even after a successful /connect. + */ + const accessToken = async () => { + if (cachedToken) + return cachedToken; + tokenInflight ??= resolveCursorAccessToken(ctx.integration).finally(() => { + tokenInflight = undefined; + }); + const token = await tokenInflight; + if (token) + cachedToken = token; + return token; + }; + const refreshSourceConnection = async () => { + try { + sourceConnection = await ctx.integration.connection.active(CURSOR_INTEGRATION_ID); + } + catch { + sourceConnection = undefined; + } + }; + // ── Provider inventory (in-memory `editor.add`) ───────────────────── + // Transform replays on provider.reload(). Skip while `models` is empty so + // the first registration is a no-op; discovery/cache then reload. + await track(ctx.provider.transform((editor) => { + applyCursorProviderInventory(editor, models, sourceConnection); + })); + try { + const cached = await readCache(cacheDir); + if (cached?.models?.length) { + models = cached.models; + await refreshSourceConnection(); + await ctx.provider.reload(); + } + } + catch { + // Cache seed is best-effort; auth/aisdk still work without it. + } + const publishModels = async (next) => { + const previousModels = models; + const previousConnection = sourceConnection; + models = next; + await refreshSourceConnection(); + try { + await ctx.provider.reload(); + return true; + } + catch { + // Keep the transform backed by the last inventory that actually + // reloaded. A later host replay must not publish a failed account + // refresh merely because the candidate remained in this closure. + models = previousModels; + sourceConnection = previousConnection; + return false; + } + }; + // ── AI SDK wiring ──────────────────────────────────────── + await track(ctx.aisdk.hook("sdk", async (event) => { + if (event.sdk) + return; + if (!isCursorPackage(event.package, event.model.providerID)) + return; + const token = await accessToken(); + event.sdk = createSdk({ + name: event.model.providerID || CURSOR_PROVIDER_ID, + ...(token ? { accessToken: token } : {}), + // Static fallback only. This hook fires once per model/package, not + // per session, and 2.0 runs one daemon across many projects — the + // real per-request directory comes from `x-opencode-directory` and + // the session.context hook below via `getSessionDirectory`. + workspaceRoot, + cacheDir, + ...event.options, + // Keep after `event.options` so the OC2 plugin always selects the + // `path`/`shell` dialect when advertised schemas are opaque. + defaultDialect: OPENCODE_2_TOOL_DIALECT, + }); + })); + await track(ctx.aisdk.hook("language", (event) => { + if (event.language) + return; + if (event.model.providerID !== CURSOR_PROVIDER_ID) + return; + if (typeof event.sdk?.languageModel !== "function") + return; + // `modelID` is the Cursor wire id; `id` may be a synthetic long-context entry. + event.language = event.sdk.languageModel(event.model.modelID || event.model.id); + })); + // ── Web search ─────────────────────────────────────────── + // Publish an OpenCode 2.0 `websearch` provider (`{url,title,content,time}`). + // The classic entrypoint owns the permission-aware `custom_websearch` + // fallback; 2.0's public plugin tool context cannot request permission. + if (ctx.websearch) { + await track(ctx.websearch.transform((draft) => { + draft.add({ + id: "cursor-exa", + name: "Exa", + execute: async (input, context) => { + const output = await fetchOpenCodeWebSearchText({ query: input.query }, context.signal); + return parseExaWebSearchResults(output); + }, + }); + })); + } + await track(ctx.tool.transform((draft) => { + // OpenCode 2 dropped host todowrite/todoread. Off by default + // (`CURSOR_OPENCODE2_TODOS=1`/`true` force-enables). When on, register + // them as direct catalog tools (`codemode: false` + output schema) if + // the host does not already own those names. When off, register none. + registerTodoTools(draft); + })); + // ── Shell timeout wrapper ──────────────────────────────── + await track(ctx.tool.hook("execute.before", (event) => { + if (!isShellTool(event.tool)) + return; + const executionID = toolExecutionID(event); + prepareCursorShellArgs(executionID, event.input, { + // OpenCode 2.0 `shell.create.before` can inject env for bash/zsh. + preferWrapperCommand: !hasShellEnvHook, + }); + })); + await track(ctx.tool.hook("execute.after", (event) => { + if (!isShellTool(event.tool)) + return; + const executionID = toolExecutionID(event); + try { + if (event.status !== "completed") + return; + const result = event.result; + // V1: `output` is the model-facing string. V2: `output` is structured + // per-tool output (shell: `{ output: string, ... }`) and the + // model-facing text lives on `content`. Sanitize every string + // location; non-strings pass through via the guards in shell-timeout. + if (typeof result.output === "string") { + result.output = captureCursorShellResult(executionID, result.output, result.metadata); + } + else if (result.output && typeof result.output === "object") { + const structured = result.output; + if (typeof structured.output === "string") { + structured.output = captureCursorShellResult(executionID, structured.output, result.metadata); + } + } + if (typeof result.content === "string") { + result.content = sanitizeRegisteredCursorShellOutput(executionID, result.content); + } + else if (Array.isArray(result.content)) { + result.content = result.content.map((item) => { + if (!item || typeof item !== "object") + return item; + const content = item; + if (content.type !== "text" || typeof content.text !== "string") + return item; + return { + ...content, + text: sanitizeRegisteredCursorShellOutput(executionID, content.text), + }; + }); + } + else if (typeof result.output === "string" && result.content === undefined) { + result.content = result.output; + } + if (result.metadata && typeof result.metadata === "object") { + const metadata = result.metadata; + if (typeof metadata.output === "string") { + metadata.output = sanitizeRegisteredCursorShellOutput(executionID, metadata.output); + } + } + } + finally { + releaseCursorShellEnv(executionID); + } + })); + if (ctx.shell) { + await track(ctx.shell.hook("create.before", (event) => { + const env = cursorShellEnvForCommand(event.command, event.cwd); + if (!env) + return; + event.env = { ...event.env, ...env }; + })); + } + const rememberSessionDirectory = async (sessionID) => { + try { + const info = (await ctx.session.get({ sessionID })); + // OpenCode 2.0 stable exposes a flat `directory`; older shapes nest it + // under `location.directory`. Prefer the flat field when both exist. + markSessionDirectory(sessionID, info.directory ?? info.location?.directory); + } + catch { + // Best effort — falls back to the static workspaceRoot above. + } + }; + await track(ctx.session.hook("context", async (event) => { + markCompactionAndOptions(event, event.agent === "compaction"); + event.options ??= {}; + event.options[CURSOR_HOST_AGENT_OPTION] = event.agent; + if (event.agent !== "compaction") { + const activeMode = getActiveCursorMode(event.sessionID); + if (event.agent === "plan") { + if (activeMode !== "plan" && activeMode !== "spec") { + setActiveCursorMode(event.sessionID, "plan"); + } + } + else if (activeMode === "plan" || activeMode === "spec") { + // A direct OpenCode UI switch away from Plan is authoritative. Do + // not overwrite other Cursor-only modes (chat/debug/etc.) merely + // because their closest native primary agent is `build`. + setActiveCursorMode(event.sessionID, "agent"); + } + } + await rememberSessionDirectory(event.sessionID); + })); + await track(ctx.session.hook("compaction", async (event) => { + markCompactionAndOptions(event, true); + await rememberSessionDirectory(event.sessionID); + })); + await track(ctx.session.hook("generate", async (event) => { + markCompactionAndOptions(event, false); + await rememberSessionDirectory(event.sessionID); + })); + await track(ctx.session.hook("title", async (event) => { + markCompactionAndOptions(event, false); + await rememberSessionDirectory(event.sessionID); + })); + // ── Model discovery ──────────────────────────────────────── + let modelsLoaded = false; + let credentialGeneration = 0; + let loadedCredentialGeneration = 0; + let ensureInflight; + const ensureModels = () => { + if (modelsLoaded && loadedCredentialGeneration === credentialGeneration) { + return Promise.resolve(); + } + // If credentials change during an existing discovery, wait for that + // attempt and immediately run again. The generation check prevents the + // older attempt from suppressing the account-scoped refresh. + if (ensureInflight) + return ensureInflight.then(() => ensureModels()); + const attemptGeneration = credentialGeneration; + const forceRefresh = loadedCredentialGeneration !== attemptGeneration; + ensureInflight = (async () => { + try { + const token = await accessToken(); + const discovered = await loadModels(cacheDir, token, forceRefresh); + if (!discovered.length) + return; + if (credentialGeneration !== attemptGeneration) + return; + if (!await publishModels(discovered)) + return; + // A newer credential event arrived while this request was in flight. + // Keep the state dirty so the chained ensure uses the latest token. + if (credentialGeneration !== attemptGeneration) + return; + modelsLoaded = true; + loadedCredentialGeneration = attemptGeneration; + if (token) { + await resolveAgentUrl(token, { + apiBaseURL: cursorApiBaseURL(), + telemetryEnabled: cursorGetServerConfigTelemetryEnabled(), + }).catch(() => { }); + } + } + finally { + ensureInflight = undefined; + } + })(); + return ensureInflight; + }; + const RETRY_INTERVAL_MS = 3_000; + const RETRY_WINDOW_MS = 300_000; + const startedAt = Date.now(); + const retry = setInterval(() => { + if (modelsLoaded || Date.now() - startedAt > RETRY_WINDOW_MS) { + clearInterval(retry); + return; + } + void ensureModels().catch(() => { }); + }, RETRY_INTERVAL_MS); + retry.unref?.(); + void ensureModels().catch(() => { }); + const onCredentialSwitch = () => { + cachedToken = undefined; + modelsLoaded = false; + credentialGeneration++; + }; + const unsubscribe = subscribeSessionActivity(ctx, ensureModels, onCredentialSwitch); + return async () => { + clearInterval(retry); + unsubscribe?.(); + setPlanExecutionKickoff(undefined); + setHostAgentModeSwitch(undefined); + for (const registration of registrations.reverse()) { + await registration.dispose().catch(() => { }); + } + }; + }, +}; +function subscribeSessionActivity(ctx, onEvent, onCredentialSwitch) { + try { + const stream = ctx.event.subscribe(); + if (!stream || typeof stream[Symbol.asyncIterator] !== "function") + return undefined; + let stopped = false; + void (async () => { + for await (const event of stream) { + if (stopped) + break; + applySessionActivity(event, onCredentialSwitch); + onEvent?.(); + } + })().catch(() => { }); + return () => { + stopped = true; + }; + } + catch { + return undefined; + } +} +function applySessionActivity(event, onCredentialSwitch) { + const payload = eventPayload(event); + const info = payload?.info; + switch (event?.type) { + case "credential.switched": + case "credential.updated": { + const integrationID = payload?.integrationID; + if (!integrationID || integrationID === CURSOR_INTEGRATION_ID || integrationID === CURSOR_PROVIDER_ID) { + onCredentialSwitch?.(); + } + break; + } + case "session.created": + case "session.updated": + case "session.forked": { + const id = payload?.sessionID ?? info?.id; + const parentID = payload?.parentID ?? info?.parentID; + if (id) { + sessionActivity.linkSession(id, parentID); + if (event.type === "session.created") + sessionActivity.recordActivity(id); + } + break; + } + case "session.deleted": { + const id = payload?.sessionID ?? info?.id; + if (id) { + sessionActivity.removeSession(id); + clearSessionTodos(id); + clearActiveCursorMode(id); + cancelPlanExecutionKickoff(id); + cancelHostAgentModeSwitch(id); + } + break; + } + case "message.updated": { + const id = payload?.sessionID ?? info?.sessionID; + if (id) + sessionActivity.recordActivity(id); + break; + } + case "message.part.updated": { + const id = payload?.sessionID ?? payload?.part?.sessionID ?? info?.sessionID; + if (id) + sessionActivity.recordActivity(id); + break; + } + case "session.usage.updated": + case "session.usage.recorded": { + const id = payload?.sessionID; + if (id) + sessionActivity.recordActivity(id); + break; + } + default: { + // OpenCode 2.0 emits granular `session.*` progress events instead of + // the V1 `message.updated` family (`session.tool.called/success/failed`, + // `session.step.*`, `session.text.*`, `session.execution.*`, ...). Any of + // them proves the session is alive and renews a pending-tool lease. + const type = typeof event?.type === "string" ? event.type : ""; + if (type.startsWith("session.") && type !== "session.deleted") { + const id = payload?.sessionID ?? info?.sessionID ?? info?.id; + if (id) + sessionActivity.recordActivity(id); + } + break; + } + } +} +export default plugin; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plugin-v2.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-v2.d.ts new file mode 100644 index 00000000..64dceffc --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-v2.d.ts @@ -0,0 +1,11 @@ +type PromiseV2Plugin = { + id: string; + setup: (ctx: { + aisdk: { + sdk: (callback: (event: any) => void) => Promise; + language: (callback: (event: any) => void) => Promise; + }; + }) => Promise; +}; +declare const plugin: PromiseV2Plugin; +export default plugin; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plugin-v2.js b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-v2.js new file mode 100644 index 00000000..b958609c --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plugin-v2.js @@ -0,0 +1,31 @@ +import { CURSOR_PROVIDER_ID } from "./shared.js"; +import { createSdk, isCursorPackage } from "./plugin-core.js"; +/** OpenCode's v2/promise `define` is an identity function. */ +function define(plugin) { + return plugin; +} +const plugin = define({ + id: "cursor.provider", + setup: async (ctx) => { + await ctx.aisdk.sdk((event) => { + if (event.sdk) + return; + if (!isCursorPackage(event.package, event.model.providerID)) + return; + event.sdk = createSdk({ + name: event.model.providerID || CURSOR_PROVIDER_ID, + ...event.options, + }); + }); + await ctx.aisdk.language((event) => { + if (event.model.providerID !== CURSOR_PROVIDER_ID) + return; + if (event.language) + return; + if (typeof event.sdk?.languageModel !== "function") + return; + event.language = event.sdk.languageModel(event.model.api.id); + }); + }, +}); +export default plugin; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plugin.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/plugin.d.ts new file mode 100644 index 00000000..c76f20ff --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plugin.d.ts @@ -0,0 +1,12 @@ +import type { Hooks, PluginInput } from "@opencode-ai/plugin"; +export { modelInfoToConfig, modelsToConfig, thinkingSuffixBaseNames } from "./model-config.js"; +export declare function loadClassicTools(options?: { + importModule?: (specifier: string) => Promise<{ + tool?: any; + }>; + configDirs?: string[]; +}): Promise<{ + webSearch: any; + imageSave: any; +}>; +export declare function CursorPlugin(input: PluginInput): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/plugin.js b/references/upstream-plugins/cursor-opencode-provider/dist/plugin.js new file mode 100644 index 00000000..3a682225 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/plugin.js @@ -0,0 +1,395 @@ +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION, CURSOR_HOST_AGENT_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, } from "./shared.js"; +import { cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled } from "./plugin-core.js"; +import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtExpiryMs } from "./auth.js"; +import { readCache, discoverModels, isCacheFresh } from "./models.js"; +// Re-exported for API compatibility: tests and downstream code import these +// from "./plugin.js". Canonical home is ./model-config.ts, which stays free of +// host plugin imports so the OpenCode 2.0 entrypoint can share it. +import { modelsToConfig } from "./model-config.js"; +export { modelInfoToConfig, modelsToConfig, thinkingSuffixBaseNames } from "./model-config.js"; +import { opencodeGlobalCacheDir, opencodeGlobalConfigDirs } from "./context/paths.js"; +import { readStoredAuth } from "./context/auth-store.js"; +import { resolveAgentUrl } from "./agent-url.js"; +import { captureCursorShellResult, cursorShellEnvForCall, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, setCursorShellPath, } from "./shell-timeout.js"; +import { sessionActivity } from "./activity.js"; +import { createOpenCodeWebSearchTool, openCodeWebSearchTool } from "./web-search-tool.js"; +import { createCursorImageSaveTool, cursorImageSaveTool } from "./image-save-tool.js"; +import { createPlanExecutionKickoffText, setPlanExecutionKickoff, } from "./plan-execution-kickoff.js"; +import { dispatchHostEventBridge } from "./host-event-bridge.js"; +const MODULE_URL = new URL("./index.js", import.meta.url).href; +export async function loadClassicTools(options = {}) { + const configDir = options.configDirs?.[0] ?? opencodeGlobalConfigDirs()[0]; + const candidates = [ + ...(configDir + ? [path.join(configDir, "node_modules", "@opencode-ai", "plugin", "dist", "index.js")] + : []), + "@opencode-ai/plugin", + ]; + const importModule = options.importModule ?? ((specifier) => import(specifier)); + // The global config copy is the host-owned installation; bare import is a fallback. + for (const candidate of candidates) { + try { + const specifier = path.win32.isAbsolute(candidate) && !path.isAbsolute(candidate) + ? new URL(`file:///${candidate.replaceAll("\\", "/")}`).href + : path.isAbsolute(candidate) + ? pathToFileURL(candidate).href + : candidate; + const module = await importModule(specifier); + if (typeof module.tool === "function" && module.tool.schema) { + const factory = { tool: module.tool, schema: module.tool.schema }; + return { + webSearch: createOpenCodeWebSearchTool(factory), + imageSave: createCursorImageSaveTool(factory), + }; + } + } + catch { + // Try the next host-owned/normal resolution location. + } + } + // `tool()` is an identity helper; the fallback definitions use plain JSON + // Schema accepted by OpenCode's legacy registry when no Zod helper is present. + return { webSearch: openCodeWebSearchTool, imageSave: cursorImageSaveTool }; +} +export async function CursorPlugin(input) { + const cacheDir = opencodeGlobalCacheDir(); + const apiBaseURL = cursorApiBaseURL(); + const classicTools = await loadClassicTools(); + // Install the OpenCode plan-exit-shaped kickoff only when the OpenCode client + // exposes that structural API. Without it, no synthetic kickoff is registered. + const sessionClient = input.client?.session; + const promptAsync = sessionClient?.promptAsync; + setPlanExecutionKickoff(typeof promptAsync === "function" + ? async ({ sessionID, planPath }) => { + await promptAsync.call(sessionClient, { + path: { id: sessionID }, + body: { + agent: "build", + parts: [{ + type: "text", + text: createPlanExecutionKickoffText(planPath), + synthetic: true, + }], + }, + }); + } + : undefined); + // Last access token successfully resolved in this plugin instance. Config's + // loadModels can only read OpenCode's durable store (auth.json / + // OPENCODE_AUTH_CONTENT); auth.loader gets live credentials via getAuth(). + // Those usually match, but after a refresh where persistAuthBestEffort fails, + // getAuth() may still see the old credentials while we already hold a usable + // token here — keep it so the loader can still discover models. + let sessionAccessToken; + async function persistAuth(body) { + await input.client.auth.set({ + path: { id: CURSOR_PROVIDER_ID }, + body, + }); + } + /** Persist refreshed credentials without failing the caller that already holds a live token. */ + async function persistAuthBestEffort(body) { + try { + await persistAuth(body); + } + catch { + // ignore — token is still usable for this process + } + } + /** + * Durable credentials OpenCode stores on disk (same file getAuth() reads in + * the normal path). Used from `config`, which has no getAuth() callback. + */ + async function authFromStore() { + return readStoredAuth(CURSOR_PROVIDER_ID); + } + /** + * Prefer OpenCode's live getAuth(); fall back to the durable store so loader + * and config share the same underlying credentials when possible. + */ + async function authForLoader(getAuth) { + return (await getAuth()) ?? (await authFromStore()); + } + async function resolveAccessToken(auth) { + if (auth.type === "api") { + let accessToken = auth.key; + const refreshToken = auth.metadata?.refreshToken; + // API-key exchange returns a short-lived JWT stored as `key`. Refresh + // it the same way as OAuth when it is expiring / already expired. + if (refreshToken && isExpiringSoon(auth.key)) { + try { + const newTokens = await refreshAccessToken(refreshToken, apiBaseURL); + accessToken = newTokens.accessToken; + await persistAuthBestEffort({ + type: "api", + key: newTokens.accessToken, + metadata: { + ...auth.metadata, + refreshToken: newTokens.refreshToken, + }, + }); + } + catch { + // refresh failed — keep the existing key; the next call may still work + } + } + if (accessToken) + sessionAccessToken = accessToken; + return accessToken; + } + if (auth.type === "oauth") { + if (!isExpiringSoon(auth.access)) { + sessionAccessToken = auth.access; + return auth.access; + } + if (!auth.refresh) + return undefined; + try { + const newTokens = await refreshAccessToken(auth.refresh, apiBaseURL); + // Preserve optional OAuth fields (v2 Auth / plugin may carry these). + const extras = auth; + // Use the new token even if persisting back to OpenCode fails. + await persistAuthBestEffort({ + type: "oauth", + access: newTokens.accessToken, + refresh: newTokens.refreshToken, + expires: decodeJwtExpiryMs(newTokens.accessToken) ?? Date.now(), + ...(extras.accountId !== undefined ? { accountId: extras.accountId } : {}), + ...(extras.enterpriseUrl !== undefined ? { enterpriseUrl: extras.enterpriseUrl } : {}), + }); + sessionAccessToken = newTokens.accessToken; + return newTokens.accessToken; + } + catch { + return undefined; + } + } + return undefined; + } + async function loadModels() { + const cached = await readCache(cacheDir); + if (cached?.models.length && isCacheFresh(cached)) { + return modelsToConfig(cached.models); + } + // Config runs before auth.loader and has no getAuth(); read the durable + // store (normally the same source getAuth() uses). Refresh missing, expired, + // or old-schema caches here so this process materializes the new model set. + const auth = await authFromStore(); + if (auth) { + const accessToken = await resolveAccessToken(auth); + if (accessToken) { + try { + const models = await discoverModels(accessToken, cacheDir, { baseURL: apiBaseURL }); + return modelsToConfig(models); + } + catch { + // No usable cache and discovery failed — leave the list empty. + } + } + } + // Preserve stale-on-failure/offline behavior for an existing cache. + return cached?.models.length ? modelsToConfig(cached.models) : {}; + } + return { + tool: { + // `websearch` is a reserved OpenCode id and is filtered for third-party + // providers after plugin tools are merged. Use the collision-safe id + // Cursor already sees so this host-side fallback survives that filter. + custom_websearch: classicTools.webSearch, + // Commits Cursor-generated image bytes, which cannot travel through the + // host's text `write`. Handle-only, so its presence in the catalog does + // not give any model a way to write arbitrary files — see image-save.ts. + cursor_image_save: classicTools.imageSave, + }, + async event({ event }) { + switch (event.type) { + case "session.created": + sessionActivity.linkSession(event.properties.info.id, event.properties.info.parentID); + sessionActivity.recordActivity(event.properties.info.id); + break; + case "session.updated": + sessionActivity.linkSession(event.properties.info.id, event.properties.info.parentID); + break; + case "session.deleted": + sessionActivity.removeSession(event.properties.info.id); + break; + case "message.updated": + sessionActivity.recordActivity(event.properties.info.sessionID); + break; + case "message.part.updated": + sessionActivity.recordActivity(event.properties.part.sessionID); + break; + } + await dispatchHostEventBridge({ + event, + client: input.client, + directory: input.directory, + serverUrl: input.serverUrl, + }); + }, + async "tool.execute.before"(hookInput, output) { + if (hookInput.tool !== "bash") + return; + // bash/zsh retain the original display/permission command and wrap via + // shell.env. sh/dash need a short wrapper-file command because their + // non-interactive `-c` path ignores BASH_ENV / ZDOTDIR. + prepareCursorShellArgs(hookInput.callID, output.args); + }, + async "shell.env"(hookInput, output) { + const env = cursorShellEnvForCall(hookInput.callID); + if (!env) + return; + Object.assign(output.env, env); + }, + async "tool.execute.after"(hookInput, output) { + if (hookInput.tool !== "bash") + return; + try { + output.title = cursorShellOriginalCommand(hookInput.callID) ?? output.title; + output.output = captureCursorShellResult(hookInput.callID, output.output, output.metadata); + // OpenCode's bash GUI falls back to metadata.output when output is empty + // (`props.output || props.metadata.output`), so strip private markers there too. + if (output.metadata && typeof output.metadata === "object") { + const metadata = output.metadata; + if (typeof metadata.output === "string") { + metadata.output = sanitizeRegisteredCursorShellOutput(hookInput.callID, metadata.output); + } + } + } + finally { + releaseCursorShellEnv(hookInput.callID); + } + }, + async "chat.params"(hookInput, output) { + if (hookInput.model.providerID !== CURSOR_PROVIDER_ID) + return; + // Agent changes can replace the host system prompt and mode contract. + // Carry the canonical OpenCode id so an incompatible Cursor checkpoint + // is rotated instead of resuming the prior agent's prompt. + output.options[CURSOR_HOST_AGENT_OPTION] = hookInput.agent; + // OpenCode's compaction pipeline invokes the LLM with agent="compaction". + // Carry that stable runtime fact into LanguageModelV3 providerOptions so + // the provider never has to guess from an empty tool list. + if (hookInput.agent === "compaction") { + output.options[CURSOR_COMPACTION_OPTION] = true; + } + }, + async config(cfg) { + setCursorShellPath(cfg.shell); + cfg.provider ??= {}; + const models = await loadModels(); + const existing = cfg.provider[CURSOR_PROVIDER_ID]; + if (existing) { + // Provider already declared (e.g. README stub with models: {}) — + // still inject the cached model list when the user hasn't filled it in. + const existingModels = existing.models; + if (!existingModels || Object.keys(existingModels).length === 0) { + ; + existing.models = models; + } + return; + } + cfg.provider[CURSOR_PROVIDER_ID] = { + name: "Cursor Integration", + npm: MODULE_URL, + models, + }; + }, + auth: { + provider: CURSOR_PROVIDER_ID, + methods: [ + { + type: "oauth", + label: "Cursor account (browser login)", + async authorize() { + const params = generatePkceParams(); + const challenge = await generatePkceChallenge(params.verifier); + const websiteUrl = process.env.CURSOR_WEBSITE_URL ?? `https://${CURSOR_WEBSITE_HOST}`; + const apiBaseUrl = process.env.CURSOR_API_BASE_URL ?? `https://${CURSOR_API_HOST}`; + const url = buildLoginUrl(challenge, params.uuid, websiteUrl); + return { + url, + instructions: "Open this URL in a browser to sign in to Cursor", + method: "auto", + async callback() { + const result = await pollForTokens(params.uuid, params.verifier, apiBaseUrl); + return { + type: "success", + provider: CURSOR_PROVIDER_ID, + access: result.accessToken, + refresh: result.refreshToken, + expires: decodeJwtExpiryMs(result.accessToken) ?? Date.now(), + }; + }, + }; + }, + }, + { + type: "api", + label: "API key (cursor.com/settings)", + prompts: [ + { + type: "text", + key: "apiKey", + message: "Cursor API key", + placeholder: "crsr_...", + validate(value) { + if (!value.startsWith("crsr_")) + return "API key should start with crsr_"; + return undefined; + }, + }, + ], + async authorize(inputs) { + const apiKey = inputs?.apiKey; + if (!apiKey) + return { type: "failed" }; + try { + const result = await exchangeApiKey(apiKey, apiBaseURL); + return { + type: "success", + key: result.accessToken, + provider: CURSOR_PROVIDER_ID, + metadata: { refreshToken: result.refreshToken }, + }; + } + catch { + return { type: "failed" }; + } + }, + }, + ], + async loader(getAuth) { + const auth = await authForLoader(getAuth); + // Prefer credentials from getAuth/store; if refresh already succeeded in + // loadModels but persist failed, fall back to the in-memory session token. + const accessToken = (auth ? await resolveAccessToken(auth) : undefined) ?? sessionAccessToken; + if (accessToken) { + // Skip when config already filled a fresh cache (avoids a second + // AvailableModels round-trip + background refresh on cold start). + const cached = await readCache(cacheDir); + if (!cached || cached.models.length === 0 || !isCacheFresh(cached)) { + // Await so an empty/missing cache is written before the loader returns + // (fire-and-forget often loses the race on short-lived CLI commands). + await discoverModels(accessToken, cacheDir, { baseURL: apiBaseURL }).catch(() => { }); + } + // Resolve the region-specific Run stream origin so the first turn + // does not spend time on GetServerConfig. Best-effort: a failure is + // surfaced by startSession, which can fail the actual model call with + // a clear endpoint-resolution error instead of using global fallback. + await resolveAgentUrl(accessToken, { + apiBaseURL, + telemetryEnabled: cursorGetServerConfigTelemetryEnabled(), + }).catch(() => { }); + } + return { + ...(accessToken ? { accessToken } : {}), + workspaceRoot: input.directory, + cacheDir, + }; + }, + }, + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.d.ts new file mode 100644 index 00000000..269ef566 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.d.ts @@ -0,0 +1,550 @@ +/** + * AUTO-GENERATED by scripts/generate-cursor-pricing.ts — do not edit by hand. + * Pricing source: https://cursor.com/docs/models-and-pricing.md + * Context source: https://cursor.com/docs.md + * Regenerate: bun run generate:pricing + */ +export declare const CURSOR_PRICING_SOURCE: "https://cursor.com/docs/models-and-pricing.md"; +export declare const CURSOR_CONTEXT_SOURCE: "https://cursor.com/docs.md"; +export declare const CURSOR_MODEL_COSTS: { + readonly "claude-fable-5": { + readonly input: 10; + readonly output: 50; + readonly cache_read: 1; + readonly cache_write: 12.5; + }; + readonly "claude-fable-5-1": { + readonly input: 10; + readonly output: 50; + readonly cache_read: 0.25; + readonly cache_write: 12.5; + }; + readonly "claude-haiku-4-5": { + readonly input: 1; + readonly output: 5; + readonly cache_read: 0.1; + readonly cache_write: 1.25; + }; + readonly "claude-opus-4-5": { + readonly input: 5; + readonly output: 25; + readonly cache_read: 0.5; + readonly cache_write: 6.25; + }; + readonly "claude-opus-4-6": { + readonly input: 5; + readonly output: 25; + readonly cache_read: 0.5; + readonly cache_write: 6.25; + }; + readonly "claude-opus-4-7": { + readonly input: 5; + readonly output: 25; + readonly cache_read: 0.5; + readonly cache_write: 6.25; + }; + readonly "claude-opus-4-8": { + readonly input: 5; + readonly output: 25; + readonly cache_read: 0.5; + readonly cache_write: 6.25; + }; + readonly "claude-opus-5": { + readonly input: 5; + readonly output: 25; + readonly cache_read: 0.5; + readonly cache_write: 6.25; + }; + readonly "claude-opus-5-5": { + readonly input: 4; + readonly output: 20; + readonly cache_read: 0.2; + readonly cache_write: 5; + }; + readonly "claude-sonnet-4": { + readonly input: 3; + readonly output: 15; + readonly cache_read: 0.3; + readonly cache_write: 3.75; + readonly context_over_200k: { + readonly input: 6; + readonly output: 22.5; + readonly cache_read: 0.6; + readonly cache_write: 7.5; + }; + }; + readonly "claude-sonnet-4-5": { + readonly input: 3; + readonly output: 15; + readonly cache_read: 0.3; + readonly cache_write: 3.75; + }; + readonly "claude-sonnet-4-6": { + readonly input: 3; + readonly output: 15; + readonly cache_read: 0.3; + readonly cache_write: 3.75; + }; + readonly "claude-sonnet-5": { + readonly input: 2; + readonly output: 10; + readonly cache_read: 0.2; + readonly cache_write: 2.5; + }; + readonly "composer-2.5": { + readonly input: 0.5; + readonly output: 2.5; + readonly cache_read: 0.2; + }; + readonly "composer-2.5-fast": { + readonly input: 3; + readonly output: 15; + readonly cache_read: 0.5; + }; + readonly "gemini-2.5-flash": { + readonly input: 0.3; + readonly output: 2.5; + readonly cache_read: 0.03; + }; + readonly "gemini-3-flash": { + readonly input: 0.5; + readonly output: 3; + readonly cache_read: 0.05; + }; + readonly "gemini-3.1-pro": { + readonly input: 2; + readonly output: 12; + readonly cache_read: 0.2; + }; + readonly "gemini-3.5-flash": { + readonly input: 1.5; + readonly output: 9; + readonly cache_read: 0.15; + }; + readonly "gemini-3.6-flash": { + readonly input: 1.5; + readonly output: 7.5; + readonly cache_read: 0.15; + }; + readonly "gemini-3.7-flash": { + readonly input: 0.75; + readonly output: 3.5; + readonly cache_read: 0.075; + }; + readonly "gemini-3.8-flash": { + readonly input: 0.75; + readonly output: 3.5; + readonly cache_read: 0.075; + }; + readonly "glm-5.2": { + readonly input: 1.4; + readonly output: 4.4; + readonly cache_read: 0.26; + }; + readonly "gpt-5-mini": { + readonly input: 0.25; + readonly output: 2; + readonly cache_read: 0.025; + }; + readonly "gpt-5.1": { + readonly input: 1.25; + readonly output: 10; + readonly cache_read: 0.125; + }; + readonly "gpt-5.2": { + readonly input: 1.75; + readonly output: 14; + readonly cache_read: 0.175; + }; + readonly "gpt-5.3-codex": { + readonly input: 1.75; + readonly output: 14; + readonly cache_read: 0.175; + }; + readonly "gpt-5.4": { + readonly input: 2.5; + readonly output: 15; + readonly cache_read: 0.25; + readonly context_over_200k: { + readonly input: 5; + readonly output: 15; + readonly cache_read: 0.5; + }; + }; + readonly "gpt-5.4-mini": { + readonly input: 0.75; + readonly output: 4.5; + readonly cache_read: 0.075; + }; + readonly "gpt-5.4-nano": { + readonly input: 0.2; + readonly output: 1.25; + readonly cache_read: 0.02; + }; + readonly "gpt-5.5": { + readonly input: 5; + readonly output: 30; + readonly cache_read: 0.5; + readonly context_over_200k: { + readonly input: 10; + readonly output: 30; + readonly cache_read: 1; + }; + }; + readonly "gpt-5.6-luna": { + readonly input: 0.2; + readonly output: 1.2; + readonly cache_read: 0.02; + readonly cache_write: 0.25; + readonly context_over_200k: { + readonly input: 0.4; + readonly output: 1.2; + readonly cache_read: 0.04; + readonly cache_write: 0.5; + }; + }; + readonly "gpt-5.6-sol": { + readonly input: 4; + readonly output: 20; + readonly cache_read: 0.4; + readonly cache_write: 5; + readonly context_over_200k: { + readonly input: 8; + readonly output: 20; + readonly cache_read: 0.8; + readonly cache_write: 10; + }; + }; + readonly "gpt-5.6-terra": { + readonly input: 2; + readonly output: 12; + readonly cache_read: 0.2; + readonly cache_write: 2.5; + readonly context_over_200k: { + readonly input: 4; + readonly output: 12; + readonly cache_read: 0.4; + readonly cache_write: 5; + }; + }; + readonly "grok-4.5": { + readonly input: 2; + readonly output: 6; + readonly cache_read: 0.5; + }; + readonly "grok-4.5-fast": { + readonly input: 4; + readonly output: 18; + readonly cache_read: 1; + }; + readonly "grok-4.6": { + readonly input: 2; + readonly output: 6; + readonly cache_read: 0.5; + }; + readonly "grok-4.6-fast": { + readonly input: 4; + readonly output: 12; + readonly cache_read: 1; + }; + readonly "grok-4.7": { + readonly input: 2; + readonly output: 6; + readonly cache_read: 0.5; + readonly context_over_200k: { + readonly input: 4; + readonly output: 12; + readonly cache_read: 1; + }; + }; + readonly "grok-4.7-fast": { + readonly input: 4; + readonly output: 12; + readonly cache_read: 1; + readonly context_over_200k: { + readonly input: 6; + readonly output: 18; + readonly cache_read: 1.5; + }; + }; + readonly "kimi-k2.7-code": { + readonly input: 0.95; + readonly output: 4; + readonly cache_read: 0.19; + }; + readonly "kimi-k3": { + readonly input: 3; + readonly output: 15; + readonly cache_read: 0.3; + }; + readonly "muse-spark-1.3": { + readonly input: 1.25; + readonly output: 4.25; + readonly cache_read: 0.15; + }; +}; +export declare const CURSOR_MODEL_CONTEXTS: { + readonly "claude-fable-5": { + readonly maxContext: 300000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-fable-5-1": { + readonly maxContext: 300000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-haiku-4-5": { + readonly maxContext: 200000; + }; + readonly "claude-opus-4-5": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 200000; + }; + readonly "claude-opus-4-6": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-opus-4-7": { + readonly maxContext: 300000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-opus-4-8": { + readonly maxContext: 300000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-opus-5": { + readonly maxContext: 300000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-opus-5-5": { + readonly maxContext: 300000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-sonnet-4": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-sonnet-4-5": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-sonnet-4-6": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "claude-sonnet-5": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "composer-2.5": { + readonly maxContext: 200000; + }; + readonly "gemini-2.5-flash": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gemini-3-flash": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gemini-3.1-pro": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gemini-3.5-flash": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gemini-3.6-flash": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gemini-3.7-flash": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gemini-3.8-flash": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "glm-5.2": { + readonly maxContext: 200000; + }; + readonly "gpt-5-mini": { + readonly maxContext: 272000; + }; + readonly "gpt-5.1": { + readonly maxContext: 272000; + }; + readonly "gpt-5.2": { + readonly maxContext: 272000; + }; + readonly "gpt-5.3-codex": { + readonly maxContext: 272000; + }; + readonly "gpt-5.4": { + readonly maxContext: 272000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gpt-5.4-mini": { + readonly maxContext: 272000; + }; + readonly "gpt-5.4-nano": { + readonly maxContext: 272000; + }; + readonly "gpt-5.5": { + readonly maxContext: 272000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gpt-5.6-luna": { + readonly maxContext: 272000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gpt-5.6-sol": { + readonly maxContext: 272000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "gpt-5.6-terra": { + readonly maxContext: 272000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "grok-4.5": { + readonly maxContext: 256000; + }; + readonly "grok-4.6": { + readonly maxContext: 256000; + }; + readonly "grok-4.7": { + readonly maxContext: 256000; + readonly maxContextForMaxMode: 500000; + }; + readonly "kimi-k2.7-code": { + readonly maxContext: 262000; + }; + readonly "kimi-k3": { + readonly maxContext: 200000; + readonly maxContextForMaxMode: 1000000; + }; + readonly "muse-spark-1.3": { + readonly maxContext: 300000; + readonly maxContextForMaxMode: 1000000; + }; +}; +export declare const CURSOR_MODEL_CAPABILITIES: { + readonly "claude-fable-5": { + readonly supportsImages: true; + }; + readonly "claude-fable-5-1": { + readonly supportsImages: true; + }; + readonly "claude-haiku-4-5": { + readonly supportsImages: true; + }; + readonly "claude-opus-4-5": { + readonly supportsImages: true; + }; + readonly "claude-opus-4-6": { + readonly supportsImages: true; + }; + readonly "claude-opus-4-7": { + readonly supportsImages: true; + }; + readonly "claude-opus-4-8": { + readonly supportsImages: true; + }; + readonly "claude-opus-5": { + readonly supportsImages: true; + }; + readonly "claude-opus-5-5": { + readonly supportsImages: true; + }; + readonly "claude-sonnet-4": { + readonly supportsImages: true; + }; + readonly "claude-sonnet-4-5": { + readonly supportsImages: true; + }; + readonly "claude-sonnet-4-6": { + readonly supportsImages: true; + }; + readonly "claude-sonnet-5": { + readonly supportsImages: true; + }; + readonly "composer-2.5": { + readonly supportsImages: true; + }; + readonly "gemini-2.5-flash": { + readonly supportsImages: true; + }; + readonly "gemini-3-flash": { + readonly supportsImages: true; + }; + readonly "gemini-3.1-pro": { + readonly supportsImages: true; + }; + readonly "gemini-3.5-flash": { + readonly supportsImages: true; + }; + readonly "gemini-3.6-flash": { + readonly supportsImages: true; + }; + readonly "gemini-3.7-flash": { + readonly supportsImages: true; + }; + readonly "gemini-3.8-flash": { + readonly supportsImages: true; + }; + readonly "glm-5.2": { + readonly supportsImages: false; + }; + readonly "gpt-5-mini": { + readonly supportsImages: true; + }; + readonly "gpt-5.1": { + readonly supportsImages: true; + }; + readonly "gpt-5.2": { + readonly supportsImages: true; + }; + readonly "gpt-5.3-codex": { + readonly supportsImages: true; + }; + readonly "gpt-5.4": { + readonly supportsImages: true; + }; + readonly "gpt-5.4-mini": { + readonly supportsImages: true; + }; + readonly "gpt-5.4-nano": { + readonly supportsImages: true; + }; + readonly "gpt-5.5": { + readonly supportsImages: true; + }; + readonly "gpt-5.6-luna": { + readonly supportsImages: true; + }; + readonly "gpt-5.6-sol": { + readonly supportsImages: true; + }; + readonly "gpt-5.6-terra": { + readonly supportsImages: true; + }; + readonly "grok-4.5": { + readonly supportsImages: false; + }; + readonly "grok-4.6": { + readonly supportsImages: false; + }; + readonly "grok-4.7": { + readonly supportsImages: false; + }; + readonly "kimi-k2.7-code": { + readonly supportsImages: true; + }; + readonly "kimi-k3": { + readonly supportsImages: true; + }; + readonly "muse-spark-1.3": { + readonly supportsImages: true; + }; +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.js b/references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.js new file mode 100644 index 00000000..c35094bf --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.js @@ -0,0 +1,550 @@ +/** + * AUTO-GENERATED by scripts/generate-cursor-pricing.ts — do not edit by hand. + * Pricing source: https://cursor.com/docs/models-and-pricing.md + * Context source: https://cursor.com/docs.md + * Regenerate: bun run generate:pricing + */ +export const CURSOR_PRICING_SOURCE = "https://cursor.com/docs/models-and-pricing.md"; +export const CURSOR_CONTEXT_SOURCE = "https://cursor.com/docs.md"; +export const CURSOR_MODEL_COSTS = { + "claude-fable-5": { + "input": 10, + "output": 50, + "cache_read": 1, + "cache_write": 12.5 + }, + "claude-fable-5-1": { + "input": 10, + "output": 50, + "cache_read": 0.25, + "cache_write": 12.5 + }, + "claude-haiku-4-5": { + "input": 1, + "output": 5, + "cache_read": 0.1, + "cache_write": 1.25 + }, + "claude-opus-4-5": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "claude-opus-4-6": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "claude-opus-4-7": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "claude-opus-4-8": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "claude-opus-5": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "claude-opus-5-5": { + "input": 4, + "output": 20, + "cache_read": 0.2, + "cache_write": 5 + }, + "claude-sonnet-4": { + "input": 3, + "output": 15, + "cache_read": 0.3, + "cache_write": 3.75, + "context_over_200k": { + "input": 6, + "output": 22.5, + "cache_read": 0.6, + "cache_write": 7.5 + } + }, + "claude-sonnet-4-5": { + "input": 3, + "output": 15, + "cache_read": 0.3, + "cache_write": 3.75 + }, + "claude-sonnet-4-6": { + "input": 3, + "output": 15, + "cache_read": 0.3, + "cache_write": 3.75 + }, + "claude-sonnet-5": { + "input": 2, + "output": 10, + "cache_read": 0.2, + "cache_write": 2.5 + }, + "composer-2.5": { + "input": 0.5, + "output": 2.5, + "cache_read": 0.2 + }, + "composer-2.5-fast": { + "input": 3, + "output": 15, + "cache_read": 0.5 + }, + "gemini-2.5-flash": { + "input": 0.3, + "output": 2.5, + "cache_read": 0.03 + }, + "gemini-3-flash": { + "input": 0.5, + "output": 3, + "cache_read": 0.05 + }, + "gemini-3.1-pro": { + "input": 2, + "output": 12, + "cache_read": 0.2 + }, + "gemini-3.5-flash": { + "input": 1.5, + "output": 9, + "cache_read": 0.15 + }, + "gemini-3.6-flash": { + "input": 1.5, + "output": 7.5, + "cache_read": 0.15 + }, + "gemini-3.7-flash": { + "input": 0.75, + "output": 3.5, + "cache_read": 0.075 + }, + "gemini-3.8-flash": { + "input": 0.75, + "output": 3.5, + "cache_read": 0.075 + }, + "glm-5.2": { + "input": 1.4, + "output": 4.4, + "cache_read": 0.26 + }, + "gpt-5-mini": { + "input": 0.25, + "output": 2, + "cache_read": 0.025 + }, + "gpt-5.1": { + "input": 1.25, + "output": 10, + "cache_read": 0.125 + }, + "gpt-5.2": { + "input": 1.75, + "output": 14, + "cache_read": 0.175 + }, + "gpt-5.3-codex": { + "input": 1.75, + "output": 14, + "cache_read": 0.175 + }, + "gpt-5.4": { + "input": 2.5, + "output": 15, + "cache_read": 0.25, + "context_over_200k": { + "input": 5, + "output": 15, + "cache_read": 0.5 + } + }, + "gpt-5.4-mini": { + "input": 0.75, + "output": 4.5, + "cache_read": 0.075 + }, + "gpt-5.4-nano": { + "input": 0.2, + "output": 1.25, + "cache_read": 0.02 + }, + "gpt-5.5": { + "input": 5, + "output": 30, + "cache_read": 0.5, + "context_over_200k": { + "input": 10, + "output": 30, + "cache_read": 1 + } + }, + "gpt-5.6-luna": { + "input": 0.2, + "output": 1.2, + "cache_read": 0.02, + "cache_write": 0.25, + "context_over_200k": { + "input": 0.4, + "output": 1.2, + "cache_read": 0.04, + "cache_write": 0.5 + } + }, + "gpt-5.6-sol": { + "input": 4, + "output": 20, + "cache_read": 0.4, + "cache_write": 5, + "context_over_200k": { + "input": 8, + "output": 20, + "cache_read": 0.8, + "cache_write": 10 + } + }, + "gpt-5.6-terra": { + "input": 2, + "output": 12, + "cache_read": 0.2, + "cache_write": 2.5, + "context_over_200k": { + "input": 4, + "output": 12, + "cache_read": 0.4, + "cache_write": 5 + } + }, + "grok-4.5": { + "input": 2, + "output": 6, + "cache_read": 0.5 + }, + "grok-4.5-fast": { + "input": 4, + "output": 18, + "cache_read": 1 + }, + "grok-4.6": { + "input": 2, + "output": 6, + "cache_read": 0.5 + }, + "grok-4.6-fast": { + "input": 4, + "output": 12, + "cache_read": 1 + }, + "grok-4.7": { + "input": 2, + "output": 6, + "cache_read": 0.5, + "context_over_200k": { + "input": 4, + "output": 12, + "cache_read": 1 + } + }, + "grok-4.7-fast": { + "input": 4, + "output": 12, + "cache_read": 1, + "context_over_200k": { + "input": 6, + "output": 18, + "cache_read": 1.5 + } + }, + "kimi-k2.7-code": { + "input": 0.95, + "output": 4, + "cache_read": 0.19 + }, + "kimi-k3": { + "input": 3, + "output": 15, + "cache_read": 0.3 + }, + "muse-spark-1.3": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } +}; +export const CURSOR_MODEL_CONTEXTS = { + "claude-fable-5": { + "maxContext": 300000, + "maxContextForMaxMode": 1000000 + }, + "claude-fable-5-1": { + "maxContext": 300000, + "maxContextForMaxMode": 1000000 + }, + "claude-haiku-4-5": { + "maxContext": 200000 + }, + "claude-opus-4-5": { + "maxContext": 200000, + "maxContextForMaxMode": 200000 + }, + "claude-opus-4-6": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "claude-opus-4-7": { + "maxContext": 300000, + "maxContextForMaxMode": 1000000 + }, + "claude-opus-4-8": { + "maxContext": 300000, + "maxContextForMaxMode": 1000000 + }, + "claude-opus-5": { + "maxContext": 300000, + "maxContextForMaxMode": 1000000 + }, + "claude-opus-5-5": { + "maxContext": 300000, + "maxContextForMaxMode": 1000000 + }, + "claude-sonnet-4": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "claude-sonnet-4-5": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "claude-sonnet-4-6": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "claude-sonnet-5": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "composer-2.5": { + "maxContext": 200000 + }, + "gemini-2.5-flash": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "gemini-3-flash": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "gemini-3.1-pro": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "gemini-3.5-flash": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "gemini-3.6-flash": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "gemini-3.7-flash": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "gemini-3.8-flash": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "glm-5.2": { + "maxContext": 200000 + }, + "gpt-5-mini": { + "maxContext": 272000 + }, + "gpt-5.1": { + "maxContext": 272000 + }, + "gpt-5.2": { + "maxContext": 272000 + }, + "gpt-5.3-codex": { + "maxContext": 272000 + }, + "gpt-5.4": { + "maxContext": 272000, + "maxContextForMaxMode": 1000000 + }, + "gpt-5.4-mini": { + "maxContext": 272000 + }, + "gpt-5.4-nano": { + "maxContext": 272000 + }, + "gpt-5.5": { + "maxContext": 272000, + "maxContextForMaxMode": 1000000 + }, + "gpt-5.6-luna": { + "maxContext": 272000, + "maxContextForMaxMode": 1000000 + }, + "gpt-5.6-sol": { + "maxContext": 272000, + "maxContextForMaxMode": 1000000 + }, + "gpt-5.6-terra": { + "maxContext": 272000, + "maxContextForMaxMode": 1000000 + }, + "grok-4.5": { + "maxContext": 256000 + }, + "grok-4.6": { + "maxContext": 256000 + }, + "grok-4.7": { + "maxContext": 256000, + "maxContextForMaxMode": 500000 + }, + "kimi-k2.7-code": { + "maxContext": 262000 + }, + "kimi-k3": { + "maxContext": 200000, + "maxContextForMaxMode": 1000000 + }, + "muse-spark-1.3": { + "maxContext": 300000, + "maxContextForMaxMode": 1000000 + } +}; +export const CURSOR_MODEL_CAPABILITIES = { + "claude-fable-5": { + "supportsImages": true + }, + "claude-fable-5-1": { + "supportsImages": true + }, + "claude-haiku-4-5": { + "supportsImages": true + }, + "claude-opus-4-5": { + "supportsImages": true + }, + "claude-opus-4-6": { + "supportsImages": true + }, + "claude-opus-4-7": { + "supportsImages": true + }, + "claude-opus-4-8": { + "supportsImages": true + }, + "claude-opus-5": { + "supportsImages": true + }, + "claude-opus-5-5": { + "supportsImages": true + }, + "claude-sonnet-4": { + "supportsImages": true + }, + "claude-sonnet-4-5": { + "supportsImages": true + }, + "claude-sonnet-4-6": { + "supportsImages": true + }, + "claude-sonnet-5": { + "supportsImages": true + }, + "composer-2.5": { + "supportsImages": true + }, + "gemini-2.5-flash": { + "supportsImages": true + }, + "gemini-3-flash": { + "supportsImages": true + }, + "gemini-3.1-pro": { + "supportsImages": true + }, + "gemini-3.5-flash": { + "supportsImages": true + }, + "gemini-3.6-flash": { + "supportsImages": true + }, + "gemini-3.7-flash": { + "supportsImages": true + }, + "gemini-3.8-flash": { + "supportsImages": true + }, + "glm-5.2": { + "supportsImages": false + }, + "gpt-5-mini": { + "supportsImages": true + }, + "gpt-5.1": { + "supportsImages": true + }, + "gpt-5.2": { + "supportsImages": true + }, + "gpt-5.3-codex": { + "supportsImages": true + }, + "gpt-5.4": { + "supportsImages": true + }, + "gpt-5.4-mini": { + "supportsImages": true + }, + "gpt-5.4-nano": { + "supportsImages": true + }, + "gpt-5.5": { + "supportsImages": true + }, + "gpt-5.6-luna": { + "supportsImages": true + }, + "gpt-5.6-sol": { + "supportsImages": true + }, + "gpt-5.6-terra": { + "supportsImages": true + }, + "grok-4.5": { + "supportsImages": false + }, + "grok-4.6": { + "supportsImages": false + }, + "grok-4.7": { + "supportsImages": false + }, + "kimi-k2.7-code": { + "supportsImages": true + }, + "kimi-k3": { + "supportsImages": true + }, + "muse-spark-1.3": { + "supportsImages": true + } +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/pricing.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/pricing.d.ts new file mode 100644 index 00000000..e693970e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/pricing.d.ts @@ -0,0 +1,68 @@ +/** + * Cursor model token pricing for OpenCode cost reporting. + * + * Rates come from Cursor's public docs (Cursor Models + Other Models tables). + * Generated data lives in `pricing-data.ts`; regenerate with + * `bun run generate:pricing`. Unknown / unpublished models get no `cost`. + */ +/** Classic OpenCode / models.dev cost shape used in provider config. */ +export type OpenCodeModelCost = { + input: number; + output: number; + cache_read?: number; + cache_write?: number; + context_over_200k?: OpenCodeModelCost; +}; +/** OpenCode 2.0 `Model.Info.cost` entry (array of tiered rates). */ +export type OpenCode2ModelCost = { + tier?: { + type: "context"; + size: number; + }; + input: number; + output: number; + cache: { + read: number; + write: number; + }; +}; +export type CursorPricingCoverage = { + priced: string[]; + missing: string[]; +}; +/** + * Models we expose that Cursor does not currently publish numeric rates for + * (Auto is billed at the routed model's list price). + */ +export declare const CURSOR_UNPRICED_MODEL_IDS: readonly ["default"]; +export declare function validateOpenCodeModelCost(value: unknown, path?: string): { + valid: boolean; + errors: string[]; +}; +export declare function isOpenCodeModelCost(value: unknown): value is OpenCodeModelCost; +/** Strip synthetic OpenCode suffixes (`…-1m`, `…-1m-2`, `…-1m-fast`, …). */ +export declare function wireModelIdForPricing(modelId: string): { + baseId: string; + longContextEntry: boolean; +}; +/** + * True when Cursor publishes a distinct Fast rate for this wire model id. + * Catalog code uses this to split Fast variants into a `-fast` entry. + */ +export declare function hasCursorFastPricing(modelId: string): boolean; +/** + * Look up classic OpenCode cost for a catalog / wire model id. + * Synthetic `-1m` entries retain both base and documented long-context rates. + * Synthetic `-fast` entries are stored as their own keys. + */ +export declare function getCursorModelCost(modelId: string): OpenCodeModelCost | undefined; +export declare function applyCursorModelCost>(modelId: string, entry: T): T & { + cost?: OpenCodeModelCost; +}; +/** Convert classic cost → OpenCode 2.0 `Model.Info.cost` array. */ +export declare function toOpenCode2Costs(cost: OpenCodeModelCost | undefined): OpenCode2ModelCost[]; +/** + * Coverage helper for CI / maintainer checks. + * Intentionally unpriced first-party ids are counted as priced (covered). + */ +export declare function checkCursorPricingCoverage(modelIds: string[]): CursorPricingCoverage; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/pricing.js b/references/upstream-plugins/cursor-opencode-provider/dist/pricing.js new file mode 100644 index 00000000..9904f71c --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/pricing.js @@ -0,0 +1,136 @@ +/** + * Cursor model token pricing for OpenCode cost reporting. + * + * Rates come from Cursor's public docs (Cursor Models + Other Models tables). + * Generated data lives in `pricing-data.ts`; regenerate with + * `bun run generate:pricing`. Unknown / unpublished models get no `cost`. + */ +import { CURSOR_MODEL_COSTS } from "./pricing-data.js"; +/** + * Models we expose that Cursor does not currently publish numeric rates for + * (Auto is billed at the routed model's list price). + */ +export const CURSOR_UNPRICED_MODEL_IDS = ["default"]; +const UNPRICED = new Set(CURSOR_UNPRICED_MODEL_IDS); +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function isNonNegativeFiniteNumber(value) { + return typeof value === "number" && Number.isFinite(value) && value >= 0; +} +function collectCostValidationErrors(value, path, errors) { + if (!isRecord(value)) { + errors.push(`${path} must be an object`); + return; + } + if (!isNonNegativeFiniteNumber(value.input)) { + errors.push(`${path}.input must be a non-negative finite number`); + } + if (!isNonNegativeFiniteNumber(value.output)) { + errors.push(`${path}.output must be a non-negative finite number`); + } + if (value.cache_read !== undefined && !isNonNegativeFiniteNumber(value.cache_read)) { + errors.push(`${path}.cache_read must be a non-negative finite number`); + } + if (value.cache_write !== undefined && !isNonNegativeFiniteNumber(value.cache_write)) { + errors.push(`${path}.cache_write must be a non-negative finite number`); + } + if (value.context_over_200k !== undefined) { + collectCostValidationErrors(value.context_over_200k, `${path}.context_over_200k`, errors); + } +} +export function validateOpenCodeModelCost(value, path = "cost") { + const errors = []; + collectCostValidationErrors(value, path, errors); + return { valid: errors.length === 0, errors }; +} +export function isOpenCodeModelCost(value) { + return validateOpenCodeModelCost(value).valid; +} +/** Strip synthetic OpenCode suffixes (`…-1m`, `…-1m-2`, `…-1m-fast`, …). */ +export function wireModelIdForPricing(modelId) { + const match = /^(.*)-1m(?:-\d+)?(-fast)?$/.exec(modelId); + if (match) { + return { baseId: `${match[1]}${match[2] ?? ""}`, longContextEntry: true }; + } + return { baseId: modelId, longContextEntry: false }; +} +/** + * True when Cursor publishes a distinct Fast rate for this wire model id. + * Catalog code uses this to split Fast variants into a `-fast` entry. + */ +export function hasCursorFastPricing(modelId) { + return Object.prototype.hasOwnProperty.call(CURSOR_MODEL_COSTS, `${modelId}-fast`); +} +function asMutableCost(value) { + const copy = { input: value.input, output: value.output }; + if (value.cache_read !== undefined) + copy.cache_read = value.cache_read; + if (value.cache_write !== undefined) + copy.cache_write = value.cache_write; + if (value.context_over_200k) { + copy.context_over_200k = asMutableCost(value.context_over_200k); + } + return copy; +} +/** + * Look up classic OpenCode cost for a catalog / wire model id. + * Synthetic `-1m` entries retain both base and documented long-context rates. + * Synthetic `-fast` entries are stored as their own keys. + */ +export function getCursorModelCost(modelId) { + const { baseId } = wireModelIdForPricing(modelId); + const found = CURSOR_MODEL_COSTS[baseId]; + if (!found) + return undefined; + return asMutableCost(found); +} +export function applyCursorModelCost(modelId, entry) { + const modelCost = getCursorModelCost(modelId); + if (!modelCost) + return entry; + return { ...entry, cost: modelCost }; +} +/** Convert classic cost → OpenCode 2.0 `Model.Info.cost` array. */ +export function toOpenCode2Costs(cost) { + if (!cost) + return []; + const out = [ + { + input: cost.input, + output: cost.output, + cache: { + read: cost.cache_read ?? 0, + write: cost.cache_write ?? 0, + }, + }, + ]; + if (cost.context_over_200k) { + out.push({ + tier: { type: "context", size: 200_000 }, + input: cost.context_over_200k.input, + output: cost.context_over_200k.output, + cache: { + read: cost.context_over_200k.cache_read ?? 0, + write: cost.context_over_200k.cache_write ?? 0, + }, + }); + } + return out; +} +/** + * Coverage helper for CI / maintainer checks. + * Intentionally unpriced first-party ids are counted as priced (covered). + */ +export function checkCursorPricingCoverage(modelIds) { + const priced = []; + const missing = []; + for (const modelId of modelIds) { + const { baseId } = wireModelIdForPricing(modelId); + if (UNPRICED.has(baseId) || getCursorModelCost(modelId)) + priced.push(modelId); + else + missing.push(modelId); + } + return { priced, missing }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/apply-patch.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/apply-patch.d.ts new file mode 100644 index 00000000..a1729658 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/apply-patch.d.ts @@ -0,0 +1,109 @@ +/** + * Synthesis of OpenCode `apply_patch` envelopes from Cursor's native write/edit + * exec requests. + * + * ## Why this exists + * + * OpenCode 1.x does not merely *add* `apply_patch` for GPT models — it removes + * `edit` and `write` from the catalog in exchange. From + * `packages/opencode/src/tool/registry.ts` (`ToolRegistry.tools`): + * + * ```ts + * // use apply tool in same format as codex + * const usePatch = + * input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4") + * if (tool.id === ApplyPatchTool.id) return usePatch + * if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch + * ``` + * + * The predicate runs on the *API* model id, so every Cursor `gpt-5*` model + * matches (and, as a quirk of substring matching, `gpt-3.5-*` does too while + * `gpt-4o` does not). + * + * Upstream context, in case this ever needs revisiting: + * + * - Introduced by opencode PR #9127 ("apply_patch tool for openai models"). The + * intent is to hand GPT/Codex models the tool surface they were post-trained + * on; the same commit tells the codex system prompt to prefer `apply_patch`. + * - The *exclusivity* is deliberate. On opencode issue #34491 a maintainer + * describes the gate as a "band-aid", "intentionally minimal", whose purpose + * is that "GPT no longer receives edit + write + apply_patch simultaneously", + * and notes that a plugin-side opt-out "isn't feasible today". + * - opencode issue #19942 reported exactly this hazard — model-conditional tool + * substitution makes model-agnostic integrations impossible — and was closed + * `not_planned` by a stale bot. + * - opencode issue #35408 (open, milestone 2.0) proposes a + * `ctx.session.context(...)` hook exposing a mutable tool roster, and lists + * "Select `apply_patch` versus `edit` + `write`" as work to move out of the + * registry. Once that lands, a host-side opt-out replaces this shim. + * - OpenCode 2.0 (`packages/core`) has **no** such gate: `apply_patch`, `edit` + * and `write` are all registered unconditionally in `tool/builtins.ts`. This + * translation is therefore 1.x-only in practice, but it is keyed off the + * advertised set rather than a version check, so it simply never fires on 2.0. + * + * ## Why translation, not advertisement + * + * `toolsToDescriptors` forwards every advertised host tool to Cursor verbatim, + * so `opencode-apply_patch` already appears in Cursor's catalog whenever + * OpenCode advertises it, and an `mcp_args` call naming it already executes. + * What breaks is Cursor's *native* write/edit channel (`write_args`, + * `pi_edit_args`), which this provider resolves to the fixed host names + * `write`/`edit`. + * + * Translating on our side is the only option: Cursor's agent protocol has no + * patch-format edit tool at all (its `ToolCall` oneof offers whole-file + * `edit_tool_call`/`write_args` and substring `pi_edit_tool_call`/`pi_edit_args`; + * `apply_agent_diff_tool_call` is unrelated — it applies a cloud agent's diff by + * `agent_id`). Cursor's CLI also contains no model-conditional branching + * whatsoever, and `ModelDetails` carries no tool-capability flags, so the same + * native requests arrive for every model. Those names are absent from + * the catalog, so the request is refused before it reaches OpenCode. + * + * ## Format notes (verified against `packages/opencode/src/patch/index.ts`) + * + * - `*** Add File:` **overwrites an existing file** — the tool's `add` case uses + * `oldContent = ""` and never stats the target (upstream test: "adds file + * overwriting existing file"). A whole-file write therefore needs no diff and + * no existence check. + * - An empty `@@` header is a valid chunk start: the parser stores + * `change_context: contextLine || undefined`, and a missing context simply + * means "match `old_lines` from the current scan position". + * - Every payload line is prefixed with `+`, `-`, or a space, and the parser + * only breaks on *unprefixed* `***` / `@@`. Content lines that themselves + * begin with `***` or `@@` therefore survive a round trip unharmed. + * - `Add File` content is rejoined with `\n` and normalized to exactly one + * trailing newline. A write of content with no trailing newline gains one. + * - Update chunks match **whole lines** via `seekSequence`, unlike OpenCode + * `edit`, which is substring-based. Substring edits must be expanded to line + * boundaries before they can be expressed as a patch — see `planSubstringEdit`. + * - Renames are rejected by OpenCode 2.0's implementation ("apply_patch moves + * are not supported yet"), so `*** Move to:` is never emitted here. + */ +export declare const APPLY_PATCH_TOOL = "apply_patch"; +/** One `@@` chunk: replace `oldLines` with `newLines`. */ +export type UpdateChunk = { + oldLines: string[]; + newLines: string[]; +}; +/** + * Whole-file write as `*** Add File:`. Valid whether or not the target exists — + * OpenCode's add case overwrites unconditionally. + */ +export declare function buildAddFilePatch(filePath: string, content: string): string; +/** Targeted replacement as `*** Update File:` with one `@@` chunk per change. */ +export declare function buildUpdateFilePatch(filePath: string, chunks: UpdateChunk[]): string; +export type SubstringEditPlan = { + ok: true; + chunks: UpdateChunk[]; +} | { + ok: false; + reason: string; +}; +/** + * Expand a substring replacement to the whole lines it touches, because + * `apply_patch` matches line sequences rather than substrings. + * + * Refuses rather than guessing: an absent match, or an ambiguous one without + * `replaceAll`, would otherwise be silently applied to the wrong region. + */ +export declare function planSubstringEdit(source: string, oldString: string, newString: string, replaceAll?: boolean): SubstringEditPlan; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/apply-patch.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/apply-patch.js new file mode 100644 index 00000000..49d5dd87 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/apply-patch.js @@ -0,0 +1,163 @@ +/** + * Synthesis of OpenCode `apply_patch` envelopes from Cursor's native write/edit + * exec requests. + * + * ## Why this exists + * + * OpenCode 1.x does not merely *add* `apply_patch` for GPT models — it removes + * `edit` and `write` from the catalog in exchange. From + * `packages/opencode/src/tool/registry.ts` (`ToolRegistry.tools`): + * + * ```ts + * // use apply tool in same format as codex + * const usePatch = + * input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4") + * if (tool.id === ApplyPatchTool.id) return usePatch + * if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch + * ``` + * + * The predicate runs on the *API* model id, so every Cursor `gpt-5*` model + * matches (and, as a quirk of substring matching, `gpt-3.5-*` does too while + * `gpt-4o` does not). + * + * Upstream context, in case this ever needs revisiting: + * + * - Introduced by opencode PR #9127 ("apply_patch tool for openai models"). The + * intent is to hand GPT/Codex models the tool surface they were post-trained + * on; the same commit tells the codex system prompt to prefer `apply_patch`. + * - The *exclusivity* is deliberate. On opencode issue #34491 a maintainer + * describes the gate as a "band-aid", "intentionally minimal", whose purpose + * is that "GPT no longer receives edit + write + apply_patch simultaneously", + * and notes that a plugin-side opt-out "isn't feasible today". + * - opencode issue #19942 reported exactly this hazard — model-conditional tool + * substitution makes model-agnostic integrations impossible — and was closed + * `not_planned` by a stale bot. + * - opencode issue #35408 (open, milestone 2.0) proposes a + * `ctx.session.context(...)` hook exposing a mutable tool roster, and lists + * "Select `apply_patch` versus `edit` + `write`" as work to move out of the + * registry. Once that lands, a host-side opt-out replaces this shim. + * - OpenCode 2.0 (`packages/core`) has **no** such gate: `apply_patch`, `edit` + * and `write` are all registered unconditionally in `tool/builtins.ts`. This + * translation is therefore 1.x-only in practice, but it is keyed off the + * advertised set rather than a version check, so it simply never fires on 2.0. + * + * ## Why translation, not advertisement + * + * `toolsToDescriptors` forwards every advertised host tool to Cursor verbatim, + * so `opencode-apply_patch` already appears in Cursor's catalog whenever + * OpenCode advertises it, and an `mcp_args` call naming it already executes. + * What breaks is Cursor's *native* write/edit channel (`write_args`, + * `pi_edit_args`), which this provider resolves to the fixed host names + * `write`/`edit`. + * + * Translating on our side is the only option: Cursor's agent protocol has no + * patch-format edit tool at all (its `ToolCall` oneof offers whole-file + * `edit_tool_call`/`write_args` and substring `pi_edit_tool_call`/`pi_edit_args`; + * `apply_agent_diff_tool_call` is unrelated — it applies a cloud agent's diff by + * `agent_id`). Cursor's CLI also contains no model-conditional branching + * whatsoever, and `ModelDetails` carries no tool-capability flags, so the same + * native requests arrive for every model. Those names are absent from + * the catalog, so the request is refused before it reaches OpenCode. + * + * ## Format notes (verified against `packages/opencode/src/patch/index.ts`) + * + * - `*** Add File:` **overwrites an existing file** — the tool's `add` case uses + * `oldContent = ""` and never stats the target (upstream test: "adds file + * overwriting existing file"). A whole-file write therefore needs no diff and + * no existence check. + * - An empty `@@` header is a valid chunk start: the parser stores + * `change_context: contextLine || undefined`, and a missing context simply + * means "match `old_lines` from the current scan position". + * - Every payload line is prefixed with `+`, `-`, or a space, and the parser + * only breaks on *unprefixed* `***` / `@@`. Content lines that themselves + * begin with `***` or `@@` therefore survive a round trip unharmed. + * - `Add File` content is rejoined with `\n` and normalized to exactly one + * trailing newline. A write of content with no trailing newline gains one. + * - Update chunks match **whole lines** via `seekSequence`, unlike OpenCode + * `edit`, which is substring-based. Substring edits must be expanded to line + * boundaries before they can be expressed as a patch — see `planSubstringEdit`. + * - Renames are rejected by OpenCode 2.0's implementation ("apply_patch moves + * are not supported yet"), so `*** Move to:` is never emitted here. + */ +export const APPLY_PATCH_TOOL = "apply_patch"; +const BEGIN = "*** Begin Patch"; +const END = "*** End Patch"; +/** + * Whole-file write as `*** Add File:`. Valid whether or not the target exists — + * OpenCode's add case overwrites unconditionally. + */ +export function buildAddFilePatch(filePath, content) { + const lines = splitLines(content); + return [BEGIN, `*** Add File: ${filePath}`, ...lines.map((line) => `+${line}`), END].join("\n"); +} +/** Targeted replacement as `*** Update File:` with one `@@` chunk per change. */ +export function buildUpdateFilePatch(filePath, chunks) { + const body = []; + for (const chunk of chunks) { + body.push("@@"); + for (const line of chunk.oldLines) + body.push(`-${line}`); + for (const line of chunk.newLines) + body.push(`+${line}`); + } + return [BEGIN, `*** Update File: ${filePath}`, ...body, END].join("\n"); +} +/** + * Expand a substring replacement to the whole lines it touches, because + * `apply_patch` matches line sequences rather than substrings. + * + * Refuses rather than guessing: an absent match, or an ambiguous one without + * `replaceAll`, would otherwise be silently applied to the wrong region. + */ +export function planSubstringEdit(source, oldString, newString, replaceAll = false) { + if (oldString === "") + return { ok: false, reason: "the text to replace is empty" }; + const offsets = []; + for (let at = source.indexOf(oldString); at !== -1; at = source.indexOf(oldString, at + oldString.length)) { + offsets.push(at); + } + if (offsets.length === 0) + return { ok: false, reason: "the text to replace was not found in the file" }; + if (offsets.length > 1 && !replaceAll) { + return { + ok: false, + reason: `the text to replace appears ${offsets.length} times; it must be unique`, + }; + } + const targets = replaceAll ? offsets : offsets.slice(0, 1); + const chunks = []; + let previousEnd = -1; + for (const offset of targets) { + const start = lineStart(source, offset); + const end = lineEnd(source, offset + oldString.length); + // Overlapping expansions would emit the same original lines twice, and the + // second chunk could never match after the first replaced them. + if (start < previousEnd) { + return { ok: false, reason: "overlapping replacements cannot be expressed as a patch" }; + } + previousEnd = end; + const before = source.slice(start, offset); + const after = source.slice(offset + oldString.length, end); + chunks.push({ + oldLines: splitLines(source.slice(start, end)), + newLines: splitLines(before + newString + after), + }); + } + return { ok: true, chunks }; +} +/** + * Split into patch payload lines. A trailing newline is dropped so it is not + * emitted as a spurious empty line — OpenCode re-adds exactly one on apply. + */ +function splitLines(text) { + const normalized = text.endsWith("\n") ? text.slice(0, -1) : text; + return normalized.split("\n"); +} +function lineStart(source, offset) { + const at = source.lastIndexOf("\n", offset - 1); + return at === -1 ? 0 : at + 1; +} +function lineEnd(source, offset) { + const at = source.indexOf("\n", offset); + return at === -1 ? source.length : at; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/ask-question.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/ask-question.d.ts new file mode 100644 index 00000000..4d594839 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/ask-question.d.ts @@ -0,0 +1,132 @@ +/** + * Cursor-native AskQuestion ⇄ OpenCode `question` translation. + * + * Cursor's AskQuestion is not a tool the client executes — the server raises an + * `InteractionQuery` (`ask_question_interaction_query`, field 3) and blocks the + * Run until the client answers with an `AskQuestionResult`. OpenCode has an + * equivalent host tool (`question`), so the provider translates rather than + * refusing: refusing made models narrate "the AskQuestion tool is unavailable" + * and fall back to prose instead of ever calling the advertised host tool. + * + * Every rule below mirrors Cursor CLI, decompiled at + * `/cursor/cli{,-local}`: + * + * - `src/utils/interaction-utils.ts` — `BU` (freeform option id), `Q7` (display + * options), `iX` (selection → success), `N$` (rejection + default reason). + * - `chunk-7076/dist/ui.js` (`askQuestionInteractionQuery` case) and + * `subagent/subagent-prompt-handler.js` (`requestAskQuestion`) — the + * sync/async split and the exact rejection reasons. + * + * Keep the reason strings byte-identical to the CLI's: they reach the model as + * tool feedback, and Cursor's server-side prompting is tuned against them. + */ +/** Cursor's synthetic "type your own answer" option id (interaction-utils `BU`). */ +export declare const FREEFORM_OPTION_ID = "__freeform_other__"; +/** + * `PendingExec.resultField` marking a held-open AskQuestion. It is not an + * `ExecClientMessage` field: continuation dispatches on it to write an + * InteractionResponse or a ConversationAction instead of an exec result. + */ +export declare const ASK_QUESTION_RESULT_FIELD = "ask_question_interaction_response"; +/** Default rejection reason when the user skips (interaction-utils `N$`). */ +export declare const SKIPPED_REASON = "Questions skipped by user"; +/** Rejection used when the host dismisses the prompt (CLI `resolveAskQuestionPrompt`). */ +export declare const DISMISSED_REASON = "Ask-question prompt dismissed"; +export declare const MISSING_QUERY_REASON = "Missing ask-question query"; +export declare const MISSING_ARGS_REASON = "Missing ask-question arguments"; +export type CursorAskQuestionOption = { + id: string; + label: string; +}; +export type CursorAskQuestionItem = { + id: string; + prompt: string; + options: CursorAskQuestionOption[]; + allowMultiple: boolean; +}; +export type CursorAskQuestionArgs = { + title: string; + questions: CursorAskQuestionItem[]; + runAsync: boolean; +}; +export type DecodedAskQuestionQuery = { + args: CursorAskQuestionArgs; + /** + * The exact `AskQuestionArgs` sub-message bytes. Async completion echoes + * `original_args` verbatim so fields this schema does not model survive. + */ + rawArgs: Uint8Array; + toolCallId: string; +}; +/** `AskQuestionResult` oneof, shaped for `encodeMessage`. */ +export type AskQuestionResultMessage = Record; +/** + * Decode an `ask_question_interaction_query` body. Returns undefined when the + * body carries no usable question set — the caller then rejects with the CLI's + * own "missing" reason rather than bridging an empty prompt to the host. + */ +export declare function decodeAskQuestionQuery(queryBytes: Uint8Array): DecodedAskQuestionQuery | undefined; +/** + * Cursor CLI drops a trailing catch-all option before rendering, because it + * always appends its own freeform "Other" row (interaction-utils `Q7`). + * OpenCode's `question` tool does the same thing via `custom` (default true, + * and not settable from the tool schema), so the strip is required here too or + * the user sees two "Other" entries for one question. + */ +export declare function isCatchAllOptionLabel(label: string): boolean; +/** Options as Cursor would display them: trailing catch-all removed. */ +export declare function displayOptions(question: CursorAskQuestionItem): CursorAskQuestionOption[]; +export type OpencodeQuestionInput = { + questions: Array<{ + question: string; + header: string; + options: Array<{ + label: string; + description: string; + }>; + multiple?: boolean; + }>; +}; +/** + * Build the OpenCode `question` tool input. `Question.Prompt` requires + * `question`, `header`, and `options[{label, description}]`; Cursor options + * carry no description, so it stays empty rather than being invented. + */ +export declare function askQuestionToolInput(args: CursorAskQuestionArgs): OpencodeQuestionInput; +/** `AskQuestionResult{rejected}` — interaction-utils `N$`. */ +export declare function rejectedResult(reason?: string): AskQuestionResultMessage; +/** `AskQuestionResult{async}` — the immediate reply for `run_async` queries. */ +export declare function asyncResult(): AskQuestionResultMessage; +/** + * Pull each question's answer text out of a host `question` tool output. + * + * OpenCode returns prose (`metadata.answers` does not cross the AI SDK + * boundary): + * + * User has answered your questions: ""="", ""="Unanswered". You + * can now continue with the user's answers in mind. + * + * The prose is located by its `""="` anchor — robust + * against commas, quotes and `"="` inside question or answer text, and against + * duplicate questions. Returns undefined for a question whose anchor is + * absent, which is treated as unanswered rather than guessed at. + */ +export declare function parseAnswerSegments(questions: readonly CursorAskQuestionItem[], output: string): Array; +/** + * Map one question's answer labels back onto Cursor option ids. + * + * OpenCode returns labels, so a label that matches an option we sent becomes a + * `selected_option_id`; anything else is the user's custom answer and becomes + * `freeform_text`. Cursor CLI's `iX` substitutes the literal "Other" when the + * freeform row was chosen without text, so an empty custom answer does too. + */ +export declare function answerForQuestion(question: CursorAskQuestionItem, segment: string | undefined): Record; +/** + * Translate an OpenCode `question` tool result into an `AskQuestionResult`. + * + * A tool error means the host dismissed or failed the prompt — OpenCode's + * `Question.RejectedError` is the dismissal path — so it maps to the CLI's + * rejection rather than an empty success, which the model would read as "the + * user answered nothing". + */ +export declare function askQuestionResultFromToolOutput(args: CursorAskQuestionArgs, output: string, isError: boolean): AskQuestionResultMessage; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/ask-question.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/ask-question.js new file mode 100644 index 00000000..2816d434 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/ask-question.js @@ -0,0 +1,319 @@ +/** + * Cursor-native AskQuestion ⇄ OpenCode `question` translation. + * + * Cursor's AskQuestion is not a tool the client executes — the server raises an + * `InteractionQuery` (`ask_question_interaction_query`, field 3) and blocks the + * Run until the client answers with an `AskQuestionResult`. OpenCode has an + * equivalent host tool (`question`), so the provider translates rather than + * refusing: refusing made models narrate "the AskQuestion tool is unavailable" + * and fall back to prose instead of ever calling the advertised host tool. + * + * Every rule below mirrors Cursor CLI, decompiled at + * `/cursor/cli{,-local}`: + * + * - `src/utils/interaction-utils.ts` — `BU` (freeform option id), `Q7` (display + * options), `iX` (selection → success), `N$` (rejection + default reason). + * - `chunk-7076/dist/ui.js` (`askQuestionInteractionQuery` case) and + * `subagent/subagent-prompt-handler.js` (`requestAskQuestion`) — the + * sync/async split and the exact rejection reasons. + * + * Keep the reason strings byte-identical to the CLI's: they reach the model as + * tool feedback, and Cursor's server-side prompting is tuned against them. + */ +import { decodeMessageSparse } from "./messages.js"; +/** Cursor's synthetic "type your own answer" option id (interaction-utils `BU`). */ +export const FREEFORM_OPTION_ID = "__freeform_other__"; +/** + * `PendingExec.resultField` marking a held-open AskQuestion. It is not an + * `ExecClientMessage` field: continuation dispatches on it to write an + * InteractionResponse or a ConversationAction instead of an exec result. + */ +export const ASK_QUESTION_RESULT_FIELD = "ask_question_interaction_response"; +/** Default rejection reason when the user skips (interaction-utils `N$`). */ +export const SKIPPED_REASON = "Questions skipped by user"; +/** Rejection used when the host dismisses the prompt (CLI `resolveAskQuestionPrompt`). */ +export const DISMISSED_REASON = "Ask-question prompt dismissed"; +export const MISSING_QUERY_REASON = "Missing ask-question query"; +export const MISSING_ARGS_REASON = "Missing ask-question arguments"; +/** OpenCode's `question` tool caps its short header label at 30 chars. */ +const HEADER_MAX_LENGTH = 30; +function asRecord(value) { + return value && typeof value === "object" && !Array.isArray(value) + ? value + : undefined; +} +function str(value) { + return typeof value === "string" ? value : ""; +} +/** + * Extract the raw bytes of `AskQuestionInteractionQuery.args` (#1) without + * decoding. protobufjs hands back a decoded copy that would silently drop + * unknown fields on re-encode; the async completion action must echo the + * server's own bytes. + */ +function extractArgsBytes(queryBytes) { + let offset = 0; + while (offset < queryBytes.length) { + let key = 0; + let shift = 0; + while (offset < queryBytes.length) { + const byte = queryBytes[offset++]; + key |= (byte & 0x7f) << shift; + if ((byte & 0x80) === 0) + break; + shift += 7; + if (shift > 28) + return undefined; + } + const fieldNumber = key >>> 3; + const wireType = key & 7; + if (wireType === 2) { + let length = 0; + let lengthShift = 0; + while (offset < queryBytes.length) { + const byte = queryBytes[offset++]; + length |= (byte & 0x7f) << lengthShift; + if ((byte & 0x80) === 0) + break; + lengthShift += 7; + if (lengthShift > 28) + return undefined; + } + if (offset + length > queryBytes.length) + return undefined; + if (fieldNumber === 1) + return queryBytes.subarray(offset, offset + length); + offset += length; + } + else if (wireType === 0) { + while (offset < queryBytes.length && (queryBytes[offset++] & 0x80) !== 0) { + // skip varint continuation bytes + } + } + else if (wireType === 5) { + offset += 4; + } + else if (wireType === 1) { + offset += 8; + } + else { + return undefined; + } + } + return undefined; +} +/** + * Decode an `ask_question_interaction_query` body. Returns undefined when the + * body carries no usable question set — the caller then rejects with the CLI's + * own "missing" reason rather than bridging an empty prompt to the host. + */ +export function decodeAskQuestionQuery(queryBytes) { + let decoded; + try { + decoded = decodeMessageSparse("AskQuestionInteractionQuery", queryBytes); + } + catch { + return undefined; + } + const rawArgs = extractArgsBytes(queryBytes); + const argsRecord = asRecord(decoded.args); + if (!argsRecord || !rawArgs) + return undefined; + const questions = Array.isArray(argsRecord.questions) + ? argsRecord.questions.flatMap((raw) => { + const item = asRecord(raw); + if (!item) + return []; + const prompt = str(item.prompt); + if (!prompt) + return []; + const options = Array.isArray(item.options) + ? item.options.flatMap((rawOption) => { + const option = asRecord(rawOption); + if (!option) + return []; + const label = str(option.label); + if (!label) + return []; + return [{ id: str(option.id), label }]; + }) + : []; + return [{ + id: str(item.id), + prompt, + options, + allowMultiple: item.allow_multiple === true, + }]; + }) + : []; + if (questions.length === 0) + return undefined; + return { + args: { + title: str(argsRecord.title), + questions, + runAsync: argsRecord.run_async === true, + }, + rawArgs, + toolCallId: str(decoded.tool_call_id), + }; +} +/** + * Cursor CLI drops a trailing catch-all option before rendering, because it + * always appends its own freeform "Other" row (interaction-utils `Q7`). + * OpenCode's `question` tool does the same thing via `custom` (default true, + * and not settable from the tool schema), so the strip is required here too or + * the user sees two "Other" entries for one question. + */ +export function isCatchAllOptionLabel(label) { + const normalized = label.toLowerCase().trim(); + return (normalized === "other" + || normalized === "something else" + || normalized.startsWith("other:") + || normalized.startsWith("other -") + || normalized.startsWith("other (") + || normalized.startsWith("something else:") + || normalized.startsWith("something else -") + || normalized.startsWith("something else (")); +} +/** Options as Cursor would display them: trailing catch-all removed. */ +export function displayOptions(question) { + const last = question.options.at(-1); + if (last && isCatchAllOptionLabel(last.label)) + return question.options.slice(0, -1); + return question.options.slice(); +} +function header(title) { + const trimmed = title.trim(); + if (!trimmed) + return "Question"; + return trimmed.length > HEADER_MAX_LENGTH + ? trimmed.slice(0, HEADER_MAX_LENGTH - 1).trimEnd() + "…" + : trimmed; +} +/** + * Build the OpenCode `question` tool input. `Question.Prompt` requires + * `question`, `header`, and `options[{label, description}]`; Cursor options + * carry no description, so it stays empty rather than being invented. + */ +export function askQuestionToolInput(args) { + return { + questions: args.questions.map((question) => ({ + question: question.prompt, + header: header(args.title), + options: displayOptions(question).map((option) => ({ + label: option.label, + description: "", + })), + ...(question.allowMultiple ? { multiple: true } : {}), + })), + }; +} +/** `AskQuestionResult{rejected}` — interaction-utils `N$`. */ +export function rejectedResult(reason) { + return { rejected: { reason: reason ?? SKIPPED_REASON } }; +} +/** `AskQuestionResult{async}` — the immediate reply for `run_async` queries. */ +export function asyncResult() { + return { async: {} }; +} +/** + * Pull each question's answer text out of a host `question` tool output. + * + * OpenCode returns prose (`metadata.answers` does not cross the AI SDK + * boundary): + * + * User has answered your questions: ""="", ""="Unanswered". You + * can now continue with the user's answers in mind. + * + * The prose is located by its `""="` anchor — robust + * against commas, quotes and `"="` inside question or answer text, and against + * duplicate questions. Returns undefined for a question whose anchor is + * absent, which is treated as unanswered rather than guessed at. + */ +export function parseAnswerSegments(questions, output) { + const answers = []; + let cursor = 0; + for (const question of questions) { + const anchor = `"${question.prompt}"="`; + const start = output.indexOf(anchor, cursor); + if (start < 0) { + answers.push(undefined); + continue; + } + const valueStart = start + anchor.length; + // The value ends at the last quote before the next anchor (or the trailing + // `. You can now continue…`). Searching backwards from the next anchor + // keeps quoted text inside an answer intact. + const nextAnchorStart = questions + .map((other) => output.indexOf(`"${other.prompt}"="`, valueStart)) + .filter((index) => index > valueStart) + .reduce((min, index) => (min < 0 || index < min ? index : min), -1); + const searchEnd = nextAnchorStart >= 0 ? nextAnchorStart : output.length; + const valueEnd = output.lastIndexOf('"', searchEnd - 1); + if (valueEnd < valueStart) { + answers.push(undefined); + continue; + } + answers.push(output.slice(valueStart, valueEnd)); + cursor = valueEnd; + } + return answers; +} +/** OpenCode's placeholder for a question the user left blank. */ +const UNANSWERED = "Unanswered"; +/** + * Map one question's answer labels back onto Cursor option ids. + * + * OpenCode returns labels, so a label that matches an option we sent becomes a + * `selected_option_id`; anything else is the user's custom answer and becomes + * `freeform_text`. Cursor CLI's `iX` substitutes the literal "Other" when the + * freeform row was chosen without text, so an empty custom answer does too. + */ +export function answerForQuestion(question, segment) { + const answer = { question_id: question.id }; + if (segment === undefined || segment.trim().length === 0 || segment.trim() === UNANSWERED) { + return answer; + } + const byLabel = new Map(displayOptions(question).map((option) => [option.label.trim().toLowerCase(), option.id])); + const selected = []; + const custom = []; + // `multiple: true` answers arrive joined with ", " (question tool `formatted`). + for (const piece of segment.split(", ")) { + const label = piece.trim(); + if (!label) + continue; + const optionId = byLabel.get(label.toLowerCase()); + if (optionId !== undefined) + selected.push(optionId); + else + custom.push(label); + } + if (selected.length > 0) + answer.selected_option_ids = selected; + if (custom.length > 0) + answer.freeform_text = custom.join(", "); + else if (selected.length === 0) + answer.freeform_text = "Other"; + return answer; +} +/** + * Translate an OpenCode `question` tool result into an `AskQuestionResult`. + * + * A tool error means the host dismissed or failed the prompt — OpenCode's + * `Question.RejectedError` is the dismissal path — so it maps to the CLI's + * rejection rather than an empty success, which the model would read as "the + * user answered nothing". + */ +export function askQuestionResultFromToolOutput(args, output, isError) { + if (isError) { + const reason = output.trim(); + return rejectedResult(reason.length > 0 ? reason : DISMISSED_REASON); + } + const segments = parseAnswerSegments(args.questions, output); + return { + success: { + answers: args.questions.map((question, index) => answerForQuestion(question, segments[index])), + }, + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-reachability.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-reachability.d.ts new file mode 100644 index 00000000..fc51e5d8 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-reachability.d.ts @@ -0,0 +1,15 @@ +type ResolveBlob = (id: Uint8Array) => Uint8Array | undefined; +/** + * Return the blob ids reachable from Cursor's latest ConversationStateStructure. + * + * This mirrors Cursor CLI's conversation-export traversal: turns lead to their + * user message and steps, shell turns lead to command/output, state-level + * summaries/todos/prompts are direct refs, and subagent states recurse. Unknown + * protobuf fields are deliberately ignored for forward compatibility. + * + * `resolveBlob` must also resolve Cursor's content-as-id values. Throwing on a + * missing hash or malformed referenced message makes the caller retain its full + * snapshot rather than risk publishing an incomplete restart graph. + */ +export declare function collectReachableConversationBlobIds(checkpoint: Uint8Array, resolveBlob: ResolveBlob): Set; +export {}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-reachability.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-reachability.js new file mode 100644 index 00000000..5554bf9e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-reachability.js @@ -0,0 +1,140 @@ +import protobuf from "protobufjs"; +const MAX_NESTED_STATE_DEPTH = 64; +function hex(bytes) { + let result = ""; + for (let index = 0; index < bytes.length; index++) { + result += bytes[index].toString(16).padStart(2, "0"); + } + return result; +} +/** Read only length-delimited fields while still validating the complete protobuf. */ +function bytesFields(data) { + const reader = protobuf.Reader.create(data); + const fields = []; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const number = tag >>> 3; + const wireType = tag & 7; + if (number === 0) + throw new Error("invalid protobuf field zero"); + if (wireType === 2) + fields.push({ number, bytes: reader.bytes() }); + else + reader.skipType(wireType); + } + return fields; +} +function mapValue(entry) { + return bytesFields(entry).find((field) => field.number === 2)?.bytes; +} +/** + * Return the blob ids reachable from Cursor's latest ConversationStateStructure. + * + * This mirrors Cursor CLI's conversation-export traversal: turns lead to their + * user message and steps, shell turns lead to command/output, state-level + * summaries/todos/prompts are direct refs, and subagent states recurse. Unknown + * protobuf fields are deliberately ignored for forward compatibility. + * + * `resolveBlob` must also resolve Cursor's content-as-id values. Throwing on a + * missing hash or malformed referenced message makes the caller retain its full + * snapshot rather than risk publishing an incomplete restart graph. + */ +export function collectReachableConversationBlobIds(checkpoint, resolveBlob) { + const reachable = new Set(); + const reference = (id) => { + if (id.length === 0) + return undefined; + reachable.add(hex(id)); + const value = resolveBlob(id); + if (!value) + throw new Error(`missing referenced blob ${hex(id)}`); + return value; + }; + const walkUserMessage = (data) => { + const selectedContext = bytesFields(data).find((field) => field.number === 3)?.bytes; + if (!selectedContext) + return; + for (const imageField of bytesFields(selectedContext)) { + if (imageField.number !== 1) + continue; + for (const imagePart of bytesFields(imageField.bytes)) { + if (imagePart.number === 1) { + reference(imagePart.bytes); + } + else if (imagePart.number === 9) { + const blobId = bytesFields(imagePart.bytes).find((field) => field.number === 1)?.bytes; + if (blobId) + reference(blobId); + } + } + } + }; + const walkTurn = (data) => { + for (const turnField of bytesFields(data)) { + if (turnField.number === 1) { + for (const agentField of bytesFields(turnField.bytes)) { + if (agentField.number === 1) { + const userMessage = reference(agentField.bytes); + if (userMessage) + walkUserMessage(userMessage); + } + else if (agentField.number === 2) { + reference(agentField.bytes); + } + } + return; + } + if (turnField.number === 2) { + for (const shellField of bytesFields(turnField.bytes)) { + if (shellField.number === 1 || shellField.number === 2) { + reference(shellField.bytes); + } + } + return; + } + } + }; + const walkPersistedSubagent = (data, depth) => { + const nestedState = bytesFields(data).find((field) => field.number === 1)?.bytes; + if (nestedState) + walkState(nestedState, depth + 1); + }; + const walkState = (data, depth) => { + if (depth > MAX_NESTED_STATE_DEPTH) + throw new Error("conversation state nesting is too deep"); + for (const field of bytesFields(data)) { + switch (field.number) { + case 8: { + const turn = reference(field.bytes); + if (turn) + walkTurn(turn); + break; + } + case 1: // root_prompt_messages_json + case 3: // todos + case 6: // summary + case 7: // plan + case 13: // summary_archives + reference(field.bytes); + break; + case 16: { // map subagent_states + const persisted = mapValue(field.bytes); + if (persisted) + walkPersistedSubagent(persisted, depth); + break; + } + case 31: { // map subagent_state_refs + const subagentId = mapValue(field.bytes); + if (!subagentId) + break; + const persisted = reference(subagentId); + if (persisted) + walkPersistedSubagent(persisted, depth); + break; + } + } + } + }; + walkState(checkpoint, 0); + return reachable; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-store.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-store.d.ts new file mode 100644 index 00000000..8172bed1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-store.d.ts @@ -0,0 +1,54 @@ +/** + * Cursor CLI keeps conversation blobs in a durable client store (SQLite) across + * Runs. ConversationStateStructure only holds blob IDs; the server re-fetches + * them via get_blob on the next turn. Our per-Run session.blobs Map was wiped + * on stream close, so follow-up gets echoed 32-byte hashes → server JSON.parse + * fails ("Unexpected token ... is not valid JSON"). + * + * This store is keyed by conversation_id and survives across Run streams. + * At a successful TurnEnded it is compacted to the graph reachable from the + * latest checkpoint, matching Cursor CLI's conversation-export traversal. + */ +export declare function setConversationBlob(conversationId: string, blobId: Uint8Array, blobData: Uint8Array): string; +export declare function getConversationBlob(conversationId: string, blobId: Uint8Array): Uint8Array | undefined; +export declare function conversationBlobCount(conversationId: string): number; +export type ConversationBlobSnapshot = { + id: string; + data: Uint8Array; +}; +export type ConversationBlobCompaction = { + blobs: ConversationBlobSnapshot[]; + beforeCount: number; + beforeBytes: number; + afterCount: number; + afterBytes: number; + compacted: boolean; + fallbackReason?: string; +}; +export type ConversationBlobGraphStats = { + count: number; + bytes: number; + complete: boolean; + fallbackReason?: string; +}; +/** + * Measure the checkpoint-reachable blob graph without mutating the store. + * Incomplete graphs conservatively report the complete retained bucket, since + * that is what the KV channel may have to serve if the checkpoint is used. + */ +export declare function inspectConversationBlobGraph(conversationId: string, checkpoint: Uint8Array | undefined): ConversationBlobGraphStats; +/** Copy all durable blobs so a completed turn can be persisted atomically. */ +export declare function snapshotConversationBlobs(conversationId: string): ConversationBlobSnapshot[]; +/** + * Keep only blobs reachable from the latest checkpoint and snapshot them. + * Malformed/unknown state or a missing referenced hash falls back to retaining + * every blob: cache size must never come at the cost of restart correctness. + */ +export declare function compactConversationBlobs(conversationId: string, checkpoint: Uint8Array | undefined): ConversationBlobCompaction; +/** Replace the in-memory blob bucket when restoring a persisted conversation. */ +export declare function restoreConversationBlobs(conversationId: string, blobs: readonly ConversationBlobSnapshot[]): void; +/** Drop all blobs for a conversation (compaction conversation reset). */ +export declare function clearConversationBlobs(conversationId: string): void; +/** SHA-256 content hashes are 32 non-text bytes — never echo those as content. */ +export declare function isBlobIdHash(blobId: Uint8Array): boolean; +export declare function resetConversationBlobsForTests(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-store.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-store.js new file mode 100644 index 00000000..e276fd3a --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/blob-store.js @@ -0,0 +1,171 @@ +/** + * Cursor CLI keeps conversation blobs in a durable client store (SQLite) across + * Runs. ConversationStateStructure only holds blob IDs; the server re-fetches + * them via get_blob on the next turn. Our per-Run session.blobs Map was wiped + * on stream close, so follow-up gets echoed 32-byte hashes → server JSON.parse + * fails ("Unexpected token ... is not valid JSON"). + * + * This store is keyed by conversation_id and survives across Run streams. + * At a successful TurnEnded it is compacted to the graph reachable from the + * latest checkpoint, matching Cursor CLI's conversation-export traversal. + */ +import { collectReachableConversationBlobIds } from "./blob-reachability.js"; +function hex(b) { + let s = ""; + for (let i = 0; i < b.length; i++) + s += b[i].toString(16).padStart(2, "0"); + return s; +} +const byConversation = new Map(); +function retainedBlobStats(current) { + let bytes = 0; + for (const data of current.values()) + bytes += data.length; + return { count: current.size, bytes }; +} +function bucket(conversationId) { + let m = byConversation.get(conversationId); + if (!m) { + m = new Map(); + byConversation.set(conversationId, m); + } + return m; +} +export function setConversationBlob(conversationId, blobId, blobData) { + const key = hex(blobId); + // Copy so decode buffers can't mutate the store later. + bucket(conversationId).set(key, Uint8Array.from(blobData)); + return key; +} +export function getConversationBlob(conversationId, blobId) { + return bucket(conversationId).get(hex(blobId)); +} +export function conversationBlobCount(conversationId) { + return byConversation.get(conversationId)?.size ?? 0; +} +/** + * Measure the checkpoint-reachable blob graph without mutating the store. + * Incomplete graphs conservatively report the complete retained bucket, since + * that is what the KV channel may have to serve if the checkpoint is used. + */ +export function inspectConversationBlobGraph(conversationId, checkpoint) { + const current = byConversation.get(conversationId) ?? new Map(); + const retainedStats = (fallbackReason) => ({ + ...retainedBlobStats(current), + complete: fallbackReason === undefined, + ...(fallbackReason ? { fallbackReason } : {}), + }); + if (!checkpoint?.length) + return retainedStats("checkpoint unavailable"); + try { + const reachable = collectReachableConversationBlobIds(checkpoint, (id) => { + const stored = current.get(hex(id)); + if (stored) + return stored; + return isBlobIdHash(id) ? undefined : id; + }); + let count = 0; + let bytes = 0; + for (const id of reachable) { + const data = current.get(id); + if (!data) + continue; + count++; + bytes += data.length; + } + return { count, bytes, complete: true }; + } + catch (error) { + return retainedStats(error instanceof Error ? error.message : String(error)); + } +} +/** Copy all durable blobs so a completed turn can be persisted atomically. */ +export function snapshotConversationBlobs(conversationId) { + return [...(byConversation.get(conversationId) ?? [])].map(([id, data]) => ({ + id, + data: Uint8Array.from(data), + })); +} +/** + * Keep only blobs reachable from the latest checkpoint and snapshot them. + * Malformed/unknown state or a missing referenced hash falls back to retaining + * every blob: cache size must never come at the cost of restart correctness. + */ +export function compactConversationBlobs(conversationId, checkpoint) { + const current = byConversation.get(conversationId) ?? new Map(); + const before = retainedBlobStats(current); + const beforeCount = before.count; + const beforeBytes = before.bytes; + const fallback = (reason) => ({ + blobs: snapshotConversationBlobs(conversationId), + beforeCount, + beforeBytes, + afterCount: beforeCount, + afterBytes: beforeBytes, + compacted: false, + fallbackReason: reason, + }); + if (!checkpoint?.length) + return fallback("checkpoint unavailable"); + try { + const reachable = collectReachableConversationBlobIds(checkpoint, (id) => { + const stored = current.get(hex(id)); + if (stored) + return stored; + // Cursor occasionally uses short literal content as its own id. + return isBlobIdHash(id) ? undefined : id; + }); + const retained = new Map([...current].filter(([id]) => reachable.has(id))); + if (retained.size > 0) + byConversation.set(conversationId, retained); + else + byConversation.delete(conversationId); + const afterBytes = [...retained.values()].reduce((total, data) => total + data.length, 0); + return { + blobs: snapshotConversationBlobs(conversationId), + beforeCount, + beforeBytes, + afterCount: retained.size, + afterBytes, + compacted: true, + }; + } + catch (error) { + return fallback(error instanceof Error ? error.message : String(error)); + } +} +/** Replace the in-memory blob bucket when restoring a persisted conversation. */ +export function restoreConversationBlobs(conversationId, blobs) { + if (!conversationId) + return; + const restored = new Map(); + for (const blob of blobs) { + if (!/^(?:[0-9a-f]{2})+$/i.test(blob.id)) + continue; + restored.set(blob.id.toLowerCase(), Uint8Array.from(blob.data)); + } + if (restored.size > 0) + byConversation.set(conversationId, restored); + else + byConversation.delete(conversationId); +} +/** Drop all blobs for a conversation (compaction conversation reset). */ +export function clearConversationBlobs(conversationId) { + byConversation.delete(conversationId); +} +/** SHA-256 content hashes are 32 non-text bytes — never echo those as content. */ +export function isBlobIdHash(blobId) { + if (blobId.length !== 32) + return false; + // Content-as-id payloads can also be 32 bytes (short JSON). Real hashes are + // binary; if every byte is printable ASCII/UTF-8 text, treat as content. + for (let i = 0; i < blobId.length; i++) { + const b = blobId[i]; + if (b < 0x09 || (b > 0x0d && b < 0x20) || b > 0x7e) + return true; + } + return false; +} +export function resetConversationBlobsForTests() { + byConversation.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checkpoint.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checkpoint.d.ts new file mode 100644 index 00000000..5e583cf3 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checkpoint.d.ts @@ -0,0 +1,17 @@ +/** + * Cursor CLI parity: the server emits `conversation_checkpoint_update` after + * (and during) a turn. The CLI replaces its local ConversationStateStructure + * and re-sends that structure as `AgentRunRequest.conversation_state` on the + * next Run. We mirror that with an in-process store keyed by conversation_id. + * + * Checkpoints are kept as opaque protobuf bytes — CLI's structure uses blob-id + * fields (repeated bytes), not the seed JSON strings we invent on turn 1. + */ +/** Replace the stored checkpoint for a conversation (CLI handleCheckpoint). */ +export declare function setCheckpoint(conversationId: string, bytes: Uint8Array): void; +/** Last checkpoint for this conversation, if any. */ +export declare function getCheckpoint(conversationId: string): Uint8Array | undefined; +/** Drop a conversation's checkpoint (tests / explicit reset). */ +export declare function clearCheckpoint(conversationId: string): void; +/** Test helper — wipe all stored checkpoints. */ +export declare function resetCheckpointsForTests(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checkpoint.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checkpoint.js new file mode 100644 index 00000000..5fe22f9a --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checkpoint.js @@ -0,0 +1,29 @@ +/** + * Cursor CLI parity: the server emits `conversation_checkpoint_update` after + * (and during) a turn. The CLI replaces its local ConversationStateStructure + * and re-sends that structure as `AgentRunRequest.conversation_state` on the + * next Run. We mirror that with an in-process store keyed by conversation_id. + * + * Checkpoints are kept as opaque protobuf bytes — CLI's structure uses blob-id + * fields (repeated bytes), not the seed JSON strings we invent on turn 1. + */ +const byConversationId = new Map(); +/** Replace the stored checkpoint for a conversation (CLI handleCheckpoint). */ +export function setCheckpoint(conversationId, bytes) { + if (!conversationId || bytes.length === 0) + return; + // Copy so later mutations of the decode buffer can't corrupt the store. + byConversationId.set(conversationId, Uint8Array.from(bytes)); +} +/** Last checkpoint for this conversation, if any. */ +export function getCheckpoint(conversationId) { + return byConversationId.get(conversationId); +} +/** Drop a conversation's checkpoint (tests / explicit reset). */ +export function clearCheckpoint(conversationId) { + byConversationId.delete(conversationId); +} +/** Test helper — wipe all stored checkpoints. */ +export function resetCheckpointsForTests() { + byConversationId.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checksum.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checksum.d.ts new file mode 100644 index 00000000..c0f3e485 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checksum.d.ts @@ -0,0 +1,2 @@ +export declare function obfuscate(bytes: Uint8Array): Uint8Array; +export declare function createCursorChecksumHeader(machineId: string, macMachineId?: string): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checksum.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checksum.js new file mode 100644 index 00000000..9caf5180 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/checksum.js @@ -0,0 +1,23 @@ +export function obfuscate(bytes) { + const out = new Uint8Array(bytes); + let a = 165; + for (let i = 0; i < out.length; i++) { + out[i] = ((out[i] ^ a) + i) & 0xff; + a = out[i]; + } + return out; +} +export function createCursorChecksumHeader(machineId, macMachineId) { + const n = Math.floor(Date.now() / 1e6); + const ts = new Uint8Array([ + (n >> 40) & 0xff, + (n >> 32) & 0xff, + (n >> 24) & 0xff, + (n >> 16) & 0xff, + (n >> 8) & 0xff, + n & 0xff, + ]); + const obfuscated = obfuscate(ts); + const prefix = btoa(String.fromCharCode(...obfuscated)).replace(/=+$/, ""); + return macMachineId ? `${prefix}${machineId}/${macMachineId}` : `${prefix}${machineId}`; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/client-version.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/client-version.d.ts new file mode 100644 index 00000000..b5d5195f --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/client-version.d.ts @@ -0,0 +1,5 @@ +export declare function resetClientVersionCache(): void; +export declare function resolveClientVersion(): Promise; +export declare function cursorAgentVersionsDir(): string | undefined; +export declare function discoverLocalVersion(dir?: string | undefined): string | undefined; +export declare function extractVersionFromInstaller(script: string): string | undefined; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/client-version.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/client-version.js new file mode 100644 index 00000000..032cd6b4 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/client-version.js @@ -0,0 +1,137 @@ +import fs from "node:fs"; +import path from "node:path"; +import { FALLBACK_CLIENT_VERSION, MODEL_CACHE_TTL_MS, VERSION_CACHE_FILE, } from "../shared.js"; +import { opencodeGlobalCacheDir } from "../context/paths.js"; +import { withAbortDeadline } from "../deadline.js"; +const INSTALL_URL = "https://cursor.com/install"; +const REMOTE_TIMEOUT_MS = 5_000; +const BUILD_RE = /^\d{4}\.\d{2}\.\d{2}-[0-9A-Za-z][0-9A-Za-z.-]*$/; +const CLIENT_VERSION_RE = /^cli-[0-9A-Za-z][0-9A-Za-z._-]*$/; +let cachedResolution; +export function resetClientVersionCache() { + cachedResolution = undefined; +} +export function resolveClientVersion() { + cachedResolution ??= resolve(); + return cachedResolution; +} +async function resolve() { + const env = process.env.CURSOR_CLIENT_VERSION?.trim(); + if (isClientVersion(env)) + return env; + const local = discoverLocalVersion(); + if (local) + return local; + return (await resolveRemoteVersion()) ?? FALLBACK_CLIENT_VERSION; +} +function isClientVersion(value) { + return typeof value === "string" && CLIENT_VERSION_RE.test(value); +} +export function cursorAgentVersionsDir() { + const home = process.env.HOME || process.env.USERPROFILE; + if (!home) + return undefined; + switch (process.platform) { + case "linux": + case "darwin": + return path.join(home, ".local", "share", "cursor-agent", "versions"); + default: + return undefined; + } +} +export function discoverLocalVersion(dir = cursorAgentVersionsDir()) { + if (!dir) + return undefined; + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } + catch { + return undefined; + } + let newest; + for (const entry of entries) { + if (!entry.isDirectory() || !BUILD_RE.test(entry.name)) + continue; + try { + const mtimeMs = fs.statSync(path.join(dir, entry.name)).mtimeMs; + if (!newest || + mtimeMs > newest.mtimeMs || + (mtimeMs === newest.mtimeMs && entry.name > newest.name)) { + newest = { name: entry.name, mtimeMs }; + } + } + catch { + // Directory disappeared during discovery. + } + } + return newest ? `cli-${newest.name}` : undefined; +} +export function extractVersionFromInstaller(script) { + const match = script.match(/downloads\.cursor\.com\/lab\/([^/"'\s]+)\//); + return match && BUILD_RE.test(match[1]) ? match[1] : undefined; +} +function versionCachePath() { + return path.join(opencodeGlobalCacheDir(), VERSION_CACHE_FILE); +} +function readVersionCache() { + const file = versionCachePath(); + try { + const value = JSON.parse(fs.readFileSync(file, "utf8")); + if (!isClientVersion(value.version) || + typeof value.fetchedAt !== "number" || + !Number.isFinite(value.fetchedAt)) { + return undefined; + } + return { version: value.version, fetchedAt: value.fetchedAt }; + } + catch { + return undefined; + } +} +function writeVersionCache(cache) { + const file = versionCachePath(); + try { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, JSON.stringify(cache, null, 2)); + } + catch { + // Cache writes are optional. + } +} +function isCacheFresh(cache, now = Date.now()) { + const age = now - cache.fetchedAt; + return age >= 0 && age < MODEL_CACHE_TTL_MS; +} +async function fetchInstallerVersion() { + return withAbortDeadline(REMOTE_TIMEOUT_MS, () => new Error("Cursor installer version request timed out"), async (signal) => { + const response = await fetch(INSTALL_URL, { signal }); + if (!response.ok) + return undefined; + const build = extractVersionFromInstaller(await response.text()); + return build ? `cli-${build}` : undefined; + }); +} +async function refreshVersionCache() { + const version = await fetchInstallerVersion(); + if (version) + writeVersionCache({ version, fetchedAt: Date.now() }); +} +async function resolveRemoteVersion() { + const cached = readVersionCache(); + if (cached && isCacheFresh(cached)) { + void refreshVersionCache().catch(() => { }); + return cached.version; + } + try { + const version = await fetchInstallerVersion(); + if (version) { + writeVersionCache({ version, fetchedAt: Date.now() }); + return version; + } + } + catch { + // Use stale cache or the fallback below. + } + return cached?.version; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-bind.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-bind.d.ts new file mode 100644 index 00000000..57fe1c58 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-bind.d.ts @@ -0,0 +1,32 @@ +export declare const MAX_ACTIVE_CONVERSATION_BINDINGS = 256; +export declare function resetConversationBindingsForTests(): void; +export declare function hasConversationBinding(sessionKey: string): boolean; +export declare function isActiveConversationBinding(sessionKey: string, conversationId: string): boolean; +/** Restore a validated durable binding before resolving the next Run. */ +export declare function restoreConversationBinding(sessionKey: string, conversationId: string): void; +/** Deterministic UUID (version-4 shape) from an arbitrary session key. */ +export declare function sessionIdToUuid(sessionId: string): string; +/** + * Stable Cursor conversation group for one OpenCode session. + * + * Individual conversation ids are reminted for compaction/rebase boundaries, + * while the native Cursor CLI keeps the enclosing agent-store group stable. + * With no host session identity, the current conversation is the only safe + * grouping scope available. + */ +export declare function resolveConversationGroupId(sessionKey: string | undefined, conversationId: string): string; +/** Current Cursor conversation_id for an OpenCode session key, creating the default binding if needed. */ +export declare function peekConversationId(sessionKey: string): string; +/** + * Resolve the Cursor conversation_id for this OpenCode session. + * When `reset` is true (compaction/summary), discard prior Cursor state and + * mint a new id so TurnEnded cache_read cannot keep overflowing OpenCode. + */ +export declare function bindConversationId(sessionKey: string | undefined, opts?: { + reset?: boolean; + ephemeral?: boolean; +}): { + conversationId: string; + reset: boolean; + previousId?: string; +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-bind.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-bind.js new file mode 100644 index 00000000..3d240fe1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-bind.js @@ -0,0 +1,105 @@ +import { createHash } from "node:crypto"; +import { clearCheckpoint } from "./checkpoint.js"; +import { clearConversationBlobs } from "./blob-store.js"; +import { transferFrozenRequestContext, } from "../context/frozen.js"; +/** + * OpenCode session key → active Cursor conversation_id. + * + * Normally the binding is the deterministic UUID from the OpenCode session id + * (CLI-shaped sticky conversation). Compaction/summary turns mint a fresh id + * and drop the prior checkpoint + blobs so Cursor no longer bills the old + * cached conversation against OpenCode's newly compacted prompt. + */ +const activeBySession = new Map(); +// A compaction id cannot be reconstructed from its host session key. Keep its +// tiny binding even when the recent-session index soft-evicts that session. +const remintedBySession = new Map(); +export const MAX_ACTIVE_CONVERSATION_BINDINGS = 256; +function rememberConversation(sessionKey, conversationId) { + if (conversationId === sessionIdToUuid(sessionKey)) + remintedBySession.delete(sessionKey); + else + remintedBySession.set(sessionKey, conversationId); + // Map insertion order is the LRU order. Refresh existing sessions on use. + activeBySession.delete(sessionKey); + activeBySession.set(sessionKey, conversationId); + while (activeBySession.size > MAX_ACTIVE_CONVERSATION_BINDINGS) { + const oldest = activeBySession.entries().next().value; + if (!oldest) + break; + activeBySession.delete(oldest[0]); + // Evict only the lookup pointer. The checkpoint may still be needed by a + // held Run, and a returning session can hydrate its durable binding before + // resolving another turn. Compaction/reset owns deletion of opaque state. + } +} +export function resetConversationBindingsForTests() { + activeBySession.clear(); + remintedBySession.clear(); +} +export function hasConversationBinding(sessionKey) { + return activeBySession.has(sessionKey) || remintedBySession.has(sessionKey); +} +export function isActiveConversationBinding(sessionKey, conversationId) { + return (activeBySession.get(sessionKey) ?? remintedBySession.get(sessionKey) + ?? sessionIdToUuid(sessionKey)) === conversationId; +} +/** Restore a validated durable binding before resolving the next Run. */ +export function restoreConversationBinding(sessionKey, conversationId) { + if (!sessionKey || !conversationId) + return; + rememberConversation(sessionKey, conversationId); +} +/** Deterministic UUID (version-4 shape) from an arbitrary session key. */ +export function sessionIdToUuid(sessionId) { + const hash = createHash("sha256").update(`cursor-opencode-provider:conv:${sessionId}`).digest(); + const bytes = Buffer.from(hash.subarray(0, 16)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = bytes.toString("hex"); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; +} +/** + * Stable Cursor conversation group for one OpenCode session. + * + * Individual conversation ids are reminted for compaction/rebase boundaries, + * while the native Cursor CLI keeps the enclosing agent-store group stable. + * With no host session identity, the current conversation is the only safe + * grouping scope available. + */ +export function resolveConversationGroupId(sessionKey, conversationId) { + return sessionKey ? sessionIdToUuid(sessionKey) : conversationId; +} +/** Current Cursor conversation_id for an OpenCode session key, creating the default binding if needed. */ +export function peekConversationId(sessionKey) { + let id = activeBySession.get(sessionKey) ?? remintedBySession.get(sessionKey); + if (!id) { + id = sessionIdToUuid(sessionKey); + } + rememberConversation(sessionKey, id); + return id; +} +/** + * Resolve the Cursor conversation_id for this OpenCode session. + * When `reset` is true (compaction/summary), discard prior Cursor state and + * mint a new id so TurnEnded cache_read cannot keep overflowing OpenCode. + */ +export function bindConversationId(sessionKey, opts) { + if (opts?.ephemeral) { + return { conversationId: crypto.randomUUID(), reset: false }; + } + if (!sessionKey) { + return { conversationId: crypto.randomUUID(), reset: !!opts?.reset }; + } + if (opts?.reset) { + const previousId = activeBySession.get(sessionKey) ?? remintedBySession.get(sessionKey) + ?? sessionIdToUuid(sessionKey); + const conversationId = crypto.randomUUID(); + clearCheckpoint(previousId); + clearConversationBlobs(previousId); + transferFrozenRequestContext(previousId, conversationId); + rememberConversation(sessionKey, conversationId); + return { conversationId, reset: true, previousId }; + } + return { conversationId: peekConversationId(sessionKey), reset: false }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-persistence.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-persistence.d.ts new file mode 100644 index 00000000..ece8b07e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-persistence.d.ts @@ -0,0 +1,34 @@ +import type { ConversationBlobSnapshot } from "./blob-store.js"; +import type { OpencodeToolDef } from "./tools.js"; +export type PersistedConversation = { + sessionKey: string; + conversationId: string; + updatedAt: number; + checkpoint?: Uint8Array; + blobs: ConversationBlobSnapshot[]; + requestContext: Record; + toolCatalog: OpencodeToolDef[]; + postCompactionRebase: boolean; + hostAgent?: string; + systemPromptHash?: string; +}; +export type ConversationLoadStatus = "restored" | "missing" | "invalid" | "expired"; +export type PersistedConversationLoad = { + status: ConversationLoadStatus; + value?: PersistedConversation; +}; +export declare function conversationCacheDirectoryPath(cacheDir: string): string; +export declare function conversationCacheFilePath(cacheDir: string, sessionKey: string): string; +/** Create the per-session cache directory and prune records older than 24 hours once per process. */ +export declare function initializeConversationPersistence(cacheDir: string, now?: number): Promise; +export declare function getPersistedConversation(cacheDir: string, sessionKey: string, now?: number): Promise; +/** Read a snapshot together with the reason restart hydration did not restore it. */ +export declare function loadPersistedConversation(cacheDir: string, sessionKey: string, now?: number): Promise; +/** Replace one session's durable snapshot and refresh its 24-hour lease. */ +export declare function persistConversation(cacheDir: string, value: Omit & { + postCompactionRebase?: boolean; + toolCatalog?: OpencodeToolDef[]; +}, now?: number): Promise; +/** Delete only the expected binding so an overlapping newer turn cannot be removed. */ +export declare function deletePersistedConversation(cacheDir: string, sessionKey: string, expectedConversationId?: string): Promise; +export declare function resetConversationPersistenceForTests(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-persistence.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-persistence.js new file mode 100644 index 00000000..08e2a3a3 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-persistence.js @@ -0,0 +1,522 @@ +import { createHash, randomUUID } from "node:crypto"; +import path from "node:path"; +import { chmod, link, mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises"; +import { constants as zlibConstants, gunzipSync, gzipSync } from "node:zlib"; +import protobuf from "protobufjs"; +import { CONVERSATION_CACHE_DIR, CONVERSATION_CACHE_SCHEMA_VERSION, CONVERSATION_CACHE_TTL_MS, } from "../shared.js"; +import { decodeMessageSparse, encodeMessage } from "./messages.js"; +import { trace } from "../debug.js"; +const stores = new Map(); +const initializedRoots = new Map(); +const startupDiscardedStatuses = new Map(); +const MAX_CONVERSATION_CACHE_BYTES = 256 * 1024 * 1024; +const utf8Decoder = new TextDecoder("utf-8", { fatal: true }); +function recordStartupDiscard(cacheDir, fileName, status) { + const root = path.resolve(cacheDir); + let statuses = startupDiscardedStatuses.get(root); + if (!statuses) { + statuses = new Map(); + startupDiscardedStatuses.set(root, statuses); + } + statuses.set(fileName, status); +} +export function conversationCacheDirectoryPath(cacheDir) { + return path.join(cacheDir, CONVERSATION_CACHE_DIR); +} +function sessionFileName(sessionKey) { + const label = sessionKey.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48); + const hash = createHash("sha256").update(sessionKey).digest("hex"); + return `${label || "session"}-${hash}.pb.gz`; +} +export function conversationCacheFilePath(cacheDir, sessionKey) { + return path.join(conversationCacheDirectoryPath(cacheDir), sessionFileName(sessionKey)); +} +function cloneConversation(value) { + return { + ...value, + checkpoint: value.checkpoint ? Uint8Array.from(value.checkpoint) : undefined, + blobs: value.blobs.map((blob) => ({ id: blob.id, data: Uint8Array.from(blob.data) })), + requestContext: structuredClone(value.requestContext), + toolCatalog: structuredClone(value.toolCatalog), + postCompactionRebase: value.postCompactionRebase, + }; +} +/** + * Private proto3 layout, encoded inline because the published package contains + * compiled JS only: + * + * ConversationCache: schema_version=1, session_key=2, conversation_id=3, + * updated_at=4, checkpoint=5, blobs=6, request_context=7, tool_catalog=8, + * post_compaction_rebase=9, host_agent=10, system_prompt_hash=11. + * Blob: id=1, data=2. Tool: name=1, description=2, + * input_schema_json=3, source_name=4. + * + * Opaque protocol state stays as bytes. Only arbitrary host JSON Schemas remain + * JSON, nested as UTF-8 bytes inside each Tool message. + */ +function fieldTag(field, wireType) { + return (field << 3) | wireType; +} +function bytesToHex(bytes) { + return Buffer.from(bytes).toString("hex"); +} +function readUint64(reader) { + const raw = reader.uint64(); + const value = typeof raw === "number" ? raw : Number(raw.toString()); + if (!Number.isSafeInteger(value) || value < 0) + throw new Error("invalid uint64"); + return value; +} +function readBlob(encoded) { + const reader = protobuf.Reader.create(encoded); + let id; + let blobData; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if ((tag & 7) !== 2) + throw new Error("invalid blob id"); + id = reader.bytes(); + break; + case 2: + if ((tag & 7) !== 2) + throw new Error("invalid blob data"); + blobData = reader.bytes(); + break; + default: + reader.skipType(tag & 7); + } + } + if (!id || id.length === 0 || !blobData) + throw new Error("incomplete blob"); + return { id: bytesToHex(id), data: Uint8Array.from(blobData) }; +} +function readTool(data) { + const reader = protobuf.Reader.create(data); + let name; + let description; + let inputSchema; + let hasInputSchema = false; + let sourceName; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const wireType = tag & 7; + switch (tag >>> 3) { + case 1: + if (wireType !== 2) + throw new Error("invalid tool name"); + name = reader.string(); + break; + case 2: + if (wireType !== 2) + throw new Error("invalid tool description"); + description = reader.string(); + break; + case 3: + if (wireType !== 2) + throw new Error("invalid tool schema"); + inputSchema = JSON.parse(utf8Decoder.decode(reader.bytes())); + hasInputSchema = true; + break; + case 4: + if (wireType !== 2) + throw new Error("invalid tool source name"); + sourceName = reader.string(); + break; + default: + reader.skipType(wireType); + } + } + if (!name) + throw new Error("incomplete tool"); + return { + name, + description, + ...(hasInputSchema ? { inputSchema } : {}), + sourceName, + }; +} +function blobIdBytes(id) { + if (!/^(?:[0-9a-f]{2})+$/i.test(id)) + throw new Error("invalid conversation blob id"); + return Buffer.from(id, "hex"); +} +function encodeCacheFile(value) { + const writer = protobuf.Writer.create(); + const requestContext = encodeMessage("RequestContext", value.requestContext); + writer.uint32(fieldTag(1, 0)).uint32(CONVERSATION_CACHE_SCHEMA_VERSION); + writer.uint32(fieldTag(2, 2)).string(value.sessionKey); + writer.uint32(fieldTag(3, 2)).string(value.conversationId); + writer.uint32(fieldTag(4, 0)).uint64(value.updatedAt); + if (value.checkpoint?.length) + writer.uint32(fieldTag(5, 2)).bytes(value.checkpoint); + for (const blob of value.blobs) { + writer.uint32(fieldTag(6, 2)).fork(); + writer.uint32(fieldTag(1, 2)).bytes(blobIdBytes(blob.id)); + writer.uint32(fieldTag(2, 2)).bytes(blob.data); + writer.ldelim(); + } + writer.uint32(fieldTag(7, 2)).bytes(requestContext); + for (const tool of value.toolCatalog) { + writer.uint32(fieldTag(8, 2)).fork(); + writer.uint32(fieldTag(1, 2)).string(tool.name); + if (tool.description !== undefined) + writer.uint32(fieldTag(2, 2)).string(tool.description); + if (tool.inputSchema !== undefined) { + writer.uint32(fieldTag(3, 2)).bytes(Buffer.from(JSON.stringify(tool.inputSchema), "utf8")); + } + if (tool.sourceName !== undefined) + writer.uint32(fieldTag(4, 2)).string(tool.sourceName); + writer.ldelim(); + } + if (value.postCompactionRebase) + writer.uint32(fieldTag(9, 0)).bool(true); + if (value.hostAgent) + writer.uint32(fieldTag(10, 2)).string(value.hostAgent); + if (value.systemPromptHash) + writer.uint32(fieldTag(11, 2)).string(value.systemPromptHash); + return { protobufBytes: writer.finish(), requestContextBytes: requestContext.length }; +} +function decodeProtobuf(data) { + const reader = protobuf.Reader.create(data); + let schemaVersion; + let sessionKey; + let conversationId; + let updatedAt; + let checkpoint; + const blobs = []; + let requestContextBytes; + const toolCatalog = []; + let postCompactionRebase = false; + let hostAgent; + let systemPromptHash; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const wireType = tag & 7; + switch (tag >>> 3) { + case 1: + if (wireType !== 0) + throw new Error("invalid schema version"); + schemaVersion = reader.uint32(); + break; + case 2: + if (wireType !== 2) + throw new Error("invalid session key"); + sessionKey = reader.string(); + break; + case 3: + if (wireType !== 2) + throw new Error("invalid conversation id"); + conversationId = reader.string(); + break; + case 4: + if (wireType !== 0) + throw new Error("invalid update timestamp"); + updatedAt = readUint64(reader); + break; + case 5: + if (wireType !== 2) + throw new Error("invalid checkpoint"); + checkpoint = Uint8Array.from(reader.bytes()); + break; + case 6: + if (wireType !== 2) + throw new Error("invalid blob"); + blobs.push(readBlob(reader.bytes())); + break; + case 7: + if (wireType !== 2) + throw new Error("invalid request context"); + requestContextBytes = reader.bytes(); + break; + case 8: + if (wireType !== 2) + throw new Error("invalid tool"); + toolCatalog.push(readTool(reader.bytes())); + break; + case 9: + if (wireType !== 0) + throw new Error("invalid compaction marker"); + postCompactionRebase = reader.bool(); + break; + case 10: + if (wireType !== 2) + throw new Error("invalid host agent"); + hostAgent = reader.string(); + break; + case 11: + if (wireType !== 2) + throw new Error("invalid system prompt hash"); + systemPromptHash = reader.string(); + break; + default: + reader.skipType(wireType); + } + } + if (schemaVersion !== CONVERSATION_CACHE_SCHEMA_VERSION || + !sessionKey || !conversationId || updatedAt === undefined || + !requestContextBytes || requestContextBytes.length === 0) + throw new Error("incomplete conversation cache"); + const requestContext = decodeMessageSparse("RequestContext", requestContextBytes); + return { + sessionKey, + conversationId, + updatedAt, + checkpoint, + blobs, + requestContext, + toolCatalog, + postCompactionRebase, + ...(hostAgent ? { hostAgent } : {}), + ...(systemPromptHash ? { systemPromptHash } : {}), + }; +} +function decodeCacheFile(compressed, expectedSessionKey) { + try { + if (compressed.length > MAX_CONVERSATION_CACHE_BYTES) + return undefined; + const protobufBytes = gunzipSync(compressed, { maxOutputLength: MAX_CONVERSATION_CACHE_BYTES }); + const conversation = decodeProtobuf(protobufBytes); + if (expectedSessionKey && conversation.sessionKey !== expectedSessionKey) + return undefined; + return conversation; + } + catch { + return undefined; + } +} +function isFresh(value, now) { + return now - value.updatedAt <= CONVERSATION_CACHE_TTL_MS; +} +async function readConversationFile(filePath, expectedSessionKey) { + try { + return decodeCacheFile(await readFile(filePath), expectedSessionKey); + } + catch (error) { + const code = error && typeof error === "object" && "code" in error + ? error.code + : undefined; + if (code !== "ENOENT") + return undefined; + return undefined; + } +} +async function readConversationFileWithStatus(filePath, expectedSessionKey) { + try { + const value = decodeCacheFile(await readFile(filePath), expectedSessionKey); + return value ? { status: "restored", value } : { status: "invalid" }; + } + catch (error) { + const code = error && typeof error === "object" && "code" in error + ? error.code + : undefined; + return { status: code === "ENOENT" ? "missing" : "invalid" }; + } +} +/** + * Remove an invalid/stale candidate without deleting a fresh atomic replacement. + * The candidate is first moved aside. If it turns out another process replaced + * it between inspection and rename, hard-link it back only when the live path is + * still empty; otherwise the writer's newer path wins. + */ +async function discardCacheCandidate(filePath, now) { + const quarantinePath = `${filePath}.${process.pid}.${randomUUID()}.stale`; + try { + await rename(filePath, quarantinePath); + } + catch (error) { + const code = error && typeof error === "object" && "code" in error + ? error.code + : undefined; + if (code === "ENOENT") + return undefined; + throw error; + } + try { + const moved = await readConversationFile(quarantinePath); + if (moved && isFresh(moved, now)) { + try { + await link(quarantinePath, filePath); + return moved; + } + catch (error) { + const code = error && typeof error === "object" && "code" in error + ? error.code + : undefined; + if (code !== "EEXIST") + throw error; + return await readConversationFile(filePath); + } + } + } + finally { + await unlink(quarantinePath).catch(() => { }); + } + return await readConversationFile(filePath); +} +async function writeConversationFile(cacheDir, value) { + const directory = conversationCacheDirectoryPath(cacheDir); + const filePath = conversationCacheFilePath(cacheDir, value.sessionKey); + const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`); + const encoded = encodeCacheFile(value); + const { protobufBytes } = encoded; + const compressed = gzipSync(protobufBytes, { level: zlibConstants.Z_BEST_COMPRESSION }); + await ensureCacheDirectory(directory); + try { + await writeFile(tempPath, compressed, { mode: 0o600 }); + await rename(tempPath, filePath); + trace(`conversation persistence: snapshot written sessionKey=${value.sessionKey} ` + + `fileBytes=${compressed.length} protobufBytes=${protobufBytes.length} ` + + `checkpointBytes=${value.checkpoint?.length ?? 0} ` + + `blobCount=${value.blobs.length} blobBytes=${value.blobs.reduce((sum, blob) => sum + blob.data.length, 0)} ` + + `requestContextBytes=${encoded.requestContextBytes} ` + + `toolCatalogBytes=${Buffer.byteLength(JSON.stringify(value.toolCatalog))}`); + } + finally { + await unlink(tempPath).catch(() => { }); + } +} +async function ensureCacheDirectory(directory) { + await mkdir(directory, { recursive: true, mode: 0o700 }); + if (process.platform !== "win32") + await chmod(directory, 0o700); +} +function queueWrite(store, value) { + const snapshot = cloneConversation(value); + const write = store.writeChain.catch(() => { }).then(() => writeConversationFile(store.cacheDir, snapshot)); + store.writeChain = write; + return write; +} +function queueDelete(store) { + const write = store.writeChain.catch(() => { }).then(async () => { + await unlink(conversationCacheFilePath(store.cacheDir, store.sessionKey)).catch((error) => { + const code = error && typeof error === "object" && "code" in error + ? error.code + : undefined; + if (code !== "ENOENT") + throw error; + }); + }); + store.writeChain = write; + return write; +} +async function pruneConversationDirectory(cacheDir, now) { + const directory = conversationCacheDirectoryPath(cacheDir); + await ensureCacheDirectory(directory); + const entries = await readdir(directory, { withFileTypes: true }); + await Promise.all(entries.map(async (entry) => { + if (!entry.isFile()) + return; + const filePath = path.join(directory, entry.name); + if (entry.name.endsWith(".json")) { + trace(`conversation persistence: startup removed legacy JSON snapshot file=${entry.name}`); + await unlink(filePath).catch(() => { }); + return; + } + if (!entry.name.endsWith(".pb.gz")) + return; + const loaded = await readConversationFileWithStatus(filePath); + if (loaded.status === "invalid") { + recordStartupDiscard(cacheDir, entry.name, "invalid"); + trace(`conversation persistence: startup discarded invalid snapshot file=${entry.name}`); + await discardCacheCandidate(filePath, now); + } + else if (loaded.value && !isFresh(loaded.value, now)) { + recordStartupDiscard(cacheDir, entry.name, "expired"); + trace(`conversation persistence: startup discarded expired snapshot ` + + `sessionKey=${loaded.value.sessionKey} ageMs=${Math.max(0, now - loaded.value.updatedAt)}`); + await discardCacheCandidate(filePath, now); + } + })); +} +/** Create the per-session cache directory and prune records older than 24 hours once per process. */ +export async function initializeConversationPersistence(cacheDir, now = Date.now()) { + const key = path.resolve(cacheDir); + let pending = initializedRoots.get(key); + if (!pending) { + pending = pruneConversationDirectory(key, now); + initializedRoots.set(key, pending); + pending.catch(() => { + if (initializedRoots.get(key) === pending) + initializedRoots.delete(key); + }); + } + await pending; +} +async function loadStore(cacheDir, sessionKey, now) { + await initializeConversationPersistence(cacheDir, now); + const filePath = conversationCacheFilePath(cacheDir, sessionKey); + const loaded = await readConversationFileWithStatus(filePath, sessionKey); + let value = loaded.value; + let loadStatus = loaded.status === "missing" + ? startupDiscardedStatuses.get(path.resolve(cacheDir))?.get(path.basename(filePath)) ?? "missing" + : loaded.status; + if (value && !isFresh(value, now)) { + loadStatus = "expired"; + value = await discardCacheCandidate(filePath, now); + if (value && isFresh(value, now)) + loadStatus = "restored"; + } + return { + cacheDir, + sessionKey, + value: value && value.sessionKey === sessionKey && isFresh(value, now) ? value : undefined, + loadStatus, + writeChain: Promise.resolve(), + }; +} +async function getStore(cacheDir, sessionKey, now = Date.now()) { + const root = path.resolve(cacheDir); + const key = `${root}\0${sessionKey}`; + let pending = stores.get(key); + if (!pending) { + pending = loadStore(root, sessionKey, now); + stores.set(key, pending); + pending.catch(() => { + if (stores.get(key) === pending) + stores.delete(key); + }); + } + return pending; +} +export async function getPersistedConversation(cacheDir, sessionKey, now = Date.now()) { + const store = await getStore(cacheDir, sessionKey, now); + return store.value ? cloneConversation(store.value) : undefined; +} +/** Read a snapshot together with the reason restart hydration did not restore it. */ +export async function loadPersistedConversation(cacheDir, sessionKey, now = Date.now()) { + const store = await getStore(cacheDir, sessionKey, now); + return store.value + ? { status: "restored", value: cloneConversation(store.value) } + : { status: store.loadStatus }; +} +/** Replace one session's durable snapshot and refresh its 24-hour lease. */ +export async function persistConversation(cacheDir, value, now = Date.now()) { + const store = await getStore(cacheDir, value.sessionKey, now); + const snapshot = cloneConversation({ + ...value, + updatedAt: now, + toolCatalog: structuredClone(value.toolCatalog ?? []), + postCompactionRebase: value.postCompactionRebase === true, + }); + store.value = snapshot; + store.loadStatus = "restored"; + startupDiscardedStatuses.get(path.resolve(cacheDir))?.delete(path.basename(conversationCacheFilePath(cacheDir, value.sessionKey))); + await queueWrite(store, snapshot); +} +/** Delete only the expected binding so an overlapping newer turn cannot be removed. */ +export async function deletePersistedConversation(cacheDir, sessionKey, expectedConversationId) { + const store = await getStore(cacheDir, sessionKey); + const current = store.value; + if (!current || (expectedConversationId && current.conversationId !== expectedConversationId)) + return; + store.value = undefined; + store.loadStatus = "missing"; + await queueDelete(store); +} +export function resetConversationPersistenceForTests() { + stores.clear(); + initializedRoots.clear(); + startupDiscardedStatuses.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-state.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-state.d.ts new file mode 100644 index 00000000..93c72c20 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-state.d.ts @@ -0,0 +1,20 @@ +import type { OpencodeToolDef } from "./tools.js"; +/** Restore one OpenCode session before its conversation binding is resolved. */ +export declare function hydrateConversationState(cacheDir: string, sessionKey: string): Promise<{ + conversationId: string; + postCompactionRebase: boolean; + toolCatalog: OpencodeToolDef[]; + hostAgent?: string; + systemPromptHash?: string; +} | undefined>; +/** Persist the complete resumable state only after Cursor confirms TurnEnded. */ +export declare function persistConversationState(cacheDir: string, input: { + sessionKey: string; + conversationId: string; + requestContext: Record; + toolCatalog?: OpencodeToolDef[]; + postCompactionRebase?: boolean; + hostAgent?: string; + systemPromptHash?: string; +}): Promise; +export declare function clearPersistedConversationState(cacheDir: string, sessionKey: string, expectedConversationId?: string): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-state.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-state.js new file mode 100644 index 00000000..508181da --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/conversation-state.js @@ -0,0 +1,66 @@ +import { trace } from "../debug.js"; +import { getFrozenRequestContext, setFrozenRequestContext, } from "../context/frozen.js"; +import { hasConversationBinding, isActiveConversationBinding, restoreConversationBinding, } from "./conversation-bind.js"; +import { compactConversationBlobs, restoreConversationBlobs, } from "./blob-store.js"; +import { getCheckpoint, setCheckpoint } from "./checkpoint.js"; +import { deletePersistedConversation, loadPersistedConversation, persistConversation, } from "./conversation-persistence.js"; +/** Restore one OpenCode session before its conversation binding is resolved. */ +export async function hydrateConversationState(cacheDir, sessionKey) { + if (hasConversationBinding(sessionKey)) + return undefined; + const loaded = await loadPersistedConversation(cacheDir, sessionKey); + const persisted = loaded.value; + if (!persisted) { + trace(`conversation persistence: hydration skipped sessionKey=${sessionKey} ` + + `status=${loaded.status}`); + return undefined; + } + restoreConversationBinding(sessionKey, persisted.conversationId); + if (persisted.checkpoint) + setCheckpoint(persisted.conversationId, persisted.checkpoint); + restoreConversationBlobs(persisted.conversationId, persisted.blobs); + setFrozenRequestContext(persisted.conversationId, persisted.requestContext); + trace(`conversation persistence: restored sessionKey=${sessionKey} ` + + `conversationId=${persisted.conversationId} checkpoint=${persisted.checkpoint?.length ?? 0}B ` + + `blobs=${persisted.blobs.length}`); + return { + conversationId: persisted.conversationId, + postCompactionRebase: persisted.postCompactionRebase, + toolCatalog: structuredClone(persisted.toolCatalog), + ...(persisted.hostAgent ? { hostAgent: persisted.hostAgent } : {}), + ...(persisted.systemPromptHash ? { systemPromptHash: persisted.systemPromptHash } : {}), + }; +} +/** Persist the complete resumable state only after Cursor confirms TurnEnded. */ +export async function persistConversationState(cacheDir, input) { + // A newer Run may have reset/superseded this conversation while its final + // frame was still in flight. Never let that late TurnEnded resurrect it. + if (!isActiveConversationBinding(input.sessionKey, input.conversationId)) { + trace(`conversation persistence: skipped superseded TurnEnded ` + + `sessionKey=${input.sessionKey} conversationId=${input.conversationId}`); + return; + } + const checkpoint = getCheckpoint(input.conversationId); + const blobCompaction = compactConversationBlobs(input.conversationId, checkpoint); + const blobs = blobCompaction.blobs; + const requestContext = getFrozenRequestContext(input.conversationId) ?? input.requestContext; + await persistConversation(cacheDir, { + sessionKey: input.sessionKey, + conversationId: input.conversationId, + checkpoint, + blobs, + requestContext, + toolCatalog: structuredClone(input.toolCatalog ?? []), + postCompactionRebase: input.postCompactionRebase, + hostAgent: input.hostAgent, + systemPromptHash: input.systemPromptHash, + }); + trace(`conversation persistence: saved sessionKey=${input.sessionKey} ` + + `conversationId=${input.conversationId} checkpoint=${checkpoint?.length ?? 0}B ` + + `blobs=${blobCompaction.beforeCount}->${blobCompaction.afterCount} ` + + `blobBytes=${blobCompaction.beforeBytes}->${blobCompaction.afterBytes}` + + (blobCompaction.fallbackReason ? ` compactionFallback=${blobCompaction.fallbackReason}` : "")); +} +export async function clearPersistedConversationState(cacheDir, sessionKey, expectedConversationId) { + await deletePersistedConversation(cacheDir, sessionKey, expectedConversationId); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/create-plan.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/create-plan.d.ts new file mode 100644 index 00000000..b35d75b0 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/create-plan.d.ts @@ -0,0 +1,140 @@ +/** + * Cursor-native CreatePlan → host-default plan file. + * + * Cursor raises `create_plan_request_query` (#7) with CreatePlanArgs and expects + * a CreatePlanResult carrying `plan_uri`. Cursor CLI writes under + * `~/.cursor/plans/*.plan.md` with YAML frontmatter; this provider deliberately + * does **not** — plans must stay host-portable so switching models does not + * strand users on Cursor-specific paths or frontmatter. + * + * Location mirrors OpenCode Session.plan shape, but the project-config segment + * comes from {@link hostPlansDir} / {@link opencodeProjectConfigDirs} + * (the active host's project config dir via the path bridge), never a + * hardcoded OpenCode-only directory name. + * + * Body is plain markdown (the same shape a plan-mode model would write with + * `write`). Cursor YAML frontmatter is never emitted. + */ +import { type OpencodeQuestionInput } from "./ask-question.js"; +export type CursorPlanTodo = { + id: string; + content: string; + status: string; +}; +export type CursorCreatePlanArgs = { + plan: string; + overview: string; + name: string; + isProject: boolean; + todos: CursorPlanTodo[]; +}; +export type DecodedCreatePlanQuery = { + args: CursorCreatePlanArgs; + toolCallId: string; +}; +export type CreatePlanWriteResult = { + ok: true; + planPath: string; + planUri: string; + markdown: string; +} | { + ok: false; + error: string; +}; +/** Optional host plan-stage tool advertised by a compatible host. */ +export declare const CURSOR_PLAN_STAGE_TOOL = "cursor_plan_stage"; +/** Held InteractionQuery continuation field for a native host plan stage. */ +export declare const CREATE_PLAN_RESULT_FIELD = "create_plan_request_response"; +export type CursorPlanStageInput = { + plan_uri: string; + content: string; + title: string; +}; +/** + * How a CreatePlan query is satisfied this turn. Keyed only on the advertised + * catalog and the provider's own plan-mode state — never on host identity. + * + * - `stage` a host plan-stage tool is advertised; the host writes the plan. + * Its tool result is the approval outcome (success = approved, + * error = not approved). The result may name the host's own + * follow-up approval call. This provider does not invent that prompt. + * - `approve` no such tool, but the host advertises `question`: the provider + * writes the plan itself and then asks the same thing with upstream + * `PlanExitTool`'s own prompt. + * - `ack` nothing to ask with (or a lifecycle turn): write, or for a + * lifecycle turn skip the write, and acknowledge the CLI way. + * + * The plan is always persisted before any prompt: writing needs no approval, + * executing does. + */ +export type CreatePlanBridge = { + kind: "stage"; +} | { + kind: "approve"; +} | { + kind: "ack"; +}; +/** Upstream `PlanExitTool` wording, with the plan the provider just wrote. */ +export declare function createPlanApprovalQuestion(planLabel: string): string; +export declare const CREATE_PLAN_APPROVAL_HEADER = "Build Agent"; +/** Cursor-visible reason when the user wants the plan revised instead of run. */ +export declare const CREATE_PLAN_NOT_APPROVED_REASON: string; +/** + * Resolve how this CreatePlan is satisfied. `planModeActive` is the provider's + * own record of an approved Cursor plan/spec mode: the approval prompt guards + * the transition *out of* planning, so a CreatePlan raised outside plan mode is + * written and acknowledged without one. + */ +export declare function resolveCreatePlanBridge(options: { + allowTools: boolean; + canStage?: boolean; + planModeActive?: boolean; + advertised: ReadonlySet | Iterable; +}): CreatePlanBridge; +/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */ +export declare function createPlanApprovalQuestionInput(planLabel: string): OpencodeQuestionInput; +/** + * True when the emulated approval prompt came back as an explicit "Yes". + * An unanswered, dismissed, or failed prompt keeps the model planning rather + * than silently starting execution. OpenCode prose is anchored on the exact + * prompt text. Every other result shape fails closed. + */ +export declare function createPlanApproved(output: string, isError: boolean, question: string): boolean; +/** Decode a `create_plan_request_query` body, or undefined when unusable. */ +export declare function decodeCreatePlanQuery(queryBytes: Uint8Array): DecodedCreatePlanQuery | undefined; +/** OpenCode-style adjective-noun slug (core/src/util/slug.ts). */ +export declare function randomPlanSlug(seed?: number): string; +/** Slugify a human title into a filesystem-safe token. */ +export declare function slugifyPlanName(name: string): string; +/** Build the advertised host plan-stage artifact payload. */ +export declare function createPlanStageInput(args: CursorCreatePlanArgs): CursorPlanStageInput; +/** + * Resolve the absolute plan file path via {@link hostPlansDir}. + * Filename shape matches OpenCode Session.plan: `-.md`. + */ +export declare function resolveHostPlanPath(workspaceRoot: string, name?: string, created?: number): string; +/** @deprecated Prefer {@link resolveHostPlanPath}. */ +export declare const resolveOpencodePlanPath: typeof resolveHostPlanPath; +/** + * Render plain markdown for the plan file. No Cursor YAML frontmatter. + * Prefer `args.plan` as the body; prepend a title/overview when useful; append + * a markdown checklist for todos. + */ +export declare function renderOpencodePlanMarkdown(args: CursorCreatePlanArgs): string; +/** + * Write the plan under {@link hostPlansDir} and return a `file://` URI. + * Empty / missing content is the caller's responsibility (empty plan_uri ack). + */ +export declare function writeOpencodePlanFile(args: CursorCreatePlanArgs, workspaceRoot: string, created?: number): CreatePlanWriteResult; +/** + * The plan as the user reads it before approving execution. + * + * Cursor routes the plan body through the interaction query rather than the + * text stream, so without this the user is asked to approve a plan they were + * never shown. It goes into the assistant message — the host renders markdown + * there, it scrolls, and it survives answering the prompt. It must not go into + * the question itself: OpenCode renders the question dock outside its + * scrollbox with `flexShrink={0}`, so a full plan there would push the + * conversation off screen. + */ +export declare function renderPlanReviewMessage(markdown: string, planPath: string): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/create-plan.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/create-plan.js new file mode 100644 index 00000000..95c5eeea --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/create-plan.js @@ -0,0 +1,372 @@ +/** + * Cursor-native CreatePlan → host-default plan file. + * + * Cursor raises `create_plan_request_query` (#7) with CreatePlanArgs and expects + * a CreatePlanResult carrying `plan_uri`. Cursor CLI writes under + * `~/.cursor/plans/*.plan.md` with YAML frontmatter; this provider deliberately + * does **not** — plans must stay host-portable so switching models does not + * strand users on Cursor-specific paths or frontmatter. + * + * Location mirrors OpenCode Session.plan shape, but the project-config segment + * comes from {@link hostPlansDir} / {@link opencodeProjectConfigDirs} + * (the active host's project config dir via the path bridge), never a + * hardcoded OpenCode-only directory name. + * + * Body is plain markdown (the same shape a plan-mode model would write with + * `write`). Cursor YAML frontmatter is never emitted. + */ +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { hostPlansDir } from "../context/paths.js"; +import { parseAnswerSegments, } from "./ask-question.js"; +import { decodeMessageSparse } from "./messages.js"; +const PLAN_ADJECTIVES = [ + "brave", + "calm", + "clever", + "cosmic", + "crisp", + "curious", + "eager", + "gentle", + "glowing", + "happy", + "hidden", + "jolly", + "kind", + "lucky", + "mighty", + "misty", + "neon", + "nimble", + "playful", + "proud", + "quick", + "quiet", + "shiny", + "silent", + "stellar", + "sunny", + "swift", + "tidy", + "witty", +]; +const PLAN_NOUNS = [ + "cabin", + "cactus", + "canyon", + "circuit", + "comet", + "eagle", + "engine", + "falcon", + "forest", + "garden", + "harbor", + "island", + "knight", + "lagoon", + "meadow", + "moon", + "mountain", + "nebula", + "orchid", + "otter", + "panda", + "pixel", + "planet", + "river", + "rocket", + "sailor", + "squid", + "star", + "tiger", + "wizard", + "wolf", +]; +/** Optional host plan-stage tool advertised by a compatible host. */ +export const CURSOR_PLAN_STAGE_TOOL = "cursor_plan_stage"; +/** Held InteractionQuery continuation field for a native host plan stage. */ +export const CREATE_PLAN_RESULT_FIELD = "create_plan_request_response"; +/** Upstream `PlanExitTool` wording, with the plan the provider just wrote. */ +export function createPlanApprovalQuestion(planLabel) { + const where = planLabel.trim(); + return where + ? `Plan at ${where} is complete. Would you like to switch to the build agent and start implementing?` + : "The plan is complete. Would you like to switch to the build agent and start implementing?"; +} +export const CREATE_PLAN_APPROVAL_HEADER = "Build Agent"; +const CREATE_PLAN_APPROVAL_YES = "Yes"; +const CREATE_PLAN_APPROVAL_NO = "No"; +/** Cursor-visible reason when the user wants the plan revised instead of run. */ +export const CREATE_PLAN_NOT_APPROVED_REASON = "The user did not approve executing this plan. Keep planning: refine the plan and " + + "propose it again when it is ready."; +/** + * Resolve how this CreatePlan is satisfied. `planModeActive` is the provider's + * own record of an approved Cursor plan/spec mode: the approval prompt guards + * the transition *out of* planning, so a CreatePlan raised outside plan mode is + * written and acknowledged without one. + */ +export function resolveCreatePlanBridge(options) { + // A lifecycle turn (title generation, compaction) runs alongside the real one + // and must not write a second plan file or raise a second prompt. + if (!options.allowTools) + return { kind: "ack" }; + if (options.canStage) + return { kind: "stage" }; + const names = options.advertised instanceof Set ? options.advertised : new Set(options.advertised); + if (options.planModeActive && names.has("question")) + return { kind: "approve" }; + return { kind: "ack" }; +} +/** The synthetic AskQuestion item backing the emulated approval prompt. */ +function createPlanApprovalItem(question) { + return { + id: "create_plan_approval", + prompt: question, + options: [ + { id: "yes", label: CREATE_PLAN_APPROVAL_YES }, + { id: "no", label: CREATE_PLAN_APPROVAL_NO }, + ], + allowMultiple: false, + }; +} +/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */ +export function createPlanApprovalQuestionInput(planLabel) { + return { + questions: [ + { + question: createPlanApprovalQuestion(planLabel), + header: CREATE_PLAN_APPROVAL_HEADER, + options: [ + { + label: CREATE_PLAN_APPROVAL_YES, + description: "Switch to build agent and start implementing the plan", + }, + { + label: CREATE_PLAN_APPROVAL_NO, + description: "Stay with plan agent to continue refining the plan", + }, + ], + }, + ], + }; +} +/** + * True when the emulated approval prompt came back as an explicit "Yes". + * An unanswered, dismissed, or failed prompt keeps the model planning rather + * than silently starting execution. OpenCode prose is anchored on the exact + * prompt text. Every other result shape fails closed. + */ +export function createPlanApproved(output, isError, question) { + if (isError) + return false; + const [segment] = parseAnswerSegments([createPlanApprovalItem(question)], output); + const answer = (segment ?? "").trim().toLowerCase(); + return answer === CREATE_PLAN_APPROVAL_YES.toLowerCase(); +} +function asRecord(value) { + return value && typeof value === "object" && !Array.isArray(value) + ? value + : undefined; +} +function str(value) { + return typeof value === "string" ? value : ""; +} +function mapTodoStatus(status) { + if (typeof status === "string") + return status.toLowerCase(); + if (typeof status === "number") { + // TodoStatus enum: PENDING=1, IN_PROGRESS=2, COMPLETED=3, CANCELLED=4 + switch (status) { + case 2: + return "in_progress"; + case 3: + return "completed"; + case 4: + return "cancelled"; + default: + return "pending"; + } + } + return "pending"; +} +function todoCheckbox(status) { + const s = status.toLowerCase(); + if (s === "completed" || s === "complete" || s === "done") + return "[x]"; + if (s === "cancelled" || s === "canceled") + return "[~]"; + return "[ ]"; +} +/** Decode a `create_plan_request_query` body, or undefined when unusable. */ +export function decodeCreatePlanQuery(queryBytes) { + let decoded; + try { + decoded = decodeMessageSparse("CreatePlanRequestQuery", queryBytes); + } + catch { + return undefined; + } + const args = asRecord(decoded.args); + if (!args) + return undefined; + const plan = str(args.plan); + const overview = str(args.overview); + const name = str(args.name); + const todos = Array.isArray(args.todos) + ? args.todos.flatMap((raw) => { + const item = asRecord(raw); + if (!item) + return []; + const content = str(item.content); + if (!content) + return []; + return [ + { + id: str(item.id), + content, + status: mapTodoStatus(item.status), + }, + ]; + }) + : []; + // Empty args: CLI acknowledges with empty plan_uri; treat as no write. + if (!plan && !overview && !name && todos.length === 0) + return undefined; + return { + args: { + plan, + overview, + name, + isProject: args.is_project === true, + todos, + }, + toolCallId: str(decoded.tool_call_id), + }; +} +/** OpenCode-style adjective-noun slug (core/src/util/slug.ts). */ +export function randomPlanSlug(seed = Date.now()) { + const adj = PLAN_ADJECTIVES[seed % PLAN_ADJECTIVES.length]; + const noun = PLAN_NOUNS[Math.floor(seed / PLAN_ADJECTIVES.length) % PLAN_NOUNS.length]; + return `${adj}-${noun}`; +} +/** Slugify a human title into a filesystem-safe token. */ +export function slugifyPlanName(name) { + const slug = name + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 64); + return slug || randomPlanSlug(); +} +/** Build the advertised host plan-stage artifact payload. */ +export function createPlanStageInput(args) { + const slug = slugifyPlanName(args.name); + return { + plan_uri: `local://${slug}-plan.md`, + content: renderOpencodePlanMarkdown(args), + title: slug, + }; +} +/** + * Resolve the absolute plan file path via {@link hostPlansDir}. + * Filename shape matches OpenCode Session.plan: `-.md`. + */ +export function resolveHostPlanPath(workspaceRoot, name, created = Date.now()) { + const slug = name?.trim() ? slugifyPlanName(name) : randomPlanSlug(created); + return path.join(hostPlansDir(workspaceRoot), `${created}-${slug}.md`); +} +/** @deprecated Prefer {@link resolveHostPlanPath}. */ +export const resolveOpencodePlanPath = resolveHostPlanPath; +/** + * Render plain markdown for the plan file. No Cursor YAML frontmatter. + * Prefer `args.plan` as the body; prepend a title/overview when useful; append + * a markdown checklist for todos. + */ +export function renderOpencodePlanMarkdown(args) { + const parts = []; + const name = args.name.trim(); + const overview = args.overview.trim(); + const plan = args.plan.trim(); + // Cursor usually repeats the plan name as the body's own leading H1. Strip it + // so the document keeps one title in the right place instead of opening with + // the same heading twice (or with the overview stranded above it). + const planLeadHeading = /^#\s+(.+?)\s*$/.exec(plan.split("\n", 1)[0] ?? "")?.[1]; + const planBody = name + && planLeadHeading !== undefined + && planLeadHeading.trim().toLowerCase() === name.toLowerCase() + ? plan.slice(plan.indexOf("\n") + 1).trimStart() + : plan; + if (name) { + parts.push(`# ${name}`); + parts.push(""); + } + if (overview) { + // Avoid duplicating overview when it already leads the plan body. + if (!planBody || !planBody.startsWith(overview)) { + parts.push(overview); + parts.push(""); + } + } + if (planBody) { + parts.push(planBody); + if (!planBody.endsWith("\n")) + parts.push(""); + } + if (args.todos.length > 0) { + if (parts.length > 0 && parts[parts.length - 1] !== "") + parts.push(""); + parts.push("## Todos"); + parts.push(""); + for (const todo of args.todos) { + parts.push(`- ${todoCheckbox(todo.status)} ${todo.content}`); + } + parts.push(""); + } + const body = parts.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd(); + return body ? `${body}\n` : ""; +} +/** + * Write the plan under {@link hostPlansDir} and return a `file://` URI. + * Empty / missing content is the caller's responsibility (empty plan_uri ack). + */ +export function writeOpencodePlanFile(args, workspaceRoot, created = Date.now()) { + const markdown = renderOpencodePlanMarkdown(args); + if (!markdown.trim()) { + return { ok: false, error: "CreatePlan produced no plan content to write" }; + } + const planPath = resolveHostPlanPath(workspaceRoot, args.name, created); + try { + mkdirSync(path.dirname(planPath), { recursive: true }); + writeFileSync(planPath, markdown, "utf-8"); + } + catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { ok: false, error: `Failed to write plan file: ${message}` }; + } + return { + ok: true, + planPath, + planUri: pathToFileURL(planPath).href, + markdown, + }; +} +/** + * The plan as the user reads it before approving execution. + * + * Cursor routes the plan body through the interaction query rather than the + * text stream, so without this the user is asked to approve a plan they were + * never shown. It goes into the assistant message — the host renders markdown + * there, it scrolls, and it survives answering the prompt. It must not go into + * the question itself: OpenCode renders the question dock outside its + * scrollbox with `flexShrink={0}`, so a full plan there would push the + * conversation off screen. + */ +export function renderPlanReviewMessage(markdown, planPath) { + const body = markdown.trim(); + const saved = `_Plan saved to ${planPath}_`; + return body ? `${body}\n\n${saved}\n` : `${saved}\n`; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/device-id.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/device-id.d.ts new file mode 100644 index 00000000..4427704f --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/device-id.d.ts @@ -0,0 +1,8 @@ +/** + * Stable device fingerprints, derived exactly like the Cursor CLI. + * Cached for the process lifetime after first computation. + */ +export declare function getDeviceIds(): { + machineId: string; + macMachineId?: string; +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/device-id.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/device-id.js new file mode 100644 index 00000000..72e74b11 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/device-id.js @@ -0,0 +1,121 @@ +import crypto from "node:crypto"; +import os from "node:os"; +import fs from "node:fs"; +import { execSync } from "node:child_process"; +// Device fingerprints MUST be stable across restarts: Cursor keys "device +// identity" off the ids embedded in x-cursor-checksum, and treats every new +// machineId as a separate device ("too many connections from different +// devices"). The real Cursor CLI derives these from stable OS identifiers +// (IOPlatformUUID / MAC address) rather than persisting them, so recomputing +// each launch yields the SAME value forever on a given machine. We replicate +// that derivation verbatim so we present exactly one device, matching the CLI. +// +// Source: cursor-agent-extracted — machineId = sha256(IOPlatformUUID) with a +// MAC/hostname fallback; macMachineId = sha256(first non-zero MAC). +let _cached; +function sha256hex(s) { + return crypto.createHash("sha256").update(s, "utf8").digest("hex"); +} +// macOS: `ioreg -rd1 -c IOPlatformExpertDevice` → IOPlatformUUID, parsed and +// normalised exactly as the CLI does. +function readMacOSUUID() { + try { + const out = execSync("ioreg -rd1 -c IOPlatformExpertDevice", { + timeout: 5000, + }).toString(); + const after = out.split("IOPlatformUUID")[1]; + if (!after) + return undefined; + return after + .split("\n")[0] + .replace(/=|\s+|"/gi, "") + .toLowerCase(); + } + catch { + return undefined; + } +} +// Linux: machine-id files (stable, per-install). +function readLinuxMachineId() { + for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) { + try { + const v = fs.readFileSync(p, "utf8"); + if (v) + return v.trim(); + } + catch { + /* ignore */ + } + } + return undefined; +} +// Windows: MachineGuid from the registry. +function readWindowsMachineGuid() { + try { + const out = execSync('reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid', { timeout: 5000 }).toString(); + const m = out.split("MachineGuid")[1]; + if (!m) + return undefined; + const hex = m.replace(/=|\s+|"/gi, ""); + return hex || undefined; + } + catch { + return undefined; + } +} +function platformUuid() { + switch (process.platform) { + case "darwin": + return readMacOSUUID(); + case "linux": + return readLinuxMachineId(); + case "win32": + return readWindowsMachineGuid(); + default: + return undefined; + } +} +// First non-zero MAC address across all interfaces (matches CLI's $e()). +function firstMacAddress() { + try { + const ifaces = os.networkInterfaces(); + for (const list of Object.values(ifaces)) { + if (!list) + continue; + for (const ni of list) { + if (ni && ni.mac && ni.mac !== "00:00:00:00:00:00") + return ni.mac; + } + } + } + catch { + /* ignore */ + } + return undefined; +} +/** + * Stable device fingerprints, derived exactly like the Cursor CLI. + * Cached for the process lifetime after first computation. + */ +export function getDeviceIds() { + if (_cached) + return _cached; + const mac = firstMacAddress(); + const uuid = platformUuid(); + // machineId: sha256(platform UUID); fall back to sha256(MAC) then hostname, + // mirroring the CLI's Xe() fallback chain. + let machineId; + if (uuid) { + machineId = sha256hex(uuid); + } + else if (mac) { + machineId = sha256hex(mac); + } + else { + machineId = sha256hex(os.hostname()); + } + // macMachineId: sha256(MAC), or undefined if no interface MAC is available. + const macMachineId = mac ? sha256hex(mac) : undefined; + _cached = { machineId, macMachineId }; + return _cached; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/exec-variants.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/exec-variants.d.ts new file mode 100644 index 00000000..16b5109c --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/exec-variants.d.ts @@ -0,0 +1,29 @@ +/** + * Canonical Cursor agent.v1 exec request/result pairs. + * + * Source of truth: Cursor CLI generated `agent/v1/exec_pb.js` plus the + * registrations in `agent-exec/dist/index.js`. Most request/result variants + * share a field number; Pi requests deliberately use result field +1. + * + * `handling` describes this provider, not the Cursor CLI: + * - opencode-tool: emitted through an advertised OpenCode tool + * - provider-control: answered directly on the held-open Run stream + * - unsupported: known Cursor-native capability with no safe AI SDK bridge + */ +export type CursorExecHandling = "opencode-tool" | "provider-control" | "unsupported"; +export type CursorExecVariant = { + requestField: number; + requestName: string; + resultField: number; + resultName: string; + handling: CursorExecHandling; +}; +export declare const CURSOR_EXEC_VARIANTS: readonly CursorExecVariant[]; +export declare function cursorExecVariantByRequestField(field: number): CursorExecVariant | undefined; +export declare function cursorExecVariantByRequestName(name: string): CursorExecVariant | undefined; +export declare function describeCursorExecVariant(field: number | undefined): string; +/** + * `ForceBackgroundShellResult` / `ForceBackgroundSubagentResult` status. + * Cursor CLI `ForceBackgroundStatus`: 0 unspecified, 1 ok, 2 error. + */ +export declare const FORCE_BACKGROUND_STATUS_ERROR = 2; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/exec-variants.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/exec-variants.js new file mode 100644 index 00000000..4a91e89c --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/exec-variants.js @@ -0,0 +1,70 @@ +export const CURSOR_EXEC_VARIANTS = [ + { requestField: 2, requestName: "shell_args", resultField: 2, resultName: "shell_result", handling: "opencode-tool" }, + { requestField: 3, requestName: "write_args", resultField: 3, resultName: "write_result", handling: "opencode-tool" }, + { requestField: 4, requestName: "delete_args", resultField: 4, resultName: "delete_result", handling: "opencode-tool" }, + { requestField: 5, requestName: "grep_args", resultField: 5, resultName: "grep_result", handling: "opencode-tool" }, + { requestField: 7, requestName: "read_args", resultField: 7, resultName: "read_result", handling: "opencode-tool" }, + { requestField: 8, requestName: "ls_args", resultField: 8, resultName: "ls_result", handling: "opencode-tool" }, + // Dedicated Cursor read_lints (#9) is not planned. OpenCode only presents + // LSP errors on write/edit/apply_patch output plus GET /lsp status / navigation. + { requestField: 9, requestName: "diagnostics_args", resultField: 9, resultName: "diagnostics_result", handling: "unsupported" }, + { requestField: 10, requestName: "request_context_args", resultField: 10, resultName: "request_context_result", handling: "provider-control" }, + { requestField: 11, requestName: "mcp_args", resultField: 11, resultName: "mcp_result", handling: "opencode-tool" }, + { requestField: 14, requestName: "shell_stream_args", resultField: 14, resultName: "shell_stream", handling: "opencode-tool" }, + { requestField: 16, requestName: "background_shell_spawn_args", resultField: 16, resultName: "background_shell_spawn_result", handling: "opencode-tool" }, + { requestField: 17, requestName: "list_mcp_resources_exec_args", resultField: 17, resultName: "list_mcp_resources_exec_result", handling: "provider-control" }, + { requestField: 18, requestName: "read_mcp_resource_exec_args", resultField: 18, resultName: "read_mcp_resource_exec_result", handling: "provider-control" }, + { requestField: 20, requestName: "fetch_args", resultField: 20, resultName: "fetch_result", handling: "unsupported" }, + { requestField: 21, requestName: "record_screen_args", resultField: 21, resultName: "record_screen_result", handling: "unsupported" }, + { requestField: 22, requestName: "computer_use_args", resultField: 22, resultName: "computer_use_result", handling: "unsupported" }, + { requestField: 23, requestName: "write_shell_stdin_args", resultField: 23, resultName: "write_shell_stdin_result", handling: "unsupported" }, + { requestField: 27, requestName: "execute_hook_args", resultField: 27, resultName: "execute_hook_result", handling: "unsupported" }, + { requestField: 28, requestName: "subagent_args", resultField: 28, resultName: "subagent_result", handling: "opencode-tool" }, + { requestField: 29, requestName: "redacted_read_args", resultField: 29, resultName: "redacted_read_result", handling: "unsupported" }, + { requestField: 30, requestName: "force_background_shell_args", resultField: 30, resultName: "force_background_shell_result", handling: "unsupported" }, + { requestField: 31, requestName: "force_background_subagent_args", resultField: 31, resultName: "force_background_subagent_result", handling: "unsupported" }, + { requestField: 36, requestName: "mcp_state_exec_args", resultField: 36, resultName: "mcp_state_exec_result", handling: "provider-control" }, + { requestField: 37, requestName: "subagent_await_args", resultField: 37, resultName: "subagent_await_result", handling: "unsupported" }, + // OpenCode has no Auto-review / `--auto-review` mode. Not planned: keep the typed soft-deny. + { requestField: 38, requestName: "smart_mode_classifier_args", resultField: 38, resultName: "smart_mode_classifier_result", handling: "unsupported" }, + { requestField: 40, requestName: "canvas_diagnostics_args", resultField: 40, resultName: "canvas_diagnostics_result", handling: "unsupported" }, + { requestField: 41, requestName: "shell_allowlist_precheck_args", resultField: 41, resultName: "shell_allowlist_precheck_result", handling: "unsupported" }, + { requestField: 42, requestName: "mcp_allowlist_precheck_args", resultField: 42, resultName: "mcp_allowlist_precheck_result", handling: "unsupported" }, + { requestField: 43, requestName: "web_fetch_allowlist_precheck_args", resultField: 43, resultName: "web_fetch_allowlist_precheck_result", handling: "unsupported" }, + { requestField: 44, requestName: "git_diff_request", resultField: 44, resultName: "git_diff_response", handling: "provider-control" }, + { requestField: 45, requestName: "pi_read_args", resultField: 46, resultName: "pi_read_result", handling: "opencode-tool" }, + { requestField: 46, requestName: "pi_bash_args", resultField: 47, resultName: "pi_bash_result", handling: "opencode-tool" }, + { requestField: 47, requestName: "pi_edit_args", resultField: 48, resultName: "pi_edit_result", handling: "opencode-tool" }, + { requestField: 48, requestName: "pi_write_args", resultField: 49, resultName: "pi_write_result", handling: "opencode-tool" }, + { requestField: 49, requestName: "pi_grep_args", resultField: 50, resultName: "pi_grep_result", handling: "opencode-tool" }, + { requestField: 50, requestName: "pi_find_args", resultField: 51, resultName: "pi_find_result", handling: "opencode-tool" }, + { requestField: 51, requestName: "pi_ls_args", resultField: 52, resultName: "pi_ls_result", handling: "opencode-tool" }, + // Mini-SWE request #52 pairs with ShellResult at #55 (Pi-style offset; #55 + // request is accept_hook_additional_contexts, not a variant). + // Mini-SWE / SWE-agent bash is not planned (same surface as Auto-review). Keep the typed soft-deny. + { requestField: 52, requestName: "mini_swe_agent_bash_args", resultField: 55, resultName: "mini_swe_agent_bash_result", handling: "unsupported" }, + { requestField: 53, requestName: "conversation_search_args", resultField: 53, resultName: "conversation_search_result", handling: "unsupported" }, + { requestField: 54, requestName: "agent_store_conflict_args", resultField: 54, resultName: "agent_store_conflict_result", handling: "unsupported" }, + { requestField: 56, requestName: "adopt_args", resultField: 56, resultName: "adopt_result", handling: "unsupported" }, +]; +const BY_REQUEST_FIELD = new Map(CURSOR_EXEC_VARIANTS.map((variant) => [variant.requestField, variant])); +const BY_REQUEST_NAME = new Map(CURSOR_EXEC_VARIANTS.map((variant) => [variant.requestName, variant])); +export function cursorExecVariantByRequestField(field) { + return BY_REQUEST_FIELD.get(field); +} +export function cursorExecVariantByRequestName(name) { + return BY_REQUEST_NAME.get(name); +} +export function describeCursorExecVariant(field) { + if (field === undefined) + return "unknown request field"; + const variant = cursorExecVariantByRequestField(field); + if (!variant) + return `unknown request field #${field}`; + return `${variant.requestName} (request field #${variant.requestField}, expected result ${variant.resultName} field #${variant.resultField}, handling=${variant.handling})`; +} +/** + * `ForceBackgroundShellResult` / `ForceBackgroundSubagentResult` status. + * Cursor CLI `ForceBackgroundStatus`: 0 unspecified, 1 ok, 2 error. + */ +export const FORCE_BACKGROUND_STATUS_ERROR = 2; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/framing.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/framing.d.ts new file mode 100644 index 00000000..fcfa07e5 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/framing.d.ts @@ -0,0 +1,10 @@ +export declare const FLAG_GZIP = 1; +export declare const FLAG_END_STREAM = 2; +export type Frame = { + flags: number; + payload: Uint8Array; +}; +export declare function encodeFrame(flags: number, payload: Uint8Array): Uint8Array; +export declare function decodeFramePayload(frame: Frame): Uint8Array; +export declare function streamFrames(buffer: Uint8Array): Generator; +export declare function asyncStreamFrames(stream: ReadableStream): AsyncGenerator; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/framing.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/framing.js new file mode 100644 index 00000000..08f8ae8e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/framing.js @@ -0,0 +1,90 @@ +import { gunzipSync, gzipSync } from "node:zlib"; +export const FLAG_GZIP = 0x01; +export const FLAG_END_STREAM = 0x02; +export function encodeFrame(flags, payload) { + let data = payload; + if (flags & FLAG_GZIP) { + data = gzipSync(data); + } + const len = data.length; + const header = new Uint8Array(5); + header[0] = flags; + header[1] = (len >> 24) & 0xff; + header[2] = (len >> 16) & 0xff; + header[3] = (len >> 8) & 0xff; + header[4] = len & 0xff; + const out = new Uint8Array(5 + len); + out.set(header, 0); + out.set(data, 5); + return out; +} +export function decodeFramePayload(frame) { + return frame.flags & FLAG_GZIP ? gunzipSync(frame.payload) : frame.payload; +} +export function* streamFrames(buffer) { + let offset = 0; + while (offset + 5 <= buffer.length) { + const flags = buffer[offset]; + const len = ((buffer[offset + 1] << 24) | + (buffer[offset + 2] << 16) | + (buffer[offset + 3] << 8) | + buffer[offset + 4]) >>> 0; + const start = offset + 5; + if (start + len > buffer.length) + break; + yield { flags, payload: buffer.subarray(start, start + len) }; + offset = start + len; + } +} +export async function* asyncStreamFrames(stream) { + const reader = stream.getReader(); + const chunks = []; + let pendingLength = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done && chunks.length === 0) + return; + if (value) { + chunks.push(value); + pendingLength += value.length; + } + // Try to parse frames from accumulated data + const merged = mergeChunks(chunks); + const frames = Array.from(streamFrames(merged)); + const consumed = frames.reduce((sum, f) => sum + 5 + f.payload.length, 0); + if (frames.length > 0) { + for (const frame of frames) { + yield frame; + } + // Keep any trailing bytes for next iteration + const remaining = merged.subarray(consumed); + chunks.length = 0; + if (remaining.length > 0) { + chunks.push(remaining); + pendingLength = remaining.length; + } + else { + pendingLength = 0; + } + } + if (done) + return; + } + } + finally { + reader.releaseLock(); + } +} +function mergeChunks(chunks) { + if (chunks.length === 1) + return chunks[0]; + const total = chunks.reduce((s, c) => s + c.length, 0); + const merged = new Uint8Array(total); + let offset = 0; + for (const c of chunks) { + merged.set(c, offset); + offset += c.length; + } + return merged; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/generate-image.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/generate-image.d.ts new file mode 100644 index 00000000..fbb3cd35 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/generate-image.d.ts @@ -0,0 +1,54 @@ +/** + * Cursor-native GenerateImage. + * + * Cursor generates the image on its own servers, then writes it to the client + * exactly like any other file — Cursor CLI's agent issues + * `WriteArgs{path, file_bytes, return_file_content_after_write: false}` to + * `/assets/` and reads the + * client's `WriteResult` back, checking for `permission_denied` + * (`agent-cli-local/src/index.ts:84752-84776`). + * + * The display `generate_image_tool_call.result.image_data` is **not** the write + * channel: the CLI feeds it to its terminal preview cache and falls back to + * reading the path off disk, so the file already exists by then. Never write + * from that frame — the bytes would be written twice. + * + * So the client-side work is: approve the interaction, then honour a binary + * write. OpenCode's `write` tool cannot carry bytes (string content, BOM split, + * diff, `Format.file()`), so the bytes are staged (`../image-staging.ts`) and + * committed by the `cursor_image_save` plugin tool, which raises OpenCode's + * `edit` permission. A refusal returns Cursor's own `permission_denied`. + */ +/** Host tool id that commits a staged image. Registered by the classic plugin. */ +export declare const CURSOR_IMAGE_SAVE_TOOL = "cursor_image_save"; +/** Cursor's agent always writes generated images into this subdirectory. */ +export declare const CURSOR_IMAGE_ASSETS_DIR = "assets"; +export type DecodedGenerateImageQuery = { + description: string; + /** Target path as Cursor asked for it; may be relative or empty. */ + filePath: string; + referenceImagePaths: string[]; + toolCallId: string; +}; +/** Cursor names the target file; its extension is the only mime signal sent. */ +export declare function imageMimeForPath(filePath: string): string; +/** Decode a `generate_image_request_query` body, or undefined when unusable. */ +export declare function decodeGenerateImageQuery(queryBytes: Uint8Array): DecodedGenerateImageQuery | undefined; +export type ImageWriteRoots = { + /** Session workspace root (`request_context.env.workspace_paths[0]`). */ + workspaceRoot: string; + /** What this provider advertises as `project_folder` / `workspace_project_dir`. */ + projectDir: string; +}; +/** + * Map Cursor's intended write target onto a real location under this host. + * + * Cursor composes the path from the `project_folder` this provider advertised, + * so an untouched target already resolves under the host cache project + * directory and is kept as-is. A target inside the workspace is also honoured + * verbatim — that is a location the user can see. Anything else (a path built + * from a folder we never advertised, or an attempted escape) is rebased onto + * `/assets/` rather than refused, so the model still gets + * a file where it expects one instead of a failed generation. + */ +export declare function remapCursorImageWritePath(target: string, roots: ImageWriteRoots): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/generate-image.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/generate-image.js new file mode 100644 index 00000000..d6561553 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/generate-image.js @@ -0,0 +1,101 @@ +/** + * Cursor-native GenerateImage. + * + * Cursor generates the image on its own servers, then writes it to the client + * exactly like any other file — Cursor CLI's agent issues + * `WriteArgs{path, file_bytes, return_file_content_after_write: false}` to + * `/assets/` and reads the + * client's `WriteResult` back, checking for `permission_denied` + * (`agent-cli-local/src/index.ts:84752-84776`). + * + * The display `generate_image_tool_call.result.image_data` is **not** the write + * channel: the CLI feeds it to its terminal preview cache and falls back to + * reading the path off disk, so the file already exists by then. Never write + * from that frame — the bytes would be written twice. + * + * So the client-side work is: approve the interaction, then honour a binary + * write. OpenCode's `write` tool cannot carry bytes (string content, BOM split, + * diff, `Format.file()`), so the bytes are staged (`../image-staging.ts`) and + * committed by the `cursor_image_save` plugin tool, which raises OpenCode's + * `edit` permission. A refusal returns Cursor's own `permission_denied`. + */ +import path from "node:path"; +import { decodeMessageSparse } from "./messages.js"; +/** Host tool id that commits a staged image. Registered by the classic plugin. */ +export const CURSOR_IMAGE_SAVE_TOOL = "cursor_image_save"; +/** Cursor's agent always writes generated images into this subdirectory. */ +export const CURSOR_IMAGE_ASSETS_DIR = "assets"; +const IMAGE_MIME_BY_EXTENSION = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", +}; +/** Cursor names the target file; its extension is the only mime signal sent. */ +export function imageMimeForPath(filePath) { + const dot = path.basename(filePath).lastIndexOf("."); + if (dot <= 0) + return "image/png"; + return IMAGE_MIME_BY_EXTENSION[path.extname(filePath).toLowerCase()] ?? "image/png"; +} +function asRecord(value) { + return value && typeof value === "object" && !Array.isArray(value) + ? value + : undefined; +} +function str(value) { + return typeof value === "string" ? value : ""; +} +/** Decode a `generate_image_request_query` body, or undefined when unusable. */ +export function decodeGenerateImageQuery(queryBytes) { + let decoded; + try { + decoded = decodeMessageSparse("GenerateImageRequestQuery", queryBytes); + } + catch { + return undefined; + } + const args = asRecord(decoded.args); + if (!args) + return undefined; + const description = str(args.description); + if (!description) + return undefined; + return { + description, + filePath: str(args.file_path), + referenceImagePaths: Array.isArray(args.reference_image_paths) + ? args.reference_image_paths.filter((item) => typeof item === "string") + : [], + toolCallId: str(decoded.tool_call_id), + }; +} +function isInside(root, target) { + const relative = path.relative(path.resolve(root), path.resolve(target)); + return relative !== "" && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); +} +/** + * Map Cursor's intended write target onto a real location under this host. + * + * Cursor composes the path from the `project_folder` this provider advertised, + * so an untouched target already resolves under the host cache project + * directory and is kept as-is. A target inside the workspace is also honoured + * verbatim — that is a location the user can see. Anything else (a path built + * from a folder we never advertised, or an attempted escape) is rebased onto + * `/assets/` rather than refused, so the model still gets + * a file where it expects one instead of a failed generation. + */ +export function remapCursorImageWritePath(target, roots) { + const projectDir = path.resolve(roots.projectDir); + const assets = path.join(projectDir, CURSOR_IMAGE_ASSETS_DIR); + const fallback = path.join(assets, path.basename(target) || "generated-image.png"); + if (!target) + return fallback; + const absolute = path.isAbsolute(target) ? target : path.resolve(projectDir, target); + if (isInside(projectDir, absolute)) + return path.resolve(absolute); + if (roots.workspaceRoot && isInside(roots.workspaceRoot, absolute)) + return path.resolve(absolute); + return fallback; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/git-diff.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/git-diff.d.ts new file mode 100644 index 00000000..026a3a7f --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/git-diff.d.ts @@ -0,0 +1,33 @@ +/** aiserver.v1.GetDiffRequest.OutputFormat */ +export declare const GIT_DIFF_FORMAT_UNSPECIFIED = 0; +export declare const GIT_DIFF_FORMAT_NAME_STATUS = 1; +export declare const GIT_DIFF_FORMAT_NAME_STATUS_AND_NUMSTAT = 2; +export declare const GIT_DIFF_FORMAT_FILE_DIFFS = 3; +export declare const GIT_DIFF_FORMAT_DIFFS_WITH_BEFORE_AND_AFTER = 4; +export type GitDiffChunk = { + content: string; + lines: string[]; + old_start: number; + old_lines: number; + new_start: number; + new_lines: number; +}; +export type GitFileDiff = { + from: string; + to: string; + chunks: GitDiffChunk[]; + added: number; + removed: number; +}; +/** Parse `git diff --no-color` into aiserver.v1.FileDiff rows (CLI `Oc` / `FR`). */ +export declare function parseUnifiedDiff(text: string): GitFileDiff[]; +export declare function executeGitDiff(request: Record, workspaceRoot: string): Promise>; +/** + * Answer exec #44. GetDiffResponse has no error oneof — failures use + * exec_client_control_message.throw, matching Cursor CLI `class Dc`. + */ +export declare function buildGitDiffExecMessages(input: { + execId: number; + request: Record; + workspaceRoot: string; +}): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/git-diff.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/git-diff.js new file mode 100644 index 00000000..e7036ced --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/git-diff.js @@ -0,0 +1,372 @@ +import { execFile } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { promisify } from "node:util"; +import { encodeMessage } from "./messages.js"; +const execFileAsync = promisify(execFile); +/** aiserver.v1.GetDiffRequest.OutputFormat */ +export const GIT_DIFF_FORMAT_UNSPECIFIED = 0; +export const GIT_DIFF_FORMAT_NAME_STATUS = 1; +export const GIT_DIFF_FORMAT_NAME_STATUS_AND_NUMSTAT = 2; +export const GIT_DIFF_FORMAT_FILE_DIFFS = 3; +export const GIT_DIFF_FORMAT_DIFFS_WITH_BEFORE_AND_AFTER = 4; +const DEFAULT_REMOTE_BRANCHES = ["main", "master", "develop"]; +const GIT_TIMEOUT_MS = 30_000; +const GIT_MAX_BUFFER = 32 * 1024 * 1024; +const GIT_ENV = { + ...process.env, + GIT_TERMINAL_PROMPT: "0", + GIT_OPTIONAL_LOCKS: "0", +}; +function gitErrorMessage(error, fallback) { + if (!error || typeof error !== "object") + return fallback; + const err = error; + const stderr = typeof err.stderr === "string" ? err.stderr.trim() : ""; + if (stderr) + return stderr.split("\n")[0] ?? stderr; + if (typeof err.message === "string" && err.message) + return err.message; + return fallback; +} +async function git(cwd, args, options) { + try { + const { stdout, stderr } = await execFileAsync("git", args, { + cwd, + encoding: "utf8", + timeout: GIT_TIMEOUT_MS, + maxBuffer: GIT_MAX_BUFFER, + env: GIT_ENV, + }); + return { + stdout: options?.trim === false ? stdout : stdout.trimEnd(), + stderr: stderr ?? "", + code: 0, + }; + } + catch (error) { + const err = error; + const code = typeof err.code === "number" ? err.code : 1; + if (options?.ignoreExit) { + return { + stdout: options.trim === false ? (err.stdout ?? "") : (err.stdout ?? "").trimEnd(), + stderr: err.stderr ?? "", + code, + }; + } + throw new Error(gitErrorMessage(error, `git ${args.join(" ")} failed`)); + } +} +function str(value) { + return typeof value === "string" ? value : ""; +} +function num(value) { + if (typeof value === "number" && Number.isFinite(value)) + return value; + if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) { + return Number(value); + } + return undefined; +} +function resolveGitCwd(requestCwd, workspaceRoot) { + const root = path.resolve(workspaceRoot || process.cwd()); + const requested = requestCwd.trim(); + if (!requested) + return root; + return path.isAbsolute(requested) ? path.resolve(requested) : path.resolve(root, requested); +} +async function resolveDefaultBase(cwd) { + try { + const head = (await git(cwd, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).stdout.trim(); + if (head) + return head; + } + catch { + /* try origin/ */ + } + for (const name of DEFAULT_REMOTE_BRANCHES) { + const ref = `origin/${name}`; + try { + await git(cwd, ["rev-parse", "--verify", ref]); + return ref; + } + catch { + /* next candidate */ + } + } + try { + const remotes = (await git(cwd, ["branch", "-r"])).stdout.split("\n") + .map((line) => line.trim()) + .find((line) => line.startsWith("origin/")); + if (remotes) + return remotes; + } + catch { + /* try init.defaultBranch */ + } + try { + const configured = (await git(cwd, ["config", "--get", "init.defaultBranch"])).stdout.trim(); + if (configured) + return configured; + } + catch { + /* no default */ + } + throw new Error("Could not determine default branch"); +} +function stripDiffPrefix(value) { + return value.replace(/^[abiwco12]\//, ""); +} +function parseDiffGitPaths(line) { + if (!line.startsWith("diff --git ")) + return undefined; + const rest = line.slice("diff --git ".length).trim(); + const match = rest.match(/^(?:[abiwco12]\/)?(.+?) (?:[abiwco12]\/)?(.+)$/); + if (!match) + return undefined; + return { from: stripDiffPrefix(match[1]), to: stripDiffPrefix(match[2]) }; +} +function parsePathHeader(line) { + const body = line.replace(/^(---|\+\+\+)\s+/, ""); + const tab = body.indexOf("\t"); + const raw = (tab >= 0 ? body.slice(0, tab) : body).trim().replace(/^["']|["']$/g, ""); + if (raw === "/dev/null") + return "/dev/null"; + return stripDiffPrefix(raw); +} +/** Parse `git diff --no-color` into aiserver.v1.FileDiff rows (CLI `Oc` / `FR`). */ +export function parseUnifiedDiff(text) { + if (!text || /^\s+$/.test(text)) + return []; + const diffs = []; + let current; + let chunk; + let remainingOld = 0; + let remainingNew = 0; + let oldLine = 0; + let newLine = 0; + let inHunk = false; + const startFile = (from = "", to = "") => { + current = { from, to, chunks: [], added: 0, removed: 0 }; + diffs.push(current); + chunk = undefined; + inHunk = false; + }; + const ensureFile = () => { + if (!current) + startFile(); + }; + const appendNoNewline = (line) => { + if (!chunk || chunk.lines.length === 0) + return; + chunk.lines.push(line); + }; + for (const line of text.split("\n")) { + if (inHunk && chunk && current) { + if (line === "\\ No newline at end of file") { + appendNoNewline(line); + continue; + } + if (line.startsWith("-")) { + chunk.lines.push(line); + current.removed++; + oldLine++; + remainingOld--; + } + else if (line.startsWith("+")) { + chunk.lines.push(line); + current.added++; + newLine++; + remainingNew--; + } + else if (line.startsWith(" ") || line === "") { + chunk.lines.push(line.length === 0 ? " " : line); + oldLine++; + newLine++; + remainingOld--; + remainingNew--; + } + else { + inHunk = false; + } + if (inHunk && remainingOld <= 0 && remainingNew <= 0) + inHunk = false; + if (inHunk) + continue; + } + if (line.startsWith("diff --git ")) { + const paths = parseDiffGitPaths(line); + startFile(paths?.from ?? "", paths?.to ?? ""); + continue; + } + if (line.startsWith("new file mode ")) { + ensureFile(); + current.from = "/dev/null"; + continue; + } + if (line.startsWith("deleted file mode ")) { + ensureFile(); + current.to = "/dev/null"; + continue; + } + if (line.startsWith("--- ")) { + ensureFile(); + current.from = parsePathHeader(line); + continue; + } + if (line.startsWith("+++ ")) { + ensureFile(); + current.to = parsePathHeader(line); + continue; + } + const hunk = /^@@\s+-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/.exec(line); + if (hunk) { + ensureFile(); + oldLine = Number(hunk[1]); + newLine = Number(hunk[3]); + remainingOld = hunk[2] !== undefined ? Number(hunk[2]) : 1; + remainingNew = hunk[4] !== undefined ? Number(hunk[4]) : 1; + chunk = { + content: line, + lines: [], + old_start: Number(hunk[1]), + old_lines: remainingOld, + new_start: Number(hunk[3]), + new_lines: remainingNew, + }; + current.chunks.push(chunk); + inHunk = true; + } + } + return diffs; +} +function effectiveFormat(raw) { + switch (raw) { + case GIT_DIFF_FORMAT_NAME_STATUS: + case GIT_DIFF_FORMAT_NAME_STATUS_AND_NUMSTAT: + case GIT_DIFF_FORMAT_FILE_DIFFS: + case GIT_DIFF_FORMAT_DIFFS_WITH_BEFORE_AND_AFTER: + return raw; + default: + return GIT_DIFF_FORMAT_FILE_DIFFS; + } +} +async function collectUntrackedDiffs(cwd, maxFiles) { + if (maxFiles <= 0) + return []; + const listed = (await git(cwd, ["ls-files", "--others", "--exclude-standard"])).stdout + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .slice(0, maxFiles); + const extra = []; + for (const file of listed) { + const raw = await git(cwd, ["diff", "--no-color", "--no-index", "/dev/null", file], { + ignoreExit: true, + trim: false, + }); + extra.push(...parseUnifiedDiff(raw.stdout)); + } + return extra; +} +export async function executeGitDiff(request, workspaceRoot) { + const cwd = resolveGitCwd(str(request.cwd), workspaceRoot); + if (!fs.existsSync(cwd)) { + throw new Error(`git_diff cwd does not exist: ${cwd}`); + } + const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"]).catch((error) => { + throw new Error(gitErrorMessage(error, "not a git repository")); + }); + if (inside.stdout.trim() !== "true") { + throw new Error("not a git repository"); + } + const ref = str(request.ref); + let base = str(request.base_ref); + if (!base) + base = await resolveDefaultBase(cwd); + if (request.merge_base === true) { + base = (await git(cwd, ["merge-base", base, ref || "HEAD"])).stdout.trim(); + } + const args = ["diff", "--no-color"]; + if (request.include_space_changes !== true) + args.push("--ignore-space-change"); + const contextLines = num(request.unified_context_lines); + if (contextLines !== undefined && contextLines > 0) + args.push(`-U${contextLines}`); + args.push(base); + if (ref) + args.push(ref); + const targetPaths = Array.isArray(request.target_paths) + ? request.target_paths.filter((item) => typeof item === "string" && item.length > 0) + : []; + if (targetPaths.length > 0) { + args.push("--"); + args.push(...targetPaths); + } + const diffText = (await git(cwd, args, { trim: false })).stdout; + const diffs = parseUnifiedDiff(diffText); + diffs.push(...await collectUntrackedDiffs(cwd, num(request.max_untracked_files) ?? 0)); + const format = effectiveFormat(num(request.output_format)); + if (format !== GIT_DIFF_FORMAT_FILE_DIFFS && format !== GIT_DIFF_FORMAT_DIFFS_WITH_BEFORE_AND_AFTER) { + for (const diff of diffs) + diff.chunks = []; + } + const response = { + diff: { + diffs, + diff_type: 0, + }, + }; + if (request.return_head_sha === true) { + response.head_sha = (await git(cwd, ["rev-parse", "HEAD"])).stdout.trim(); + const status = (await git(cwd, ["status", "--porcelain=v1", "--untracked-files=all"])).stdout.trim(); + response.has_uncommitted_changes = status.length > 0; + } + return response; +} +function throwAndClose(execId, error) { + return [ + encodeMessage("AgentClientMessage", { + exec_client_control_message: { + throw: { id: execId, error }, + }, + }), + encodeMessage("AgentClientMessage", { + exec_client_control_message: { + stream_close: { id: execId }, + }, + }), + ]; +} +/** + * Answer exec #44. GetDiffResponse has no error oneof — failures use + * exec_client_control_message.throw, matching Cursor CLI `class Dc`. + */ +export async function buildGitDiffExecMessages(input) { + let response; + try { + response = await executeGitDiff(input.request, input.workspaceRoot); + } + catch (error) { + return throwAndClose(input.execId, error.message || "git_diff failed"); + } + const responseBytes = encodeMessage("GetDiffResponse", response); + const maxBytes = num(input.request.max_response_bytes) ?? 0; + if (maxBytes > 0 && responseBytes.byteLength > maxBytes) { + return throwAndClose(input.execId, `GetDiffResponseTooLarge: ${responseBytes.byteLength} bytes exceeds the ${maxBytes} byte limit`); + } + return [ + encodeMessage("AgentClientMessage", { + exec_client_message: { + id: input.execId, + local_execution_time_ms: 0, + git_diff_response: response, + }, + }), + encodeMessage("AgentClientMessage", { + exec_client_control_message: { + stream_close: { id: input.execId }, + }, + }), + ]; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/interactions.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/interactions.d.ts new file mode 100644 index 00000000..0bfccbb0 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/interactions.d.ts @@ -0,0 +1,132 @@ +import { type AskQuestionResultMessage, type DecodedAskQuestionQuery } from "./ask-question.js"; +import { type DecodedGenerateImageQuery } from "./generate-image.js"; +import { CURSOR_PLAN_STAGE_TOOL, type CreatePlanBridge, type DecodedCreatePlanQuery, createPlanApprovalQuestionInput } from "./create-plan.js"; +import { type DecodedSwitchModeQuery, type SwitchModeBridge, type SwitchModeHostTool } from "./switch-mode.js"; +export type InteractionQueryWireInfo = { + id?: number; + variantField?: number; + variantName?: string; + /** Raw bytes of the selected variant sub-message, when it is length-delimited. */ + variantBytes?: Uint8Array; +}; +export type HandledInteraction = { + id: number; + variantField: number; + variantName: string; + outcome: "rejected" | "acknowledged" | "failed" | "bridged" | "approved"; + /** + * Immediate reply for this query. Absent for bridged *synchronous* AskQuestion + * or SwitchMode: Cursor blocks until the host tool returns, exactly as its own + * CLI does while the user is choosing / approving the mode switch. + */ + reply?: Uint8Array; + /** Present when `outcome === "bridged"`: the question set to hand OpenCode. */ + askQuestion?: DecodedAskQuestionQuery; + /** + * Present when SwitchMode should mutate session mode — bridged to a host + * tool, or approved outright when entering plan mode needs none. Absent on + * lifecycle soft-acks (`ack`): those reply `approved{}` without recording + * a mode. The caller reads `bridge` to decide whether a tool call is emitted, + * and `args.targetModeId` to record the active Cursor mode when present. + */ + switchMode?: DecodedSwitchModeQuery & { + bridge: SwitchModeBridge; + toolName?: SwitchModeHostTool | "question"; + }; + /** + * Present when CreatePlan needs a host tool: either a native plan stage that + * owns the write *and* the approval, or the emulated `question` prompt the + * provider raises after writing the plan itself. `planUri` is set only for + * the emulated path, where the provider already knows the file it wrote. + */ + createPlan?: DecodedCreatePlanQuery & { + bridge: CreatePlanBridge; + toolName: typeof CURSOR_PLAN_STAGE_TOOL | "question"; + planUri?: string; + /** Filesystem path of the plan the provider just wrote (emulated path). */ + planPath?: string; + questionInput?: ReturnType; + /** The plan to show the user before they approve it (emulated path only). */ + planReview?: string; + }; + /** Present when an image generation was approved: the target to expect. */ + generateImage?: DecodedGenerateImageQuery; +}; +export type HandleInteractionQueryOptions = { + /** + * True when the host advertises `question` on this turn and tool calls are + * allowed, i.e. the provider can actually surface the prompt to the user. + */ + canBridgeAskQuestion?: boolean; + /** + * True when tool calls are allowed this turn (needed before advertising can + * be checked for plan_enter / plan_exit). + */ + allowTools?: boolean; + /** + * Host tool names advertised this turn. Used to resolve SwitchMode → + * plan_enter / plan_exit only when the matching tool is present. + */ + advertisedTools?: ReadonlySet | Iterable; + /** + * True when `cursor_image_save` is advertised, i.e. a generated image can be + * committed to disk under OpenCode's permission. Approving generation the + * provider cannot then save would spend the user's Cursor quota for nothing. + */ + canSaveGeneratedImage?: boolean; + /** + * Session workspace root used to place CreatePlan files via {@link hostPlansDir} + * (host project-config `plans/` in a git worktree, else host global data/plans). + */ + workspaceRoot?: string; + /** True when the advertised host plan-stage tool is available. */ + canBridgeCreatePlan?: boolean; + /** + * True when the provider has recorded an approved Cursor plan/spec mode for + * this session. A written plan then ends with an execution-approval prompt, + * which is the transition out of planning; outside plan mode there is none. + */ + planModeActive?: boolean; + /** + * Cursor unified mode currently recorded for this session. A SwitchMode to + * the mode already in effect needs no approval, matching Cursor's own IDE + * handler and keeping the plan-approval prompt from firing twice. + */ + activeCursorModeId?: string; +}; +export declare class UnsupportedInteractionQueryError extends Error { + constructor(info: InteractionQueryWireInfo); +} +/** + * Inspect the raw wrapper as well as the decoded object. protobufjs drops + * fields introduced by newer Cursor schemas; raw inspection lets us fail fast + * instead of accidentally restoring the heartbeat-only deadlock. + */ +export declare function inspectInteractionQueryWire(agentServerPayload: Uint8Array): InteractionQueryWireInfo; +/** + * Build the typed response required by Cursor's Run RPC. + * + * OpenCode has no Cursor UI callbacks, so UI-bound queries get a conservative + * headless policy (reject; ack a few no-UI cases). + * + * Bridged / persisted exceptions: + * - AskQuestion (#3) → OpenCode `question` tool + * - SwitchMode (#4) → OpenCode `plan_enter` / `plan_exit` when advertised + * - CreatePlan (#7) → host-calculated plan file via hostPlansDir (project-config + * `plans/` in a git worktree, else host global data/plans); empty args still + * get the CLI empty-`plan_uri` success ack + * - GenerateImage (#12) → approve when `cursor_image_save` is advertised + */ +export declare function handleInteractionQuery(query: Record, agentServerPayload: Uint8Array, options?: HandleInteractionQueryOptions): HandledInteraction; +/** `AgentClientMessage{interaction_response{ask_question_interaction_response}}`. */ +export declare function buildAskQuestionInteractionReply(id: number, result: AskQuestionResultMessage): Uint8Array; +/** `AgentClientMessage{interaction_response{switch_mode_request_response}}`. */ +export declare function buildSwitchModeInteractionReply(id: number, result: Record): Uint8Array; +/** `AgentClientMessage{interaction_response{create_plan_request_response}}`. */ +export declare function buildCreatePlanInteractionReply(id: number, result: Record): Uint8Array; +/** + * Deferred answers for a query already acknowledged with `async`. + * `originalArgs` must be the server's own `AskQuestionArgs` bytes so Cursor can + * correlate the completion with the tool call it raised. + */ +export declare function buildAsyncAskQuestionCompletion(originalToolCallId: string, originalArgs: Uint8Array, result: AskQuestionResultMessage): Uint8Array; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/interactions.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/interactions.js new file mode 100644 index 00000000..7f3433d6 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/interactions.js @@ -0,0 +1,409 @@ +import { encodeMessage } from "./messages.js"; +import { readAllFields } from "./struct.js"; +import { decodeAskQuestionQuery, MISSING_ARGS_REASON, MISSING_QUERY_REASON, } from "./ask-question.js"; +import { decodeGenerateImageQuery, } from "./generate-image.js"; +import { CURSOR_PLAN_STAGE_TOOL, createPlanApprovalQuestionInput, decodeCreatePlanQuery, renderPlanReviewMessage, resolveCreatePlanBridge, writeOpencodePlanFile, } from "./create-plan.js"; +import { decodeSwitchModeQuery, MISSING_ARGS_REASON as SWITCH_MODE_MISSING_ARGS_REASON, MISSING_QUERY_REASON as SWITCH_MODE_MISSING_QUERY_REASON, resolveSwitchModeBridge, switchModeApprovedResult, } from "./switch-mode.js"; +const HEADLESS_REASON = "This Cursor interaction requires UI approval and is not available through the OpenCode provider."; +/** + * Rejection used when AskQuestion cannot be bridged because the current + * OpenCode agent does not advertise `question` (subagents deny it by default, + * and compaction turns advertise no tools at all). Naming the actual reason + * keeps the model from concluding that asking is impossible in general. + */ +const ASK_QUESTION_UNAVAILABLE_REASON = "The OpenCode `question` tool is not available to the current agent, so questions " + + "cannot be shown to the user this turn. State the question in your reply instead."; +const variantNames = { + 2: "web_search_request_query", + 3: "ask_question_interaction_query", + 4: "switch_mode_request_query", + 7: "create_plan_request_query", + 8: "setup_vm_environment_args", + 9: "web_fetch_request_query", + 10: "pr_management_request_query", + 11: "mcp_auth_request_query", + 12: "generate_image_request_query", + 13: "replace_env_args", + 14: "connect_scm_request_query", +}; +export class UnsupportedInteractionQueryError extends Error { + constructor(info) { + const variant = info.variantField === undefined + ? "missing variant" + : `unsupported variant field #${info.variantField}`; + const id = info.id === undefined ? "missing id" : `id=${info.id}`; + super(`Cursor interaction query cannot be handled (${id}, ${variant})`); + this.name = "UnsupportedInteractionQueryError"; + } +} +/** + * Inspect the raw wrapper as well as the decoded object. protobufjs drops + * fields introduced by newer Cursor schemas; raw inspection lets us fail fast + * instead of accidentally restoring the heartbeat-only deadlock. + */ +export function inspectInteractionQueryWire(agentServerPayload) { + const queryBytes = readAllFields(agentServerPayload) + .find((field) => field.fn === 7 && field.wt === 2)?.bytes; + if (!queryBytes) + return {}; + // InteractionQuery.id is a proto3 uint32. Cursor commonly uses id=0, whose + // default scalar value is omitted from the wire; absence therefore means + // zero, not a malformed/missing correlation id. + let id = 0; + let variantField; + let variantBytes; + for (const field of readAllFields(queryBytes)) { + if (field.fn === 1 && field.wt === 0) + id = field.varint; + else if (field.wt === 2 && variantField === undefined) { + variantField = field.fn; + variantBytes = field.bytes; + } + } + return { + id, + variantField, + variantName: variantField === undefined ? undefined : variantNames[variantField], + variantBytes, + }; +} +/** + * Build the typed response required by Cursor's Run RPC. + * + * OpenCode has no Cursor UI callbacks, so UI-bound queries get a conservative + * headless policy (reject; ack a few no-UI cases). + * + * Bridged / persisted exceptions: + * - AskQuestion (#3) → OpenCode `question` tool + * - SwitchMode (#4) → OpenCode `plan_enter` / `plan_exit` when advertised + * - CreatePlan (#7) → host-calculated plan file via hostPlansDir (project-config + * `plans/` in a git worktree, else host global data/plans); empty args still + * get the CLI empty-`plan_uri` success ack + * - GenerateImage (#12) → approve when `cursor_image_save` is advertised + */ +export function handleInteractionQuery(query, agentServerPayload, options = {}) { + const info = inspectInteractionQueryWire(agentServerPayload); + if (info.id === undefined || info.variantField === undefined || !info.variantName) { + throw new UnsupportedInteractionQueryError(info); + } + if (typeof query.id === "number" && query.id !== info.id) { + throw new Error(`Cursor interaction query id mismatch: decoded=${query.id} wire=${info.id}`); + } + if (info.variantField === 3) { + return handleAskQuestionQuery(info.id, info.variantBytes, options); + } + if (info.variantField === 4) { + return handleSwitchModeQuery(info.id, info.variantBytes, options); + } + if (info.variantField === 7) { + return handleCreatePlanQuery(info.id, info.variantBytes, options); + } + if (info.variantField === 12) { + return handleGenerateImageQuery(info.id, info.variantBytes, options); + } + let response; + let outcome; + switch (info.variantField) { + case 2: + response = { web_search_request_response: { rejected: { reason: HEADLESS_REASON } } }; + outcome = "rejected"; + break; + case 8: + // OpenCode owns the local environment; there is no Cursor VM to create. + response = { setup_vm_environment_result: { success: {} } }; + outcome = "acknowledged"; + break; + case 9: + response = { web_fetch_request_response: { rejected: { reason: HEADLESS_REASON } } }; + outcome = "rejected"; + break; + case 10: + response = { pr_management_result: { rejected: { reason: HEADLESS_REASON } } }; + outcome = "rejected"; + break; + case 11: + response = { mcp_auth_request_response: { rejected: { reason: HEADLESS_REASON } } }; + outcome = "rejected"; + break; + case 13: + response = { + replace_env_result: { + failure: { + error_message: "Environment replacement is not supported by the OpenCode provider.", + setup_logs: "", + }, + }, + }; + outcome = "failed"; + break; + case 14: + response = { connect_scm_request_response: { rejected: { reason: HEADLESS_REASON } } }; + outcome = "rejected"; + break; + default: + throw new UnsupportedInteractionQueryError(info); + } + return { + id: info.id, + variantField: info.variantField, + variantName: info.variantName, + outcome, + reply: encodeMessage("AgentClientMessage", { + interaction_response: { id: info.id, ...response }, + }), + }; +} +/** + * AskQuestion is the one interaction OpenCode can genuinely satisfy, so it is + * translated instead of refused. Cursor CLI's own handler is mirrored: reject + * a malformed query, answer `async` immediately when the server asked for a + * non-blocking prompt, and otherwise hold the query open until the user picks. + */ +function handleAskQuestionQuery(id, variantBytes, options) { + const base = { id, variantField: 3, variantName: "ask_question_interaction_query" }; + const reject = (reason) => ({ + ...base, + outcome: "rejected", + reply: buildAskQuestionInteractionReply(id, { rejected: { reason } }), + }); + if (!variantBytes) + return reject(MISSING_QUERY_REASON); + const decoded = decodeAskQuestionQuery(variantBytes); + if (!decoded) + return reject(MISSING_ARGS_REASON); + if (!options.canBridgeAskQuestion) + return reject(ASK_QUESTION_UNAVAILABLE_REASON); + return { + ...base, + outcome: "bridged", + askQuestion: decoded, + // Async queries are unblocked now and answered later through a + // ConversationAction; synchronous ones keep Cursor waiting, which is what + // its CLI does while the user is still choosing. + reply: decoded.args.runAsync + ? buildAskQuestionInteractionReply(id, { async: {} }) + : undefined, + }; +} +/** `AgentClientMessage{interaction_response{ask_question_interaction_response}}`. */ +export function buildAskQuestionInteractionReply(id, result) { + return encodeMessage("AgentClientMessage", { + interaction_response: { + id, + ask_question_interaction_response: { result }, + }, + }); +} +/** + * SwitchMode is bridged to OpenCode `plan_enter` / `plan_exit` when advertised. + * Cursor CLI blocks until the user approves or rejects; we hold the query open + * until the host tool returns, then reply approved{} or rejected{reason}. + */ +function handleSwitchModeQuery(id, variantBytes, options) { + const base = { id, variantField: 4, variantName: "switch_mode_request_query" }; + const reject = (reason) => ({ + ...base, + outcome: "rejected", + reply: buildSwitchModeInteractionReply(id, { rejected: { reason } }), + }); + if (!variantBytes) + return reject(SWITCH_MODE_MISSING_QUERY_REASON); + const decoded = decodeSwitchModeQuery(variantBytes); + if (!decoded) + return reject(SWITCH_MODE_MISSING_ARGS_REASON); + const bridge = resolveSwitchModeBridge(decoded.args.targetModeId, { + allowTools: options.allowTools === true, + advertised: options.advertisedTools ?? [], + ...(options.activeCursorModeId ? { activeModeId: options.activeCursorModeId } : {}), + }); + if (bridge.kind === "reject") + return reject(bridge.reason); + // Lifecycle turn: approve on the wire so the model never sees a rejection, but + // do not attach switchMode — the real turn owns session mode. + if (bridge.kind === "ack") { + return { + ...base, + outcome: "acknowledged", + reply: buildSwitchModeInteractionReply(id, switchModeApprovedResult()), + }; + } + if (bridge.kind === "approve") { + // Entering plan mode needs no host tool, so Cursor is released at once and + // the caller records the mode for the reminder injected on later Runs. + return { + ...base, + outcome: "approved", + switchMode: { ...decoded, bridge }, + reply: buildSwitchModeInteractionReply(id, switchModeApprovedResult()), + }; + } + return { + ...base, + outcome: "bridged", + switchMode: { + ...decoded, + bridge, + toolName: bridge.kind === "native" ? bridge.toolName : "question", + }, + // Keep Cursor waiting until the host tool returns, matching CLI blocking on + // the mode-switch approval prompt. + reply: undefined, + }; +} +/** `AgentClientMessage{interaction_response{switch_mode_request_response}}`. */ +export function buildSwitchModeInteractionReply(id, result) { + return encodeMessage("AgentClientMessage", { + interaction_response: { + id, + switch_mode_request_response: result, + }, + }); +} +/** `AgentClientMessage{interaction_response{create_plan_request_response}}`. */ +export function buildCreatePlanInteractionReply(id, result) { + return encodeMessage("AgentClientMessage", { + interaction_response: { + id, + create_plan_request_response: { result }, + }, + }); +} +/** + * Deferred answers for a query already acknowledged with `async`. + * `originalArgs` must be the server's own `AskQuestionArgs` bytes so Cursor can + * correlate the completion with the tool call it raised. + */ +export function buildAsyncAskQuestionCompletion(originalToolCallId, originalArgs, result) { + return encodeMessage("AgentClientMessage", { + conversation_action: { + async_ask_question_completion_action: { + original_tool_call_id: originalToolCallId, + original_args: originalArgs, + result, + }, + }, + }); +} +/** + * CreatePlan persistence and execution approval. Cursor CLI writes + * `~/.cursor/plans/*.plan.md` with YAML frontmatter; this provider writes plain + * markdown under the host's calculated plans dir (`hostPlansDir` / path bridge) + * so switching models and hosts stays coherent. Empty / missing args keep the + * CLI empty-`plan_uri` success ack (nothing to write). + * + * Writing the plan never asks; executing it always does. Which channel raises + * that prompt is resolved from the advertised catalog by + * {@link resolveCreatePlanBridge}, and every channel reports the same outcome: + * `success` means the user approved execution, `error` means the plan was + * written but not accepted, so the model keeps planning. + */ +function handleCreatePlanQuery(id, variantBytes, options) { + const base = { id, variantField: 7, variantName: "create_plan_request_query" }; + const reply = (result) => ({ + ...base, + outcome: result.error ? "failed" : "acknowledged", + reply: encodeMessage("AgentClientMessage", { + interaction_response: { + id, + create_plan_request_response: { result }, + }, + }), + }); + // Missing / empty args: CLI headless fallback — success with empty plan_uri. + if (!variantBytes) + return reply({ success: {}, plan_uri: "" }); + const decoded = decodeCreatePlanQuery(variantBytes); + if (!decoded) + return reply({ success: {}, plan_uri: "" }); + const bridge = resolveCreatePlanBridge({ + allowTools: options.allowTools === true, + canStage: options.canBridgeCreatePlan === true, + planModeActive: options.planModeActive === true, + advertised: options.advertisedTools ?? [], + }); + // The host plan-stage tool owns the write and the approval prompt together. + if (bridge.kind === "stage") { + return { + ...base, + outcome: "bridged", + createPlan: { ...decoded, bridge, toolName: CURSOR_PLAN_STAGE_TOOL }, + reply: undefined, + }; + } + // A lifecycle turn (title generation, compaction) runs alongside the real one + // and would otherwise write a second, throwaway plan file for the same + // request. Ack it the CLI way instead of persisting anything. + if (options.allowTools !== true) + return reply({ success: {}, plan_uri: "" }); + const workspaceRoot = options.workspaceRoot?.trim(); + if (!workspaceRoot) { + return reply({ + error: { error: "CreatePlan requires a workspace root to write the plan file" }, + plan_uri: "", + }); + } + const written = writeOpencodePlanFile(decoded.args, workspaceRoot); + if (!written.ok) { + return reply({ error: { error: written.error }, plan_uri: "" }); + } + // The plan is on disk either way; only execution needs the user. Hold Cursor's + // query open while the host asks, exactly as the CLI blocks on its own prompt. + if (bridge.kind === "approve") { + return { + ...base, + outcome: "bridged", + createPlan: { + ...decoded, + bridge, + toolName: "question", + planUri: written.planUri, + planPath: written.planPath, + questionInput: createPlanApprovalQuestionInput(written.planPath), + planReview: renderPlanReviewMessage(written.markdown, written.planPath), + }, + reply: undefined, + }; + } + return reply({ success: {}, plan_uri: written.planUri }); +} +/** + * GenerateImage approval. Cursor renders its own confirmation in the CLI; here + * the meaningful gate is OpenCode's `edit` permission raised by + * `cursor_image_save` when the finished bytes are written, so generation itself + * is approved whenever the provider can actually commit the result. Approving + * without a commit path would spend Cursor quota on an image that then has + * nowhere to go. + */ +function handleGenerateImageQuery(id, variantBytes, options) { + const base = { id, variantField: 12, variantName: "generate_image_request_query" }; + const reject = (reason) => ({ + ...base, + outcome: "rejected", + reply: encodeMessage("AgentClientMessage", { + interaction_response: { id, generate_image_request_response: { rejected: { reason } } }, + }), + }); + if (!variantBytes) + return reject("Missing generate image query"); + const decoded = decodeGenerateImageQuery(variantBytes); + if (!decoded) + return reject("Missing generate image arguments"); + if (!options.canSaveGeneratedImage) { + return reject("This OpenCode agent cannot save a generated image, so generating one would " + + "produce nothing. Describe the image in your reply instead."); + } + return { + ...base, + outcome: "acknowledged", + generateImage: decoded, + reply: encodeMessage("AgentClientMessage", { + interaction_response: { + id, + // Cursor's CLI returns the description the user may have edited at the + // prompt. There is no text field on OpenCode's permission, so the + // model's own description is passed through unchanged. + generate_image_request_response: { approved: { description: decoded.description } }, + }, + }), + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/kv.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/kv.d.ts new file mode 100644 index 00000000..73964fd7 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/kv.d.ts @@ -0,0 +1,25 @@ +import type { CursorSession } from "../session.js"; +/** + * Handle a server `KvServerMessage` (AgentServerMessage #4). Cursor moves large + * payloads out-of-band via this blob channel: + * - `set_blob_args{blob_id, blob_data}` → server stores a blob on the client. + * We persist it (per-session + durable per conversation_id) and ACK. + * - `get_blob_args{blob_id}` → server asks the client for a blob it stored. + * We return `get_blob_result{blob_data}` (empty if unknown hash). + * The reply MUST be sent as `AgentClientMessage.kv_client_message` (field #3) on + * the same Run stream, echoing `id`. If we don't reply, the server hangs the + * turn (endless heartbeats, never any interaction_update) — the "no response" + * root cause. + * + * Returns the AgentClientMessage bytes to write back, or null if the message + * carried no get/set blob request. + */ +export declare function handleKvServerMessage(ksm: Record, session: CursorSession): { + reply: Uint8Array; + replyBlobBytes: number; + kind: "set" | "get"; + id: number; + blobIdHex: string; + found: boolean; + echoed?: boolean; +} | null; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/kv.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/kv.js new file mode 100644 index 00000000..3139345a --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/kv.js @@ -0,0 +1,83 @@ +import { encodeMessage } from "./messages.js"; +import { getConversationBlob, isBlobIdHash, setConversationBlob, } from "./blob-store.js"; +function hex(b) { + let s = ""; + for (let i = 0; i < b.length; i++) + s += b[i].toString(16).padStart(2, "0"); + return s; +} +/** + * Handle a server `KvServerMessage` (AgentServerMessage #4). Cursor moves large + * payloads out-of-band via this blob channel: + * - `set_blob_args{blob_id, blob_data}` → server stores a blob on the client. + * We persist it (per-session + durable per conversation_id) and ACK. + * - `get_blob_args{blob_id}` → server asks the client for a blob it stored. + * We return `get_blob_result{blob_data}` (empty if unknown hash). + * The reply MUST be sent as `AgentClientMessage.kv_client_message` (field #3) on + * the same Run stream, echoing `id`. If we don't reply, the server hangs the + * turn (endless heartbeats, never any interaction_update) — the "no response" + * root cause. + * + * Returns the AgentClientMessage bytes to write back, or null if the message + * carried no get/set blob request. + */ +export function handleKvServerMessage(ksm, session) { + const id = ksm.id ?? 0; + const setArgs = ksm.set_blob_args; + const getArgs = ksm.get_blob_args; + if (setArgs && setArgs.blob_id) { + const data = setArgs.blob_data ?? new Uint8Array(0); + const key = hex(setArgs.blob_id); + session.blobs.set(key, data); + // Survive across Run streams — required for checkpoint echo on turn 2+. + if (session.conversationId) { + setConversationBlob(session.conversationId, setArgs.blob_id, data); + } + return { + kind: "set", + id, + blobIdHex: key, + found: true, + replyBlobBytes: 0, + reply: encodeMessage("AgentClientMessage", { + kv_client_message: { id, set_blob_result: {} }, + }), + }; + } + if (getArgs && getArgs.blob_id) { + const key = hex(getArgs.blob_id); + const stored = session.blobs.get(key) ?? + (session.conversationId + ? getConversationBlob(session.conversationId, getArgs.blob_id) + : undefined); + let blobData; + let echoed = false; + let found = false; + if (stored) { + blobData = stored; + found = true; + } + else if (!isBlobIdHash(getArgs.blob_id)) { + // Content-as-id: server sometimes sends blob_id as the literal payload + // (e.g. inline JSON). Echo it. Never do this for 32-byte hashes — that + // made the server JSON.parse binary and return "Unexpected token". + blobData = getArgs.blob_id; + echoed = true; + } + else { + blobData = new Uint8Array(0); + } + return { + kind: "get", + id, + blobIdHex: key, + found, + echoed, + replyBlobBytes: blobData.length, + reply: encodeMessage("AgentClientMessage", { + kv_client_message: { id, get_blob_result: { blob_data: blobData } }, + }), + }; + } + return null; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/messages.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/messages.d.ts new file mode 100644 index 00000000..8013fbed --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/messages.d.ts @@ -0,0 +1,23 @@ +import protobuf from "protobufjs"; +export declare function createMessageTypes(): protobuf.Root; +export declare function getMessageTypes(): protobuf.Root; +export declare function encodeMessage(typeName: string, message: Record): Uint8Array; +export declare function decodeMessage>(typeName: string, data: Uint8Array): T; +/** Decode without inventing absent proto defaults (used for byte-stable snapshots). */ +export declare function decodeMessageSparse>(typeName: string, data: Uint8Array): T; +/** + * Diagnostic-only: walk AgentServerMessage.interaction_update(1).turn_ended(14) + * without going through our hand-written TurnEnded schema, so a `cache_write`/ + * `reasoning_tokens` question can be checked against the raw wire instead of + * guessed. Confirmed field ids 1-5 (input/output/cache_read/cache_write/ + * reasoning) decode correctly. Live captures routinely include `f4` explicitly + * as `0` while `f3` (cache_read) and `f5` (reasoning) are non-zero — Cursor's + * agent TurnEnded populates the field but does not report write counts here. + */ +export declare function debugWalkTurnEnded(payload: Uint8Array): string; +/** + * Decode a protobuf sub-message from a frame payload that wraps it in + * a top-level field key + length varint. Skips the outer wrapper and + * decodes the inner body as `typeName`. + */ +export declare function decodeWrappedMessage>(typeName: string, data: Uint8Array): T; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/messages.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/messages.js new file mode 100644 index 00000000..2adf58c6 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/messages.js @@ -0,0 +1,1810 @@ +import protobuf from "protobufjs"; +function addType(root, name, fields, oneofs) { + const t = new protobuf.Type(name); + for (const f of fields) { + t.add(new protobuf.Field(f.name, f.id, f.type, f.repeated ? "repeated" : undefined)); + } + for (const o of oneofs ?? []) { + t.add(new protobuf.OneOf(o.name, o.fields)); + } + root.add(t); + return t; +} +// ── Utility messages ── +export function createMessageTypes() { + const root = new protobuf.Root(); + addType(root, "TextDeltaUpdate", [ + { id: 1, name: "text", type: "string" }, + ]); + addType(root, "ThinkingDeltaUpdate", [ + { id: 1, name: "text", type: "string" }, + ]); + addType(root, "TurnEnded", [ + { id: 1, name: "input_tokens", type: "uint32" }, + { id: 2, name: "output_tokens", type: "uint32" }, + { id: 3, name: "cache_read", type: "uint32" }, + { id: 4, name: "cache_write", type: "uint32" }, + { id: 5, name: "reasoning_tokens", type: "uint32" }, + ]); + addType(root, "Heartbeat", []); + // Display ToolCall (interaction_update.tool_call_*) — agent.v1 oneof, not + // {tool_name,args} strings. Args-only wrappers are enough to bridge into + // OpenCode; result payloads are ignored on decode. + root.add(new protobuf.Enum("TodoStatus", { + TODO_STATUS_UNSPECIFIED: 0, + TODO_STATUS_PENDING: 1, + TODO_STATUS_IN_PROGRESS: 2, + TODO_STATUS_COMPLETED: 3, + TODO_STATUS_CANCELLED: 4, + })); + addType(root, "TodoItem", [ + { id: 1, name: "id", type: "string" }, + { id: 2, name: "content", type: "string" }, + { id: 3, name: "status", type: "TodoStatus" }, + { id: 4, name: "created_at", type: "int64" }, + { id: 5, name: "updated_at", type: "int64" }, + { id: 6, name: "dependencies", type: "string", repeated: true }, + ]); + addType(root, "UpdateTodosArgs", [ + { id: 1, name: "todos", type: "TodoItem", repeated: true }, + { id: 2, name: "merge", type: "bool" }, + ]); + addType(root, "UpdateTodosSuccess", [ + { id: 1, name: "todos", type: "TodoItem", repeated: true }, + { id: 2, name: "total_count", type: "int32" }, + { id: 3, name: "was_merge", type: "bool" }, + ]); + addType(root, "UpdateTodosError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "UpdateTodosResult", [ + { id: 1, name: "success", type: "UpdateTodosSuccess" }, + { id: 2, name: "error", type: "UpdateTodosError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "UpdateTodosToolCall", [ + { id: 1, name: "args", type: "UpdateTodosArgs" }, + { id: 2, name: "result", type: "UpdateTodosResult" }, + ]); + addType(root, "ReadToolArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "offset", type: "int32" }, + { id: 3, name: "limit", type: "int32" }, + ]); + addType(root, "ReadToolCall", [{ id: 1, name: "args", type: "ReadToolArgs" }]); + addType(root, "ShellToolCall", [{ id: 1, name: "args", type: "ShellArgs" }]); + addType(root, "DeleteToolCall", [{ id: 1, name: "args", type: "DeleteArgs" }]); + addType(root, "GlobToolCall", [{ id: 1, name: "args", type: "GlobArgs" }]); + addType(root, "GrepToolCall", [{ id: 1, name: "args", type: "GrepArgs" }]); + addType(root, "EditToolArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 6, name: "stream_content", type: "string" }, + ]); + addType(root, "EditToolCall", [{ id: 1, name: "args", type: "EditToolArgs" }]); + addType(root, "LsToolCall", [{ id: 1, name: "args", type: "LsArgs" }]); + addType(root, "McpToolCall", [{ id: 1, name: "args", type: "McpArgs" }]); + addType(root, "CreatePlanArgs", [ + { id: 1, name: "plan", type: "string" }, + { id: 2, name: "todos", type: "TodoItem", repeated: true }, + { id: 3, name: "overview", type: "string" }, + { id: 4, name: "name", type: "string" }, + { id: 5, name: "is_project", type: "bool" }, + ]); + addType(root, "CreatePlanToolCall", [{ id: 1, name: "args", type: "CreatePlanArgs" }]); + addType(root, "WebSearchToolArgs", [ + { id: 1, name: "search_term", type: "string" }, + { id: 2, name: "tool_call_id", type: "string" }, + ]); + addType(root, "WebSearchToolCall", [{ id: 1, name: "args", type: "WebSearchToolArgs" }]); + addType(root, "SubagentTypeUnspecified", []); + addType(root, "SubagentTypeComputerUse", []); + addType(root, "SubagentTypeCustom", [{ id: 1, name: "name", type: "string" }]); + addType(root, "SubagentTypeExplore", []); + addType(root, "SubagentTypeMediaReview", []); + addType(root, "SubagentTypeBash", []); + addType(root, "SubagentTypeBrowserUse", []); + addType(root, "SubagentTypeShell", []); + addType(root, "SubagentTypeVmSetupHelper", []); + addType(root, "SubagentTypeDebug", []); + addType(root, "SubagentTypeCursorGuide", []); + addType(root, "SubagentTypeWatchVideo", []); + addType(root, "SubagentType", [ + { id: 1, name: "unspecified", type: "SubagentTypeUnspecified" }, + { id: 2, name: "computer_use", type: "SubagentTypeComputerUse" }, + { id: 3, name: "custom", type: "SubagentTypeCustom" }, + { id: 4, name: "explore", type: "SubagentTypeExplore" }, + { id: 5, name: "media_review", type: "SubagentTypeMediaReview" }, + { id: 6, name: "bash", type: "SubagentTypeBash" }, + { id: 7, name: "browser_use", type: "SubagentTypeBrowserUse" }, + { id: 8, name: "shell", type: "SubagentTypeShell" }, + { id: 9, name: "vm_setup_helper", type: "SubagentTypeVmSetupHelper" }, + { id: 10, name: "debug", type: "SubagentTypeDebug" }, + { id: 11, name: "cursor_guide", type: "SubagentTypeCursorGuide" }, + { id: 12, name: "watch_video", type: "SubagentTypeWatchVideo" }, + ], [{ + name: "type", + fields: [ + "unspecified", "computer_use", "custom", "explore", "media_review", "bash", + "browser_use", "shell", "vm_setup_helper", "debug", "cursor_guide", "watch_video", + ], + }]); + addType(root, "TaskToolArgs", [ + { id: 1, name: "description", type: "string" }, + { id: 2, name: "prompt", type: "string" }, + { id: 3, name: "subagent_type", type: "SubagentType" }, + { id: 4, name: "model", type: "string" }, + { id: 5, name: "resume", type: "string" }, + { id: 6, name: "agent_id", type: "string" }, + ]); + addType(root, "TaskToolCall", [{ id: 1, name: "args", type: "TaskToolArgs" }]); + // Native subagent execution is separate from the display-only TaskToolCall. + // Cursor asks the local client to run the task through ExecServerMessage #28 + // and waits for the correlated SubagentResult at ExecClientMessage #28. + addType(root, "SubagentArgs", [ + { id: 1, name: "tool_call_id", type: "string" }, + { id: 2, name: "subagent_type", type: "string" }, + { id: 3, name: "model_id", type: "string" }, + { id: 4, name: "prompt", type: "string" }, + { id: 5, name: "readonly", type: "bool" }, + { id: 6, name: "resume_agent_id", type: "string" }, + { id: 7, name: "run_in_background", type: "bool" }, + ]); + addType(root, "SubagentSuccess", [ + { id: 1, name: "agent_id", type: "string" }, + { id: 2, name: "final_message", type: "string" }, + { id: 3, name: "tool_call_count", type: "int32" }, + { id: 4, name: "background_reason", type: "int32" }, + { id: 5, name: "transcript_path", type: "string" }, + ]); + addType(root, "SubagentError", [ + { id: 1, name: "agent_id", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + addType(root, "SubagentResult", [ + { id: 1, name: "success", type: "SubagentSuccess" }, + { id: 2, name: "error", type: "SubagentError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "AskQuestionOption", [ + { id: 1, name: "id", type: "string" }, + { id: 2, name: "label", type: "string" }, + ]); + addType(root, "AskQuestionItem", [ + { id: 1, name: "id", type: "string" }, + { id: 2, name: "prompt", type: "string" }, + { id: 3, name: "options", type: "AskQuestionOption", repeated: true }, + { id: 4, name: "allow_multiple", type: "bool" }, + ]); + addType(root, "AskQuestionArgs", [ + { id: 1, name: "title", type: "string" }, + { id: 2, name: "questions", type: "AskQuestionItem", repeated: true }, + // #5/#6 drive Cursor's non-blocking AskQuestion: the client answers the + // interaction with `async` right away and delivers the real answers later + // through ConversationAction.async_ask_question_completion_action, keyed by + // the originating tool call id. See protocol/ask-question.ts. + { id: 5, name: "run_async", type: "bool" }, + { id: 6, name: "async_original_tool_call_id", type: "string" }, + ]); + addType(root, "AskQuestionToolCall", [{ id: 1, name: "args", type: "AskQuestionArgs" }]); + addType(root, "FetchToolArgs", [ + { id: 1, name: "url", type: "string" }, + { id: 2, name: "tool_call_id", type: "string" }, + ]); + addType(root, "FetchToolCall", [{ id: 1, name: "args", type: "FetchToolArgs" }]); + addType(root, "WebFetchToolCall", [{ id: 1, name: "args", type: "FetchToolArgs" }]); + addType(root, "SwitchModeToolArgs", [ + { id: 1, name: "target_mode_id", type: "string" }, + { id: 2, name: "explanation", type: "string" }, + { id: 3, name: "tool_call_id", type: "string" }, + ]); + // Display ToolCall.result (CLI success hardcodes from_mode_id=""); distinct from + // InteractionResponse approved/rejected, which is the consent gate. + addType(root, "SwitchModeSuccess", [ + { id: 1, name: "from_mode_id", type: "string" }, + { id: 2, name: "to_mode_id", type: "string" }, + ]); + addType(root, "SwitchModeError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "SwitchModeRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "SwitchModeResult", [ + { id: 1, name: "success", type: "SwitchModeSuccess" }, + { id: 2, name: "error", type: "SwitchModeError" }, + { id: 3, name: "rejected", type: "SwitchModeRejected" }, + ], [{ name: "result", fields: ["success", "error", "rejected"] }]); + addType(root, "SwitchModeToolCall", [ + { id: 1, name: "args", type: "SwitchModeToolArgs" }, + { id: 2, name: "result", type: "SwitchModeResult" }, + ]); + addType(root, "PiReadToolArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "offset", type: "int32" }, + { id: 3, name: "limit", type: "int32" }, + ]); + addType(root, "PiBashToolArgs", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "timeout", type: "double" }, + ]); + addType(root, "PiEditReplacement", [ + { id: 1, name: "old_text", type: "string" }, + { id: 2, name: "new_text", type: "string" }, + ]); + addType(root, "PiEditToolArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "edits", type: "PiEditReplacement", repeated: true }, + ]); + addType(root, "PiWriteToolArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "content", type: "string" }, + ]); + addType(root, "PiGrepToolArgs", [ + { id: 1, name: "pattern", type: "string" }, + { id: 2, name: "path", type: "string" }, + { id: 3, name: "glob", type: "string" }, + { id: 4, name: "ignore_case", type: "bool" }, + { id: 5, name: "literal", type: "bool" }, + { id: 6, name: "context", type: "int32" }, + { id: 7, name: "limit", type: "int32" }, + ]); + addType(root, "PiFindToolArgs", [ + { id: 1, name: "pattern", type: "string" }, + { id: 2, name: "path", type: "string" }, + { id: 3, name: "limit", type: "int32" }, + ]); + addType(root, "PiLsToolArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "limit", type: "int32" }, + ]); + addType(root, "PiWriteToolCall", [{ id: 1, name: "args", type: "PiWriteToolArgs" }]); + addType(root, "PiReadToolCall", [{ id: 1, name: "args", type: "PiReadToolArgs" }]); + addType(root, "PiBashToolCall", [{ id: 1, name: "args", type: "PiBashToolArgs" }]); + addType(root, "PiEditToolCall", [{ id: 1, name: "args", type: "PiEditToolArgs" }]); + addType(root, "PiGrepToolCall", [{ id: 1, name: "args", type: "PiGrepToolArgs" }]); + addType(root, "PiFindToolCall", [{ id: 1, name: "args", type: "PiFindToolArgs" }]); + addType(root, "PiLsToolCall", [{ id: 1, name: "args", type: "PiLsToolArgs" }]); + // Display-only native tools Cursor may complete without an ExecServerMessage. + // ReadTodos mirrors UpdateTodos: args + result (see agent.v1 ReadTodosToolCall). + addType(root, "ReadTodosArgs", [ + { id: 1, name: "status_filter", type: "TodoStatus", repeated: true }, + { id: 2, name: "id_filter", type: "string", repeated: true }, + ]); + addType(root, "ReadTodosSuccess", [ + { id: 1, name: "todos", type: "TodoItem", repeated: true }, + { id: 2, name: "total_count", type: "int32" }, + ]); + addType(root, "ReadTodosError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "ReadTodosResult", [ + { id: 1, name: "success", type: "ReadTodosSuccess" }, + { id: 2, name: "error", type: "ReadTodosError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "ReadTodosToolCall", [ + { id: 1, name: "args", type: "ReadTodosArgs" }, + { id: 2, name: "result", type: "ReadTodosResult" }, + ]); + addType(root, "AwaitArgs", [ + { id: 1, name: "task_id", type: "string" }, + { id: 2, name: "block_until_ms", type: "uint32" }, + { id: 3, name: "regex", type: "string" }, + ]); + addType(root, "AwaitToolCall", [{ id: 1, name: "args", type: "AwaitArgs" }]); + addType(root, "GetMcpToolsArgs", [ + { id: 1, name: "server", type: "string" }, + { id: 2, name: "tool_name", type: "string" }, + { id: 3, name: "pattern", type: "string" }, + { id: 4, name: "tool_call_id", type: "string" }, + ]); + // agent.v1 GetMcpToolsToolCall is args + result. Cursor's agent fills result + // locally (GetDynamicTools / GetMcpTools); large catalogs spill through + // write_args and put the spill path on GetMcpToolsSuccess.output_file_path. + addType(root, "GetMcpToolsSuccess", [ + { id: 1, name: "content", type: "string" }, + { id: 2, name: "output_file_path", type: "string" }, + ]); + addType(root, "GetMcpToolsError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "GetMcpToolsAgentResult", [ + { id: 1, name: "success", type: "GetMcpToolsSuccess" }, + { id: 2, name: "error", type: "GetMcpToolsError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "GetMcpToolsToolCall", [ + { id: 1, name: "args", type: "GetMcpToolsArgs" }, + { id: 2, name: "result", type: "GetMcpToolsAgentResult" }, + ]); + addType(root, "GenerateImageToolArgs", [ + { id: 1, name: "description", type: "string" }, + { id: 2, name: "file_path", type: "string" }, + { id: 5, name: "reference_image_paths", type: "string", repeated: true }, + ]); + // Cursor generates the image server-side and returns it on the display + // tool_call frame. `image_data` is a base64 string, not bytes. + addType(root, "GenerateImageSuccess", [ + { id: 1, name: "file_path", type: "string" }, + { id: 2, name: "image_data", type: "string" }, + ]); + addType(root, "GenerateImageError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "GenerateImageResult", [ + { id: 1, name: "success", type: "GenerateImageSuccess" }, + { id: 2, name: "error", type: "GenerateImageError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "GenerateImageToolCall", [ + { id: 1, name: "args", type: "GenerateImageToolArgs" }, + { id: 2, name: "result", type: "GenerateImageResult" }, + ]); + // InteractionQuery keeps query bodies opaque; decoded on demand like + // AskQuestionInteractionQuery. + addType(root, "GenerateImageRequestQuery", [ + { id: 1, name: "args", type: "GenerateImageToolArgs" }, + { id: 2, name: "tool_call_id", type: "string" }, + ]); + addType(root, "ToolCall", [ + { id: 57, name: "tool_call_id", type: "string" }, + { id: 1, name: "shell_tool_call", type: "ShellToolCall" }, + { id: 3, name: "delete_tool_call", type: "DeleteToolCall" }, + { id: 4, name: "glob_tool_call", type: "GlobToolCall" }, + { id: 5, name: "grep_tool_call", type: "GrepToolCall" }, + { id: 8, name: "read_tool_call", type: "ReadToolCall" }, + { id: 9, name: "update_todos_tool_call", type: "UpdateTodosToolCall" }, + { id: 10, name: "read_todos_tool_call", type: "ReadTodosToolCall" }, + { id: 12, name: "edit_tool_call", type: "EditToolCall" }, + { id: 13, name: "ls_tool_call", type: "LsToolCall" }, + { id: 15, name: "mcp_tool_call", type: "McpToolCall" }, + { id: 17, name: "create_plan_tool_call", type: "CreatePlanToolCall" }, + { id: 18, name: "web_search_tool_call", type: "WebSearchToolCall" }, + { id: 19, name: "task_tool_call", type: "TaskToolCall" }, + { id: 23, name: "ask_question_tool_call", type: "AskQuestionToolCall" }, + { id: 24, name: "fetch_tool_call", type: "FetchToolCall" }, + { id: 25, name: "switch_mode_tool_call", type: "SwitchModeToolCall" }, + { id: 28, name: "generate_image_tool_call", type: "GenerateImageToolCall" }, + { id: 37, name: "web_fetch_tool_call", type: "WebFetchToolCall" }, + { id: 42, name: "await_tool_call", type: "AwaitToolCall" }, + { id: 44, name: "get_mcp_tools_tool_call", type: "GetMcpToolsToolCall" }, + { id: 61, name: "pi_read_tool_call", type: "PiReadToolCall" }, + { id: 62, name: "pi_bash_tool_call", type: "PiBashToolCall" }, + { id: 63, name: "pi_edit_tool_call", type: "PiEditToolCall" }, + { id: 64, name: "pi_write_tool_call", type: "PiWriteToolCall" }, + { id: 65, name: "pi_grep_tool_call", type: "PiGrepToolCall" }, + { id: 66, name: "pi_find_tool_call", type: "PiFindToolCall" }, + { id: 67, name: "pi_ls_tool_call", type: "PiLsToolCall" }, + ], [{ + name: "tool", + fields: [ + "shell_tool_call", "delete_tool_call", "glob_tool_call", "grep_tool_call", + "read_tool_call", "update_todos_tool_call", "read_todos_tool_call", + "edit_tool_call", "ls_tool_call", + "mcp_tool_call", "create_plan_tool_call", "web_search_tool_call", "task_tool_call", + "ask_question_tool_call", "fetch_tool_call", "switch_mode_tool_call", + "generate_image_tool_call", "web_fetch_tool_call", "await_tool_call", "get_mcp_tools_tool_call", + "pi_read_tool_call", "pi_bash_tool_call", "pi_edit_tool_call", + "pi_write_tool_call", "pi_grep_tool_call", "pi_find_tool_call", "pi_ls_tool_call", + ], + }]); + addType(root, "ToolCallStarted", [ + { id: 1, name: "call_id", type: "string" }, + { id: 2, name: "tool_call", type: "ToolCall" }, + { id: 3, name: "model_call_id", type: "string" }, + ]); + // agent.v1 ToolCallCompletedUpdate has no string result field — result lives + // inside the typed ToolCall variant. + addType(root, "ToolCallCompleted", [ + { id: 1, name: "call_id", type: "string" }, + { id: 2, name: "tool_call", type: "ToolCall" }, + { id: 3, name: "model_call_id", type: "string" }, + ]); + addType(root, "PartialToolCall", [ + { id: 1, name: "call_id", type: "string" }, + { id: 2, name: "tool_call", type: "ToolCall" }, + { id: 3, name: "args_text_delta", type: "string" }, + { id: 4, name: "model_call_id", type: "string" }, + ]); + // agent.v1 StepStartedUpdate / StepCompletedUpdate: step_id is uint64 (T:4), + // not string — wrong type made every step_completed frame throw + // "index out of range" in decodeMessage. + addType(root, "StepStarted", [ + { id: 1, name: "step_id", type: "uint64" }, + ]); + addType(root, "StepCompleted", [ + { id: 1, name: "step_id", type: "uint64" }, + { id: 2, name: "step_duration_ms", type: "int64" }, + ]); + // InteractionUpdate — the core streaming update message + addType(root, "InteractionUpdate", [ + { id: 1, name: "text_delta", type: "TextDeltaUpdate" }, + { id: 2, name: "tool_call_started", type: "ToolCallStarted" }, + { id: 3, name: "tool_call_completed", type: "ToolCallCompleted" }, + { id: 4, name: "thinking_delta", type: "ThinkingDeltaUpdate" }, + { id: 7, name: "partial_tool_call", type: "PartialToolCall" }, + { id: 13, name: "heartbeat", type: "Heartbeat" }, + { id: 14, name: "turn_ended", type: "TurnEnded" }, + { id: 16, name: "step_started", type: "StepStarted" }, + { id: 17, name: "step_completed", type: "StepCompleted" }, + ], [{ name: "update", fields: ["text_delta", "tool_call_started", "tool_call_completed", "thinking_delta", "partial_tool_call", "heartbeat", "turn_ended", "step_started", "step_completed"] }]); + // ── Exec channel ── + // Field numbers match agent.v1. Extra fields we don't use are still declared + // so protobufjs doesn't drop them on decode. + addType(root, "ReadArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "tool_call_id", type: "string" }, + { id: 4, name: "offset", type: "int32" }, + { id: 5, name: "limit", type: "uint32" }, + ]); + // agent.v1 ReadResult is a oneof — flat {content,error} was rejected by the + // server (endless heartbeats after read_result). + addType(root, "ReadSuccess", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "content", type: "string" }, + { id: 3, name: "total_lines", type: "int32" }, + { id: 4, name: "file_size", type: "int64" }, + { id: 5, name: "data", type: "bytes" }, + { id: 6, name: "truncated", type: "bool" }, + { id: 7, name: "output_blob_id", type: "bytes" }, + { id: 8, name: "range_applied", type: "bool" }, + ], [{ name: "output", fields: ["content", "data"] }]); + addType(root, "ReadError", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + // Cursor's LocalReadExecutor validates the target before execution and, for a + // missing / non-regular path, returns one of these typed ReadResult cases + // instead of running the tool. Mirroring the exact field ids/shapes lets this + // provider reply the same way so the model sees a structured file-not-found + // observation and the host never surfaces a permission prompt. + addType(root, "ReadFileNotFound", [{ id: 1, name: "path", type: "string" }]); + addType(root, "ReadPermissionDenied", [{ id: 1, name: "path", type: "string" }]); + addType(root, "ReadInvalidFile", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "reason", type: "string" }, + ]); + addType(root, "ReadResult", [ + { id: 1, name: "success", type: "ReadSuccess" }, + { id: 2, name: "error", type: "ReadError" }, + { id: 4, name: "file_not_found", type: "ReadFileNotFound" }, + { id: 5, name: "permission_denied", type: "ReadPermissionDenied" }, + { id: 6, name: "invalid_file", type: "ReadInvalidFile" }, + ], [ + { + name: "result", + fields: ["success", "error", "file_not_found", "permission_denied", "invalid_file"], + }, + ]); + addType(root, "GrepArgs", [ + { id: 1, name: "pattern", type: "string" }, + { id: 2, name: "path", type: "string" }, + { id: 3, name: "glob", type: "string" }, + { id: 4, name: "output_mode", type: "string" }, + { id: 8, name: "case_insensitive", type: "bool" }, + { id: 10, name: "head_limit", type: "int32" }, + { id: 14, name: "tool_call_id", type: "string" }, + { id: 16, name: "offset", type: "int32" }, + ]); + addType(root, "GrepError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "GrepFilesResult", [ + { id: 1, name: "files", type: "string", repeated: true }, + { id: 2, name: "total_files", type: "int32" }, + { id: 3, name: "client_truncated", type: "bool" }, + ]); + addType(root, "GrepContentMatch", [ + { id: 1, name: "line_number", type: "int32" }, + { id: 2, name: "content", type: "string" }, + ]); + addType(root, "GrepFileMatch", [ + { id: 1, name: "file", type: "string" }, + { id: 2, name: "matches", type: "GrepContentMatch", repeated: true }, + ]); + addType(root, "GrepContentResult", [ + { id: 1, name: "matches", type: "GrepFileMatch", repeated: true }, + { id: 2, name: "total_lines", type: "int32" }, + { id: 3, name: "total_matched_lines", type: "int32" }, + { id: 4, name: "client_truncated", type: "bool" }, + { id: 5, name: "ripgrep_truncated", type: "bool" }, + ]); + addType(root, "GrepUnionResult", [ + { id: 2, name: "files", type: "GrepFilesResult" }, + { id: 3, name: "content", type: "GrepContentResult" }, + ], [{ name: "result", fields: ["files", "content"] }]); + { + const t = new protobuf.Type("GrepSuccess"); + t.add(new protobuf.Field("pattern", 1, "string")); + t.add(new protobuf.Field("path", 2, "string")); + t.add(new protobuf.Field("output_mode", 3, "string")); + t.add(new protobuf.MapField("workspace_results", 4, "string", "GrepUnionResult")); + root.add(t); + } + addType(root, "GrepResult", [ + { id: 1, name: "success", type: "GrepSuccess" }, + { id: 2, name: "error", type: "GrepError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + // agent.v1.WriteArgs. Cursor's own LocalWriteExecutor reads `file_bytes` + // first and only falls back to `file_text`, so a write whose content arrives + // as bytes decodes to an empty string unless field 5 is declared here. + addType(root, "WriteArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "file_text", type: "string" }, + { id: 3, name: "tool_call_id", type: "string" }, + { id: 4, name: "return_file_content_after_write", type: "bool" }, + { id: 5, name: "file_bytes", type: "bytes" }, + { id: 6, name: "encoding_hint", type: "string" }, + ]); + addType(root, "WriteSuccess", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "lines_created", type: "int32" }, + { id: 3, name: "file_size", type: "int32" }, + { id: 4, name: "file_content_after_write", type: "string" }, + ]); + addType(root, "WriteError", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + // Cursor's own executor reports a refused write with this variant rather than + // a generic error, and its agent reads `operation` / `is_readonly` back. + addType(root, "WritePermissionDenied", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "directory", type: "string" }, + { id: 3, name: "operation", type: "string" }, + { id: 4, name: "error", type: "string" }, + { id: 5, name: "is_readonly", type: "bool" }, + ]); + addType(root, "WriteResult", [ + { id: 1, name: "success", type: "WriteSuccess" }, + { id: 3, name: "permission_denied", type: "WritePermissionDenied" }, + { id: 5, name: "error", type: "WriteError" }, + ], [{ name: "result", fields: ["success", "permission_denied", "error"] }]); + // agent.v1 Pi exec results. Cursor places Pi requests at ExecServerMessage + // fields #45..#51 and their corresponding ExecClientMessage results at + // #46..#52 (the result fields are deliberately offset by one). + // + // The success payloads contain additional optional truncation/diff metadata; + // OpenCode returns text, so the minimal output/error shapes are sufficient. + addType(root, "PiWriteArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "content", type: "string" }, + ]); + addType(root, "PiWriteSuccess", [ + { id: 1, name: "output", type: "string" }, + ]); + addType(root, "PiWriteError", [ + { id: 1, name: "error", type: "string" }, + ]); + addType(root, "PiWriteRejected", [ + { id: 1, name: "reason", type: "string" }, + ]); + addType(root, "PiWriteResult", [ + { id: 1, name: "success", type: "PiWriteSuccess" }, + { id: 2, name: "error", type: "PiWriteError" }, + { id: 3, name: "rejected", type: "PiWriteRejected" }, + ], [{ name: "result", fields: ["success", "error", "rejected"] }]); + // agent.v1.PiTruncation. Every Pi *ExecSuccess shares output(1)+truncation(2) + // and only diverges at field 3, so the shared PiOutputSuccess can carry it. + // Cursor's own CLI always reports truncation rather than silently shortening + // a payload; emitting this keeps a capped read distinguishable from a + // complete one. + addType(root, "PiTruncation", [ + { id: 1, name: "truncated", type: "bool" }, + { id: 2, name: "truncated_by", type: "string" }, + { id: 3, name: "total_lines", type: "uint32" }, + { id: 4, name: "output_lines", type: "uint32" }, + { id: 5, name: "output_bytes", type: "uint32" }, + { id: 6, name: "max_lines", type: "uint32" }, + { id: 7, name: "max_bytes", type: "uint32" }, + { id: 8, name: "first_line_exceeds_limit", type: "bool" }, + { id: 9, name: "last_line_partial", type: "bool" }, + ]); + addType(root, "PiOutputSuccess", [ + { id: 1, name: "output", type: "string" }, + { id: 2, name: "truncation", type: "PiTruncation" }, + ]); + addType(root, "PiExecError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "PiExecRejected", [{ id: 1, name: "reason", type: "string" }]); + for (const resultType of [ + "PiReadExecResult", + "PiBashExecResult", + "PiGrepExecResult", + "PiFindExecResult", + "PiLsExecResult", + ]) { + addType(root, resultType, [ + { id: 1, name: "success", type: "PiOutputSuccess" }, + { id: 2, name: "error", type: "PiExecError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + } + addType(root, "PiEditExecResult", [ + { id: 1, name: "success", type: "PiOutputSuccess" }, + { id: 2, name: "error", type: "PiExecError" }, + { id: 3, name: "rejected", type: "PiExecRejected" }, + ], [{ name: "result", fields: ["success", "error", "rejected"] }]); + addType(root, "DeleteArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "tool_call_id", type: "string" }, + ]); + addType(root, "DeleteSuccess", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "deleted_file", type: "string" }, + ]); + addType(root, "DeleteError", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + addType(root, "DeleteResult", [ + { id: 1, name: "success", type: "DeleteSuccess" }, + { id: 7, name: "error", type: "DeleteError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "LsArgs", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "ignore", type: "string", repeated: true }, + { id: 3, name: "tool_call_id", type: "string" }, + ]); + addType(root, "LsDirectoryTreeFile", [{ id: 1, name: "name", type: "string" }]); + addType(root, "LsDirectoryTreeNode", [ + { id: 1, name: "abs_path", type: "string" }, + { id: 2, name: "children_dirs", type: "LsDirectoryTreeNode", repeated: true }, + { id: 3, name: "children_files", type: "LsDirectoryTreeFile", repeated: true }, + { id: 6, name: "num_files", type: "int32" }, + ]); + addType(root, "LsSuccess", [ + { id: 1, name: "directory_tree_root", type: "LsDirectoryTreeNode" }, + ]); + addType(root, "LsError", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + addType(root, "LsResult", [ + { id: 1, name: "success", type: "LsSuccess" }, + { id: 2, name: "error", type: "LsError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "ShellArgs", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "working_directory", type: "string" }, + { id: 3, name: "timeout", type: "uint32" }, + { id: 4, name: "tool_call_id", type: "string" }, + // Canonical agent.v1 fields used by the native CLI to distinguish a + // foreground cancellation deadline from a soft background handoff. + { id: 13, name: "timeout_behavior", type: "uint32" }, + { id: 14, name: "hard_timeout", type: "uint32" }, + ]); + addType(root, "ShellStreamStart", []); // optional SandboxPolicy only; empty is valid + addType(root, "ShellStreamStdout", [{ id: 1, name: "data", type: "string" }]); + addType(root, "ShellStreamStderr", [{ id: 1, name: "data", type: "string" }]); + // agent.v1: code is uint32; protobufjs must emit code=0 (defaults are load-bearing). + addType(root, "ShellStreamExit", [ + { id: 1, name: "code", type: "uint32" }, + { id: 2, name: "cwd", type: "string" }, + { id: 4, name: "aborted", type: "bool" }, + { id: 5, name: "abort_reason", type: "uint32" }, + { id: 6, name: "local_execution_time_ms", type: "uint32" }, + ]); + addType(root, "ShellStreamBackgrounded", [ + { id: 1, name: "shell_id", type: "uint32" }, + { id: 2, name: "command", type: "string" }, + { id: 3, name: "working_directory", type: "string" }, + { id: 4, name: "pid", type: "uint32" }, + { id: 5, name: "ms_to_wait", type: "uint32" }, + { id: 6, name: "reason", type: "uint32" }, + ]); + // ShellRejected / ShellPermissionDenied (shared with ShellResult) — reason/error at #3. + addType(root, "ShellRejected", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "working_directory", type: "string" }, + { id: 3, name: "reason", type: "string" }, + ]); + addType(root, "ShellPermissionDenied", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "working_directory", type: "string" }, + { id: 3, name: "error", type: "string" }, + ]); + // ── Soft-deny stubs for known unsupported Cursor-native exec variants ── + // These are encode-only: enough to emit a populated deny, not full success. + // Canonical field numbers from agent.proto ExecClientMessage. + // ShellResult is also the live reply for exec #2 shell_args (OpenCode bash). + addType(root, "ShellSuccess", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "working_directory", type: "string" }, + { id: 3, name: "exit_code", type: "int32" }, + { id: 4, name: "signal", type: "string" }, + { id: 5, name: "stdout", type: "string" }, + { id: 6, name: "stderr", type: "string" }, + { id: 7, name: "execution_time", type: "int32" }, + { id: 9, name: "shell_id", type: "uint32" }, + { id: 11, name: "pid", type: "uint32" }, + { id: 12, name: "ms_to_wait", type: "int32" }, + { id: 13, name: "local_execution_time_ms", type: "int32" }, + { id: 14, name: "background_reason", type: "uint32" }, + ]); + addType(root, "ShellFailure", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "working_directory", type: "string" }, + { id: 3, name: "exit_code", type: "int32" }, + { id: 4, name: "signal", type: "string" }, + { id: 5, name: "stdout", type: "string" }, + { id: 6, name: "stderr", type: "string" }, + { id: 7, name: "execution_time", type: "int32" }, + { id: 10, name: "abort_reason", type: "uint32" }, + { id: 11, name: "aborted", type: "bool" }, + ]); + addType(root, "ShellTimeout", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "working_directory", type: "string" }, + { id: 3, name: "timeout_ms", type: "int32" }, + ]); + addType(root, "ShellResult", [ + { id: 1, name: "success", type: "ShellSuccess" }, + { id: 2, name: "failure", type: "ShellFailure" }, + { id: 3, name: "timeout", type: "ShellTimeout" }, + { id: 4, name: "rejected", type: "ShellRejected" }, + { id: 102, name: "is_background", type: "bool" }, + { id: 104, name: "pid", type: "uint32" }, + ], [{ name: "result", fields: ["success", "failure", "timeout", "rejected"] }]); + addType(root, "DiagnosticsError", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + addType(root, "DiagnosticsResult", [ + { id: 2, name: "error", type: "DiagnosticsError" }, + ], [{ name: "result", fields: ["error"] }]); + addType(root, "FetchError", [ + { id: 1, name: "url", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + addType(root, "FetchResult", [ + { id: 2, name: "error", type: "FetchError" }, + ], [{ name: "result", fields: ["error"] }]); + addType(root, "RecordScreenFailure", [ + { id: 1, name: "error", type: "string" }, + ]); + addType(root, "RecordScreenResult", [ + { id: 4, name: "failure", type: "RecordScreenFailure" }, + ], [{ name: "result", fields: ["failure"] }]); + addType(root, "ComputerUseError", [ + { id: 1, name: "error", type: "string" }, + ]); + addType(root, "ComputerUseResult", [ + { id: 2, name: "error", type: "ComputerUseError" }, + ], [{ name: "result", fields: ["error"] }]); + addType(root, "WriteShellStdinError", [ + { id: 1, name: "error", type: "string" }, + ]); + addType(root, "WriteShellStdinResult", [ + { id: 2, name: "error", type: "WriteShellStdinError" }, + ], [{ name: "result", fields: ["error"] }]); + addType(root, "SubagentAwaitError", [ + { id: 1, name: "agent_id", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + addType(root, "SubagentAwaitResult", [ + { id: 4, name: "error", type: "SubagentAwaitError" }, + ], [{ name: "result", fields: ["error"] }]); + addType(root, "SmartModeClassifierError", [ + { id: 1, name: "error", type: "string" }, + ]); + addType(root, "SmartModeClassifierResult", [ + { id: 2, name: "error", type: "SmartModeClassifierError" }, + ], [{ name: "result", fields: ["error"] }]); + addType(root, "CanvasDiagnosticsError", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + addType(root, "CanvasDiagnosticsResult", [ + { id: 2, name: "error", type: "CanvasDiagnosticsError" }, + ], [{ name: "result", fields: ["error"] }]); + addType(root, "ShellAllowlistPrecheckResult", [ + { id: 1, name: "allowlisted", type: "bool" }, + ]); + addType(root, "McpAllowlistPrecheckResult", [ + { id: 1, name: "allowlisted", type: "bool" }, + ]); + addType(root, "WebFetchAllowlistPrecheckResult", [ + { id: 1, name: "allowlisted", type: "bool" }, + ]); + addType(root, "ForceBackgroundShellResult", [ + { id: 1, name: "status", type: "uint32" }, + ]); + addType(root, "ForceBackgroundSubagentResult", [ + { id: 1, name: "status", type: "uint32" }, + ]); + addType(root, "ConversationSearchHit", [ + { id: 1, name: "conversation_id", type: "string" }, + { id: 2, name: "title", type: "string" }, + { id: 3, name: "source", type: "uint32" }, + { id: 4, name: "updated_at_ms", type: "int64" }, + { id: 5, name: "snippet", type: "string" }, + ]); + addType(root, "ConversationSearchSuccess", [ + { id: 1, name: "hits", type: "ConversationSearchHit", repeated: true }, + { id: 2, name: "truncated", type: "bool" }, + { id: 3, name: "partial", type: "bool" }, + { id: 4, name: "rebuilding", type: "bool" }, + ]); + addType(root, "ConversationSearchError", [ + { id: 1, name: "error", type: "string" }, + ]); + addType(root, "ConversationSearchResult", [ + { id: 1, name: "success", type: "ConversationSearchSuccess" }, + { id: 2, name: "error", type: "ConversationSearchError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "AgentStoreConflictError", [ + { id: 1, name: "error", type: "string" }, + ]); + addType(root, "AgentStoreConflictResult", [ + { id: 2, name: "error", type: "AgentStoreConflictError" }, + ], [{ name: "result", fields: ["error"] }]); + addType(root, "AdoptResult", [ + { id: 1, name: "source_agent_id", type: "string" }, + { id: 2, name: "target_agent_id", type: "string" }, + { id: 3, name: "project_root_id", type: "string" }, + { id: 5, name: "error", type: "string" }, + ], [{ name: "result", fields: ["error"] }]); + // aiserver.v1.GetDiffRequest / GetDiffResponse (exec #44). Enums as uint32 + // so protobufjs json decode does not stringify output_format / diff_type. + addType(root, "GitDiffChunk", [ + { id: 1, name: "content", type: "string" }, + { id: 2, name: "lines", type: "string", repeated: true }, + { id: 3, name: "old_start", type: "int32" }, + { id: 4, name: "old_lines", type: "int32" }, + { id: 5, name: "new_start", type: "int32" }, + { id: 6, name: "new_lines", type: "int32" }, + ]); + addType(root, "GitFileDiff", [ + { id: 1, name: "from", type: "string" }, + { id: 2, name: "to", type: "string" }, + { id: 3, name: "chunks", type: "GitDiffChunk", repeated: true }, + { id: 4, name: "added", type: "int32" }, + { id: 5, name: "removed", type: "int32" }, + { id: 6, name: "before_file_contents", type: "string" }, + { id: 7, name: "after_file_contents", type: "string" }, + { id: 8, name: "is_generated", type: "bool" }, + ]); + addType(root, "GitDiff", [ + { id: 1, name: "diffs", type: "GitFileDiff", repeated: true }, + { id: 2, name: "diff_type", type: "uint32" }, + ]); + addType(root, "GitDiffSubmoduleDiff", [ + { id: 1, name: "relative_path", type: "string" }, + { id: 2, name: "diff", type: "GitDiff" }, + { id: 3, name: "errored", type: "bool" }, + ]); + addType(root, "GetDiffRequest", [ + { id: 1, name: "cwd", type: "string" }, + { id: 2, name: "ref", type: "string" }, + { id: 3, name: "base_ref", type: "string" }, + { id: 4, name: "merge_base", type: "bool" }, + { id: 5, name: "target_paths", type: "string", repeated: true }, + { id: 6, name: "unified_context_lines", type: "int32" }, + { id: 7, name: "max_untracked_files", type: "int32" }, + { id: 8, name: "output_format", type: "uint32" }, + { id: 9, name: "submodule_recurse_depth", type: "int32" }, + { id: 10, name: "include_space_changes", type: "bool" }, + { id: 11, name: "committed_only", type: "bool" }, + { id: 12, name: "compute_patch_id", type: "bool" }, + { id: 13, name: "return_head_sha", type: "bool" }, + { id: 14, name: "max_response_bytes", type: "int32" }, + ]); + addType(root, "GetDiffResponse", [ + { id: 1, name: "diff", type: "GitDiff" }, + { id: 2, name: "submodule_diffs", type: "GitDiffSubmoduleDiff", repeated: true }, + { id: 3, name: "patch_id", type: "string" }, + { id: 4, name: "head_sha", type: "string" }, + { id: 5, name: "has_uncommitted_changes", type: "bool" }, + ]); + // Cursor's non-blocking shell path is a separate exec variant from the + // foreground ShellStream above. The host only needs the fields used at the + // OpenCode boundary; protobufjs safely skips the richer classifier/approval + // messages that are private to Cursor's native client. + addType(root, "BackgroundShellSpawnArgs", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "working_directory", type: "string" }, + { id: 3, name: "tool_call_id", type: "string" }, + { id: 6, name: "enable_write_shell_stdin_tool", type: "bool" }, + { id: 7, name: "description", type: "string" }, + { id: 12, name: "skip_approval", type: "bool" }, + { id: 13, name: "conversation_id", type: "string" }, + ]); + addType(root, "BackgroundShellSpawnSuccess", [ + { id: 1, name: "shell_id", type: "uint32" }, + { id: 2, name: "command", type: "string" }, + { id: 3, name: "working_directory", type: "string" }, + { id: 4, name: "pid", type: "uint32" }, + ]); + addType(root, "BackgroundShellSpawnError", [ + { id: 1, name: "command", type: "string" }, + { id: 2, name: "working_directory", type: "string" }, + { id: 3, name: "error", type: "string" }, + ]); + addType(root, "BackgroundShellSpawnResult", [ + { id: 1, name: "success", type: "BackgroundShellSpawnSuccess" }, + { id: 2, name: "error", type: "BackgroundShellSpawnError" }, + { id: 3, name: "rejected", type: "ShellRejected" }, + { id: 4, name: "permission_denied", type: "ShellPermissionDenied" }, + ], [{ name: "result", fields: ["success", "error", "rejected", "permission_denied"] }]); + addType(root, "ShellStream", [ + { id: 1, name: "stdout", type: "ShellStreamStdout" }, + { id: 2, name: "stderr", type: "ShellStreamStderr" }, + { id: 3, name: "exit", type: "ShellStreamExit" }, + { id: 4, name: "start", type: "ShellStreamStart" }, + { id: 5, name: "rejected", type: "ShellRejected" }, + { id: 6, name: "permission_denied", type: "ShellPermissionDenied" }, + { id: 7, name: "backgrounded", type: "ShellStreamBackgrounded" }, + ], [{ name: "event", fields: ["stdout", "stderr", "exit", "start", "rejected", "permission_denied", "backgrounded"] }]); + addType(root, "GlobArgs", [ + { id: 1, name: "target_directory", type: "string" }, + { id: 2, name: "glob_pattern", type: "string" }, + ]); + addType(root, "GlobResult", [ + { id: 1, name: "files", type: "string", repeated: true }, + { id: 2, name: "error", type: "string" }, + ]); + // `args` is a map on the wire (repeated map + // entries at field #2). We capture each entry as raw bytes and decode them in + // tools.ts via struct.decodeStructEntriesToJson. + addType(root, "McpArgs", [ + { id: 1, name: "name", type: "string" }, + { id: 2, name: "args", type: "bytes", repeated: true }, + { id: 3, name: "tool_call_id", type: "string" }, + { id: 4, name: "provider_identifier", type: "string" }, + { id: 5, name: "tool_name", type: "string" }, + ]); + addType(root, "McpTextContent", [{ id: 1, name: "text", type: "string" }]); + addType(root, "McpToolResultContentItem", [{ id: 1, name: "text", type: "McpTextContent" }], [{ name: "content", fields: ["text"] }]); + addType(root, "McpSuccess", [ + { id: 1, name: "content", type: "McpToolResultContentItem", repeated: true }, + { id: 2, name: "is_error", type: "bool" }, + ]); + addType(root, "McpError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "McpResult", [ + { id: 1, name: "success", type: "McpSuccess" }, + { id: 2, name: "error", type: "McpError" }, + ], [{ name: "result", fields: ["success", "error"] }]); + // McpToolDefinition — agent.v1 shape for RequestContext.tools (#7) and + // AgentRunRequest.mcp_tools (#4). Defined early so RequestContext* can + // reference it. + addType(root, "McpToolDefinition", [ + { id: 1, name: "name", type: "string" }, + { id: 2, name: "description", type: "string" }, + { id: 3, name: "input_schema", type: "bytes" }, + { id: 4, name: "provider_identifier", type: "string" }, + { id: 5, name: "tool_name", type: "string" }, + ]); + // ── MCP resource exec (fields 17/18) ── + // agent.v1 ListMcpResourcesExecArgs/Result, ReadMcpResourceExecArgs/Result. + // Native Cursor exec variants: no descriptor, no advertisement, and Cursor + // may emit either unsolicited regardless of what this provider advertises + // (tasks/plans/fix-cursor-mcp-resource-exec.md, Correction 2). Under Option + // B, list_mcp_resources/read_mcp_resource execute through the ordinary + // field-11 MCP path via the alias rules below, so any 17/18 that still + // reaches this provider gets the total fallback built in tools.ts. + addType(root, "SmartModeApproval", [ + { id: 1, name: "request_id", type: "string" }, + { id: 2, name: "reason", type: "string" }, + ]); + addType(root, "OutputLocation", [ + { id: 1, name: "file_path", type: "string" }, + { id: 2, name: "size_bytes", type: "int64" }, + { id: 3, name: "line_count", type: "int64" }, + ]); + addType(root, "ListMcpResourcesExecArgs", [ + { id: 1, name: "server", type: "string" }, + ]); + { + const t = new protobuf.Type("ListMcpResourcesExecResult_McpResource"); + t.add(new protobuf.Field("uri", 1, "string")); + t.add(new protobuf.Field("name", 2, "string")); + t.add(new protobuf.Field("description", 3, "string")); + t.add(new protobuf.Field("mime_type", 4, "string")); + t.add(new protobuf.Field("server", 5, "string")); + t.add(new protobuf.MapField("annotations", 6, "string", "string")); + root.add(t); + } + addType(root, "ListMcpResourcesSuccess", [ + { id: 1, name: "resources", type: "ListMcpResourcesExecResult_McpResource", repeated: true }, + ]); + addType(root, "ListMcpResourcesError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "ListMcpResourcesRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "ListMcpResourcesExecResult", [ + { id: 1, name: "success", type: "ListMcpResourcesSuccess" }, + { id: 2, name: "error", type: "ListMcpResourcesError" }, + { id: 3, name: "rejected", type: "ListMcpResourcesRejected" }, + ], [{ name: "result", fields: ["success", "error", "rejected"] }]); + addType(root, "ReadMcpResourceExecArgs", [ + { id: 1, name: "server", type: "string" }, + { id: 2, name: "uri", type: "string" }, + { id: 3, name: "download_path", type: "string" }, + { id: 4, name: "tool_call_id", type: "string" }, + { id: 5, name: "smart_mode_approval", type: "SmartModeApproval" }, + ]); + { + // Interleaved field order matches agent.proto exactly: the text/blob oneof + // (5/6) sits between the shared metadata (1-4) and the trailing + // annotations/download/output fields (7-9). + const t = new protobuf.Type("ReadMcpResourceSuccess"); + t.add(new protobuf.Field("uri", 1, "string")); + t.add(new protobuf.Field("name", 2, "string")); + t.add(new protobuf.Field("description", 3, "string")); + t.add(new protobuf.Field("mime_type", 4, "string")); + t.add(new protobuf.Field("text", 5, "string")); + t.add(new protobuf.Field("blob", 6, "bytes")); + t.add(new protobuf.MapField("annotations", 7, "string", "string")); + t.add(new protobuf.Field("download_path", 8, "string")); + t.add(new protobuf.Field("output_location", 9, "OutputLocation")); + t.add(new protobuf.OneOf("content", ["text", "blob"])); + root.add(t); + } + addType(root, "ReadMcpResourceError", [ + { id: 1, name: "uri", type: "string" }, + { id: 2, name: "error", type: "string" }, + ]); + // Cursor quirk: on `rejected` only `reason` is set — `uri` (field 1) is + // deliberately left empty. Mirror this; do not backfill uri on encode. + addType(root, "ReadMcpResourceRejected", [ + { id: 1, name: "uri", type: "string" }, + { id: 2, name: "reason", type: "string" }, + ]); + addType(root, "ReadMcpResourceNotFound", [{ id: 1, name: "uri", type: "string" }]); + addType(root, "ReadMcpResourceExecResult", [ + { id: 1, name: "success", type: "ReadMcpResourceSuccess" }, + { id: 2, name: "error", type: "ReadMcpResourceError" }, + { id: 3, name: "rejected", type: "ReadMcpResourceRejected" }, + { id: 4, name: "not_found", type: "ReadMcpResourceNotFound" }, + ], [{ name: "result", fields: ["success", "error", "rejected", "not_found"] }]); + // ── request_context (#10): server-initiated setup probe ── + // At the start of an agent turn the server sends ExecServerMessage + // {request_context_args} to ask the client for workspace/env/tool context. + // The reply is ExecClientMessage {request_context_result{success{request_context}}}. + // If we don't reply, the server blocks on heartbeats forever (the "times out, + // no response" symptom). We populate a minimal-but-real RequestContext — env + // (workspace/shell/os) plus the tool list echoed into #7 tools (same + // McpToolDefinition shape as AgentRunRequest #4 mcp_tools) so the model keeps + // the tools opencode advertised. + addType(root, "RequestContextArgs", [ + { id: 2, name: "notes_session_id", type: "string" }, + { id: 3, name: "workspace_id", type: "string" }, + { id: 7, name: "use_cached", type: "bool" }, + ]); + addType(root, "RequestContextEnv", [ + { id: 1, name: "os_version", type: "string" }, + { id: 2, name: "workspace_paths", type: "string", repeated: true }, + { id: 3, name: "shell", type: "string" }, + { id: 5, name: "sandbox_enabled", type: "bool" }, + { id: 7, name: "terminals_folder", type: "string" }, + { id: 10, name: "time_zone", type: "string" }, + { id: 11, name: "project_folder", type: "string" }, + { id: 12, name: "agent_transcripts_folder", type: "string" }, + { id: 14, name: "sandbox_supported", type: "bool" }, + { id: 20, name: "is_working_dir_home_dir", type: "bool" }, + { id: 21, name: "process_working_directory", type: "string" }, + ]); + addType(root, "CursorRule", [ + { id: 1, name: "full_path", type: "string" }, + { id: 2, name: "content", type: "string" }, + ]); + addType(root, "RepositoryIndexingInfo", [ + { id: 1, name: "relative_workspace_path", type: "string" }, + { id: 2, name: "remote_urls", type: "string", repeated: true }, + { id: 3, name: "remote_names", type: "string", repeated: true }, + { id: 4, name: "repo_name", type: "string" }, + { id: 5, name: "repo_owner", type: "string" }, + { id: 6, name: "is_tracked", type: "bool" }, + { id: 7, name: "is_local", type: "bool" }, + { id: 9, name: "workspace_uri", type: "string" }, + ]); + addType(root, "GitRepoInfo", [ + { id: 1, name: "path", type: "string" }, + { id: 2, name: "status", type: "string" }, + { id: 3, name: "branch_name", type: "string" }, + { id: 4, name: "remote_url", type: "string" }, + ]); + addType(root, "AgentSkill", [ + { id: 1, name: "full_path", type: "string" }, + { id: 2, name: "content", type: "string" }, + { id: 3, name: "description", type: "string" }, + ]); + addType(root, "CustomSubagent", [ + { id: 1, name: "full_path", type: "string" }, + { id: 2, name: "name", type: "string" }, + { id: 3, name: "description", type: "string" }, + { id: 6, name: "prompt", type: "string" }, + ]); + // Nested MCP filesystem / meta-tool shapes (agent.v1). Must be defined + // before RequestContextPayload / RequestContext reference them. + addType(root, "McpFsToolDescriptor", [ + { id: 1, name: "tool_name", type: "string" }, + { id: 3, name: "description", type: "string" }, + { id: 4, name: "input_schema", type: "bytes" }, + ]); + addType(root, "McpDescriptor", [ + { id: 1, name: "server_name", type: "string" }, + { id: 2, name: "server_identifier", type: "string" }, + { id: 5, name: "tools", type: "McpFsToolDescriptor", repeated: true }, + ]); + addType(root, "McpFileSystemOptions", [ + { id: 1, name: "enabled", type: "bool" }, + { id: 2, name: "workspace_project_dir", type: "string" }, + { id: 3, name: "mcp_descriptors", type: "McpDescriptor", repeated: true }, + ]); + addType(root, "McpMetaToolOptions", [ + { id: 1, name: "enabled", type: "bool" }, + { id: 2, name: "mcp_descriptors", type: "McpDescriptor", repeated: true }, + ]); + addType(root, "PermissionsAutoRunInstructions", [ + { id: 1, name: "allow_instructions", type: "string", repeated: true }, + { id: 2, name: "block_instructions", type: "string", repeated: true }, + ]); + addType(root, "RequestContextPayload", [ + { id: 2, name: "rules", type: "CursorRule", repeated: true }, + { id: 4, name: "env", type: "RequestContextEnv" }, + { id: 6, name: "repository_info", type: "RepositoryIndexingInfo", repeated: true }, + { id: 7, name: "tools", type: "McpToolDefinition", repeated: true }, + { id: 11, name: "git_repos", type: "GitRepoInfo", repeated: true }, + { id: 13, name: "project_layouts", type: "LsDirectoryTreeNode", repeated: true }, + { id: 17, name: "web_search_enabled", type: "bool" }, + { id: 22, name: "custom_subagents", type: "CustomSubagent", repeated: true }, + { id: 23, name: "mcp_file_system_options", type: "McpFileSystemOptions" }, + { id: 24, name: "web_fetch_enabled", type: "bool" }, + { id: 25, name: "hooks_additional_context", type: "string" }, + { id: 29, name: "agent_skills", type: "AgentSkill", repeated: true }, + { id: 33, name: "git_repo_info_complete", type: "bool" }, + { id: 34, name: "mcp_meta_tool_options", type: "McpMetaToolOptions" }, + { id: 36, name: "mcp_info_complete", type: "bool" }, + { id: 39, name: "rules_info_complete", type: "bool" }, + { id: 40, name: "env_info_complete", type: "bool" }, + { id: 41, name: "repository_info_complete", type: "bool" }, + { id: 42, name: "custom_subagents_info_complete", type: "bool" }, + { id: 43, name: "agent_skills_info_complete", type: "bool" }, + { id: 44, name: "mcp_file_system_info_complete", type: "bool" }, + { id: 45, name: "git_status_info_complete", type: "bool" }, + { id: 46, name: "user_permissions_auto_run", type: "PermissionsAutoRunInstructions" }, + { id: 47, name: "project_permissions_auto_run", type: "PermissionsAutoRunInstructions" }, + ]); + addType(root, "RequestContextSuccess", [ + { id: 1, name: "request_context", type: "RequestContextPayload" }, + ]); + addType(root, "RequestContextResult", [ + { id: 1, name: "success", type: "RequestContextSuccess" }, + ]); + // Cursor probes MCP server availability before emitting an MCP-backed tool + // call. OpenCode owns those servers, so answer from the descriptors already + // advertised in RequestContext rather than surfacing this as a user tool. + addType(root, "McpStateExecArgs", [ + { id: 1, name: "server_identifiers", type: "string", repeated: true }, + { id: 2, name: "kick_only", type: "bool" }, + ]); + addType(root, "McpInstructions", [ + { id: 1, name: "server_name", type: "string" }, + { id: 2, name: "instructions", type: "string" }, + { id: 3, name: "server_identifier", type: "string" }, + ]); + addType(root, "McpStateServer", [ + { id: 1, name: "server_name", type: "string" }, + { id: 2, name: "server_identifier", type: "string" }, + { id: 3, name: "plugin", type: "string" }, + { id: 4, name: "marketplace", type: "string" }, + // Unlike McpDescriptor (#23/#34), exec #36 returns the full canonical + // McpToolDefinition shape, including composite name and provider identity. + { id: 5, name: "tools", type: "McpToolDefinition", repeated: true }, + { id: 6, name: "instructions", type: "McpInstructions", repeated: true }, + { id: 7, name: "status", type: "string" }, + ]); + addType(root, "McpStateSuccess", [ + { id: 1, name: "servers", type: "McpStateServer", repeated: true }, + ]); + addType(root, "McpStateError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "McpStateRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "McpStateExecResult", [ + { id: 1, name: "success", type: "McpStateSuccess" }, + { id: 2, name: "error", type: "McpStateError" }, + { id: 3, name: "rejected", type: "McpStateRejected" }, + ], [{ name: "result", fields: ["success", "error", "rejected"] }]); + // ExecServerMessage — server asks us to execute a tool + addType(root, "ExecServerMessage", [ + { id: 1, name: "id", type: "uint32" }, + { id: 15, name: "exec_id", type: "string" }, + { id: 19, name: "span", type: "string" }, + { id: 2, name: "shell_args", type: "ShellArgs" }, + { id: 3, name: "write_args", type: "WriteArgs" }, + { id: 4, name: "delete_args", type: "DeleteArgs" }, + { id: 5, name: "grep_args", type: "GrepArgs" }, + { id: 7, name: "read_args", type: "ReadArgs" }, + { id: 8, name: "ls_args", type: "LsArgs" }, + { id: 10, name: "request_context_args", type: "RequestContextArgs" }, + { id: 11, name: "mcp_args", type: "McpArgs" }, + { id: 14, name: "shell_stream_args", type: "ShellArgs" }, + { id: 16, name: "background_shell_spawn_args", type: "BackgroundShellSpawnArgs" }, + { id: 17, name: "list_mcp_resources_exec_args", type: "ListMcpResourcesExecArgs" }, + { id: 18, name: "read_mcp_resource_exec_args", type: "ReadMcpResourceExecArgs" }, + { id: 28, name: "subagent_args", type: "SubagentArgs" }, + { id: 36, name: "mcp_state_exec_args", type: "McpStateExecArgs" }, + { id: 44, name: "git_diff_request", type: "GetDiffRequest" }, + { id: 45, name: "pi_read_args", type: "PiReadToolArgs" }, + { id: 46, name: "pi_bash_args", type: "PiBashToolArgs" }, + { id: 47, name: "pi_edit_args", type: "PiEditToolArgs" }, + { id: 48, name: "pi_write_args", type: "PiWriteArgs" }, + { id: 49, name: "pi_grep_args", type: "PiGrepToolArgs" }, + { id: 50, name: "pi_find_args", type: "PiFindToolArgs" }, + { id: 51, name: "pi_ls_args", type: "PiLsToolArgs" }, + ], [{ name: "args", fields: [ + "shell_args", "write_args", "delete_args", "grep_args", "read_args", "ls_args", + "request_context_args", "mcp_args", "shell_stream_args", "background_shell_spawn_args", + "list_mcp_resources_exec_args", "read_mcp_resource_exec_args", "mcp_state_exec_args", + "subagent_args", "git_diff_request", + "pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args", + "pi_grep_args", "pi_find_args", "pi_ls_args", + ] }]); + // ExecClientMessage — client sends tool result back + addType(root, "ExecClientMessage", [ + { id: 1, name: "id", type: "uint32" }, + { id: 15, name: "exec_id", type: "string" }, + { id: 39, name: "local_execution_time_ms", type: "uint64" }, + { id: 2, name: "shell_result", type: "ShellResult" }, + { id: 3, name: "write_result", type: "WriteResult" }, + { id: 4, name: "delete_result", type: "DeleteResult" }, + { id: 5, name: "grep_result", type: "GrepResult" }, + { id: 7, name: "read_result", type: "ReadResult" }, + { id: 8, name: "ls_result", type: "LsResult" }, + { id: 9, name: "diagnostics_result", type: "DiagnosticsResult" }, + { id: 10, name: "request_context_result", type: "RequestContextResult" }, + { id: 11, name: "mcp_result", type: "McpResult" }, + { id: 14, name: "shell_stream", type: "ShellStream" }, + { id: 16, name: "background_shell_spawn_result", type: "BackgroundShellSpawnResult" }, + { id: 17, name: "list_mcp_resources_exec_result", type: "ListMcpResourcesExecResult" }, + { id: 18, name: "read_mcp_resource_exec_result", type: "ReadMcpResourceExecResult" }, + { id: 20, name: "fetch_result", type: "FetchResult" }, + { id: 21, name: "record_screen_result", type: "RecordScreenResult" }, + { id: 22, name: "computer_use_result", type: "ComputerUseResult" }, + { id: 23, name: "write_shell_stdin_result", type: "WriteShellStdinResult" }, + { id: 28, name: "subagent_result", type: "SubagentResult" }, + { id: 29, name: "redacted_read_result", type: "ReadResult" }, + { id: 30, name: "force_background_shell_result", type: "ForceBackgroundShellResult" }, + { id: 31, name: "force_background_subagent_result", type: "ForceBackgroundSubagentResult" }, + { id: 36, name: "mcp_state_exec_result", type: "McpStateExecResult" }, + { id: 37, name: "subagent_await_result", type: "SubagentAwaitResult" }, + { id: 38, name: "smart_mode_classifier_result", type: "SmartModeClassifierResult" }, + { id: 40, name: "canvas_diagnostics_result", type: "CanvasDiagnosticsResult" }, + { id: 41, name: "shell_allowlist_precheck_result", type: "ShellAllowlistPrecheckResult" }, + { id: 42, name: "mcp_allowlist_precheck_result", type: "McpAllowlistPrecheckResult" }, + { id: 43, name: "web_fetch_allowlist_precheck_result", type: "WebFetchAllowlistPrecheckResult" }, + { id: 44, name: "git_diff_response", type: "GetDiffResponse" }, + { id: 46, name: "pi_read_result", type: "PiReadExecResult" }, + { id: 47, name: "pi_bash_result", type: "PiBashExecResult" }, + { id: 48, name: "pi_edit_result", type: "PiEditExecResult" }, + { id: 49, name: "pi_write_result", type: "PiWriteResult" }, + { id: 50, name: "pi_grep_result", type: "PiGrepExecResult" }, + { id: 51, name: "pi_find_result", type: "PiFindExecResult" }, + { id: 52, name: "pi_ls_result", type: "PiLsExecResult" }, + { id: 53, name: "conversation_search_result", type: "ConversationSearchResult" }, + { id: 54, name: "agent_store_conflict_result", type: "AgentStoreConflictResult" }, + { id: 55, name: "mini_swe_agent_bash_result", type: "ShellResult" }, + { id: 56, name: "adopt_result", type: "AdoptResult" }, + ], [{ name: "result", fields: [ + "shell_result", "write_result", "delete_result", "grep_result", "read_result", "ls_result", + "diagnostics_result", "request_context_result", "mcp_result", "shell_stream", "background_shell_spawn_result", + "list_mcp_resources_exec_result", "read_mcp_resource_exec_result", + "fetch_result", "record_screen_result", "computer_use_result", "write_shell_stdin_result", + "subagent_result", "redacted_read_result", + "force_background_shell_result", "force_background_subagent_result", + "mcp_state_exec_result", + "subagent_await_result", "smart_mode_classifier_result", "canvas_diagnostics_result", + "shell_allowlist_precheck_result", "mcp_allowlist_precheck_result", "web_fetch_allowlist_precheck_result", + "git_diff_response", + "pi_read_result", "pi_bash_result", "pi_edit_result", "pi_write_result", + "pi_grep_result", "pi_find_result", "pi_ls_result", + "conversation_search_result", "agent_store_conflict_result", + "mini_swe_agent_bash_result", "adopt_result", + ] }]); + addType(root, "ExecServerControlMessage", [ + { id: 1, name: "abort", type: "ExecServerAbort" }, + ]); + addType(root, "ExecServerAbort", [ + { id: 1, name: "id", type: "uint32" }, + ]); + // Client→server control (ACM #5). Distinct from ExecServerControlMessage (ASM #5). + // After every exec result (especially multi-frame shell_stream), the real CLI + // sends stream_close{id} — without it the cloud waits forever on shell execs. + addType(root, "ExecClientStreamClose", [{ id: 1, name: "id", type: "uint32" }]); + addType(root, "ExecClientThrow", [ + { id: 1, name: "id", type: "uint32" }, + { id: 2, name: "error", type: "string" }, + ]); + addType(root, "ExecClientHeartbeat", [{ id: 1, name: "id", type: "uint32" }]); + addType(root, "ExecClientControlMessage", [ + { id: 1, name: "stream_close", type: "ExecClientStreamClose" }, + { id: 2, name: "throw", type: "ExecClientThrow" }, + { id: 3, name: "heartbeat", type: "ExecClientHeartbeat" }, + ], [{ name: "message", fields: ["stream_close", "throw", "heartbeat"] }]); + // ── Run request types ── + addType(root, "ParameterValue", [ + { id: 1, name: "id", type: "string" }, + { id: 2, name: "value", type: "string" }, + ]); + addType(root, "RequestedModel", [ + { id: 1, name: "model_id", type: "string" }, + { id: 2, name: "max_mode", type: "bool" }, + { id: 3, name: "parameters", type: "ParameterValue", repeated: true }, + ]); + addType(root, "SelectedImageDimension", [ + { id: 1, name: "width", type: "int32" }, + { id: 2, name: "height", type: "int32" }, + ]); + addType(root, "SelectedImageBlobIdWithData", [ + { id: 1, name: "blob_id", type: "bytes" }, + { id: 2, name: "data", type: "bytes" }, + ]); + addType(root, "SelectedImage", [ + { id: 1, name: "blob_id", type: "bytes" }, + { id: 8, name: "data", type: "bytes" }, + { id: 9, name: "blob_id_with_data", type: "SelectedImageBlobIdWithData" }, + { id: 2, name: "uuid", type: "string" }, + { id: 3, name: "path", type: "string" }, + { id: 4, name: "dimension", type: "SelectedImageDimension" }, + { id: 7, name: "mime_type", type: "string" }, + ], [{ name: "data_or_blob_id", fields: ["blob_id", "data", "blob_id_with_data"] }]); + addType(root, "SelectedContext", [ + { id: 1, name: "selected_images", type: "SelectedImage", repeated: true }, + ]); + addType(root, "UserMessage", [ + { id: 1, name: "text", type: "string" }, + { id: 2, name: "message_id", type: "string" }, + { id: 3, name: "selected_context", type: "SelectedContext" }, + ]); + // RequestContext — UserMessageAction #2. Live per-turn tools go here + // (AgentRunRequest.mcp_tools #4 is prewarm-only / empty on real turns). + addType(root, "RequestContext", [ + { id: 2, name: "rules", type: "CursorRule", repeated: true }, + { id: 4, name: "env", type: "RequestContextEnv" }, + { id: 6, name: "repository_info", type: "RepositoryIndexingInfo", repeated: true }, + { id: 7, name: "tools", type: "McpToolDefinition", repeated: true }, + { id: 11, name: "git_repos", type: "GitRepoInfo", repeated: true }, + { id: 13, name: "project_layouts", type: "LsDirectoryTreeNode", repeated: true }, + { id: 17, name: "web_search_enabled", type: "bool" }, + { id: 22, name: "custom_subagents", type: "CustomSubagent", repeated: true }, + { id: 23, name: "mcp_file_system_options", type: "McpFileSystemOptions" }, + { id: 24, name: "web_fetch_enabled", type: "bool" }, + { id: 25, name: "hooks_additional_context", type: "string" }, + { id: 29, name: "agent_skills", type: "AgentSkill", repeated: true }, + { id: 33, name: "git_repo_info_complete", type: "bool" }, + { id: 34, name: "mcp_meta_tool_options", type: "McpMetaToolOptions" }, + { id: 36, name: "mcp_info_complete", type: "bool" }, + { id: 39, name: "rules_info_complete", type: "bool" }, + { id: 40, name: "env_info_complete", type: "bool" }, + { id: 41, name: "repository_info_complete", type: "bool" }, + { id: 42, name: "custom_subagents_info_complete", type: "bool" }, + { id: 43, name: "agent_skills_info_complete", type: "bool" }, + { id: 44, name: "mcp_file_system_info_complete", type: "bool" }, + { id: 45, name: "git_status_info_complete", type: "bool" }, + { id: 46, name: "user_permissions_auto_run", type: "PermissionsAutoRunInstructions" }, + { id: 47, name: "project_permissions_auto_run", type: "PermissionsAutoRunInstructions" }, + ]); + addType(root, "UserMessageAction", [ + { id: 1, name: "user_message", type: "UserMessage" }, + { id: 2, name: "request_context", type: "RequestContext" }, + ]); + addType(root, "ResumeAction", [ + // Cursor CLI sends an empty ResumeAction. Field #2 is available for a + // refreshed RequestContext; the conversation id belongs to AgentRunRequest. + { id: 2, name: "request_context", type: "RequestContext" }, + ]); + addType(root, "CancelAction", [ + { id: 1, name: "conversation_id", type: "string" }, + ]); + // Deferred answers for an AskQuestion the client already replied to with + // `async`. `original_args` is kept as raw bytes so the exact AskQuestionArgs + // Cursor sent — including fields this schema does not model — is echoed back + // verbatim rather than re-encoded from a lossy decode. + addType(root, "AsyncAskQuestionCompletionAction", [ + { id: 1, name: "original_tool_call_id", type: "string" }, + { id: 2, name: "original_args", type: "bytes" }, + { id: 3, name: "result", type: "AskQuestionResult" }, + ]); + addType(root, "ConversationAction", [ + { id: 1, name: "user_message_action", type: "UserMessageAction" }, + { id: 2, name: "resume_action", type: "ResumeAction" }, + { id: 3, name: "cancel_action", type: "CancelAction" }, + { id: 8, name: "async_ask_question_completion_action", type: "AsyncAskQuestionCompletionAction" }, + ], [{ + name: "action", + fields: [ + "user_message_action", + "resume_action", + "cancel_action", + "async_ask_question_completion_action", + ], + }]); + // Seed ConversationStateStructure for turn 1 (system prompt as JSON strings + // in #1). After the first conversation_checkpoint_update we echo opaque + // server bytes instead — CLI's live structure uses blob-id bytes in #1/#8. + addType(root, "AssistantMessage", [ + { id: 1, name: "text", type: "string" }, + ]); + addType(root, "ConversationStep", [ + { id: 1, name: "assistant_message", type: "AssistantMessage" }, + ], [{ name: "message", fields: ["assistant_message"] }]); + addType(root, "AgentConversationTurn", [ + { id: 1, name: "user_message", type: "UserMessage" }, + { id: 2, name: "steps", type: "ConversationStep", repeated: true }, + ]); + addType(root, "ConversationTurn", [ + { id: 1, name: "agent_conversation_turn", type: "AgentConversationTurn" }, + ], [{ name: "turn", fields: ["agent_conversation_turn"] }]); + // Cursor checkpoints carry authoritative context occupancy in field #5. The + // provider still transports the complete checkpoint as opaque bytes; these + // declarations support targeted diagnostics/tests without a re-encode. + addType(root, "PromptTokenBreakdownCategory", [ + { id: 1, name: "id", type: "string" }, + { id: 2, name: "label", type: "string" }, + { id: 3, name: "estimated_tokens", type: "uint32" }, + { id: 4, name: "character_count", type: "uint32" }, + ]); + addType(root, "PromptTokenBreakdownSnapshot", [ + { id: 1, name: "total_used_tokens", type: "uint32" }, + { id: 2, name: "max_tokens", type: "uint32" }, + { id: 3, name: "categories", type: "PromptTokenBreakdownCategory", repeated: true }, + ]); + addType(root, "ConversationTokenDetails", [ + { id: 1, name: "used_tokens", type: "uint32" }, + { id: 2, name: "max_tokens", type: "uint32" }, + { id: 3, name: "breakdown", type: "PromptTokenBreakdownSnapshot" }, + ]); + // Seed schema (JSON strings). AgentRunRequest.conversation_state remains + // bytes, so live checkpoints are never decoded/re-encoded on transport. + addType(root, "ConversationStateStructure", [ + { id: 1, name: "root_prompt_messages_json", type: "string", repeated: true }, + { id: 5, name: "token_details", type: "ConversationTokenDetails" }, + { id: 8, name: "turns", type: "ConversationTurn", repeated: true }, + ]); + addType(root, "ModelDetails", [ + { id: 1, name: "model_id", type: "string" }, + ]); + addType(root, "McpTools", [ + { id: 1, name: "mcp_tools", type: "McpToolDefinition", repeated: true }, + ]); + addType(root, "AgentRunRequest", [ + { id: 1, name: "conversation_state", type: "bytes" }, + { id: 2, name: "action", type: "ConversationAction" }, + { id: 4, name: "mcp_tools", type: "McpTools" }, + { id: 5, name: "conversation_id", type: "string" }, + { id: 8, name: "custom_system_prompt", type: "string" }, + { id: 9, name: "requested_model", type: "RequestedModel" }, + { id: 10, name: "unknown_flag", type: "uint32" }, + { id: 12, name: "field_12", type: "uint32" }, + { id: 14, name: "selected_subagent_models", type: "RequestedModel", repeated: true }, + { id: 16, name: "conversation_group_id", type: "string" }, + { id: 25, name: "run_id", type: "string" }, + ]); + // ── Client heartbeat ── + addType(root, "ClientHeartbeat", []); + // ── Interaction query/response channel ── + // Cursor sends UI/integration requests outside the exec tool channel. Query + // bodies stay opaque: this headless provider only needs their id + variant + // to return a conservative typed response on the same Run stream. + addType(root, "InteractionQuery", [ + { id: 1, name: "id", type: "uint32" }, + { id: 2, name: "web_search_request_query", type: "bytes" }, + { id: 3, name: "ask_question_interaction_query", type: "bytes" }, + { id: 4, name: "switch_mode_request_query", type: "bytes" }, + { id: 7, name: "create_plan_request_query", type: "bytes" }, + { id: 8, name: "setup_vm_environment_args", type: "bytes" }, + { id: 9, name: "web_fetch_request_query", type: "bytes" }, + { id: 10, name: "pr_management_request_query", type: "bytes" }, + { id: 11, name: "mcp_auth_request_query", type: "bytes" }, + { id: 12, name: "generate_image_request_query", type: "bytes" }, + { id: 13, name: "replace_env_args", type: "bytes" }, + { id: 14, name: "connect_scm_request_query", type: "bytes" }, + ], [{ + name: "query", + fields: [ + "web_search_request_query", "ask_question_interaction_query", + "switch_mode_request_query", "create_plan_request_query", + "setup_vm_environment_args", "web_fetch_request_query", + "pr_management_request_query", "mcp_auth_request_query", + "generate_image_request_query", "replace_env_args", + "connect_scm_request_query", + ], + }]); + addType(root, "WebSearchRequestApproved", []); + addType(root, "WebSearchRequestRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "WebSearchRequestResponse", [ + { id: 1, name: "approved", type: "WebSearchRequestApproved" }, + { id: 2, name: "rejected", type: "WebSearchRequestRejected" }, + ], [{ name: "result", fields: ["approved", "rejected"] }]); + addType(root, "AskQuestionRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "AskQuestionError", [{ id: 1, name: "error_message", type: "string" }]); + addType(root, "AskQuestionAsync", []); + addType(root, "AskQuestionSuccessAnswer", [ + { id: 1, name: "question_id", type: "string" }, + { id: 2, name: "selected_option_ids", type: "string", repeated: true }, + { id: 3, name: "freeform_text", type: "string" }, + ]); + addType(root, "AskQuestionSuccess", [ + { id: 1, name: "answers", type: "AskQuestionSuccessAnswer", repeated: true }, + ]); + addType(root, "AskQuestionResult", [ + { id: 1, name: "success", type: "AskQuestionSuccess" }, + { id: 2, name: "error", type: "AskQuestionError" }, + { id: 3, name: "rejected", type: "AskQuestionRejected" }, + { id: 4, name: "async", type: "AskQuestionAsync" }, + ], [{ name: "result", fields: ["success", "error", "rejected", "async"] }]); + addType(root, "AskQuestionInteractionResponse", [ + { id: 1, name: "result", type: "AskQuestionResult" }, + ]); + // InteractionQuery keeps query bodies opaque (see the InteractionQuery + // comment); this type decodes only the ask-question body on demand. + addType(root, "AskQuestionInteractionQuery", [ + { id: 1, name: "args", type: "AskQuestionArgs" }, + { id: 2, name: "tool_call_id", type: "string" }, + ]); + addType(root, "SwitchModeRequestApproved", []); + addType(root, "SwitchModeRequestRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "SwitchModeRequestResponse", [ + { id: 1, name: "approved", type: "SwitchModeRequestApproved" }, + { id: 2, name: "rejected", type: "SwitchModeRequestRejected" }, + ], [{ name: "result", fields: ["approved", "rejected"] }]); + // InteractionQuery keeps query bodies opaque; decoded on demand like + // AskQuestionInteractionQuery / CreatePlanRequestQuery. + addType(root, "SwitchModeRequestQuery", [ + { id: 1, name: "args", type: "SwitchModeToolArgs" }, + ]); + addType(root, "CreatePlanSuccess", []); + addType(root, "CreatePlanError", [{ id: 1, name: "error", type: "string" }]); + addType(root, "CreatePlanResult", [ + { id: 1, name: "success", type: "CreatePlanSuccess" }, + { id: 2, name: "error", type: "CreatePlanError" }, + { id: 3, name: "plan_uri", type: "string" }, + ], [{ name: "result", fields: ["success", "error"] }]); + addType(root, "CreatePlanRequestResponse", [ + { id: 1, name: "result", type: "CreatePlanResult" }, + ]); + // InteractionQuery keeps query bodies opaque; decoded on demand like + // AskQuestionInteractionQuery / GenerateImageRequestQuery. + addType(root, "CreatePlanRequestQuery", [ + { id: 1, name: "args", type: "CreatePlanArgs" }, + { id: 2, name: "tool_call_id", type: "string" }, + ]); + addType(root, "SetupVmEnvironmentSuccess", []); + addType(root, "SetupVmEnvironmentResult", [ + { id: 1, name: "success", type: "SetupVmEnvironmentSuccess" }, + ], [{ name: "result", fields: ["success"] }]); + addType(root, "WebFetchRequestApproved", []); + addType(root, "WebFetchRequestRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "WebFetchRequestResponse", [ + { id: 1, name: "approved", type: "WebFetchRequestApproved" }, + { id: 2, name: "rejected", type: "WebFetchRequestRejected" }, + ], [{ name: "result", fields: ["approved", "rejected"] }]); + addType(root, "PrManagementRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "PrManagementResult", [ + { id: 3, name: "rejected", type: "PrManagementRejected" }, + ], [{ name: "result", fields: ["rejected"] }]); + addType(root, "McpAuthRequestApproved", []); + addType(root, "McpAuthRequestRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "McpAuthRequestResponse", [ + { id: 1, name: "approved", type: "McpAuthRequestApproved" }, + { id: 2, name: "rejected", type: "McpAuthRequestRejected" }, + ], [{ name: "result", fields: ["approved", "rejected"] }]); + addType(root, "GenerateImageRequestApproved", [{ id: 1, name: "description", type: "string" }]); + addType(root, "GenerateImageRequestRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "GenerateImageRequestResponse", [ + { id: 1, name: "approved", type: "GenerateImageRequestApproved" }, + { id: 2, name: "rejected", type: "GenerateImageRequestRejected" }, + ], [{ name: "result", fields: ["approved", "rejected"] }]); + addType(root, "ReplaceEnvSuccess", []); + addType(root, "ReplaceEnvFailure", [ + { id: 1, name: "error_message", type: "string" }, + { id: 2, name: "setup_logs", type: "string" }, + ]); + addType(root, "ReplaceEnvResult", [ + { id: 1, name: "success", type: "ReplaceEnvSuccess" }, + { id: 2, name: "failure", type: "ReplaceEnvFailure" }, + ], [{ name: "result", fields: ["success", "failure"] }]); + addType(root, "ConnectScmRequestApproved", []); + addType(root, "ConnectScmRequestRejected", [{ id: 1, name: "reason", type: "string" }]); + addType(root, "ConnectScmRequestFailed", [{ id: 1, name: "error", type: "string" }]); + addType(root, "ConnectScmRequestResponse", [ + { id: 1, name: "approved", type: "ConnectScmRequestApproved" }, + { id: 2, name: "rejected", type: "ConnectScmRequestRejected" }, + { id: 3, name: "failed", type: "ConnectScmRequestFailed" }, + ], [{ name: "result", fields: ["approved", "rejected", "failed"] }]); + addType(root, "InteractionResponse", [ + { id: 1, name: "id", type: "uint32" }, + { id: 2, name: "web_search_request_response", type: "WebSearchRequestResponse" }, + { id: 3, name: "ask_question_interaction_response", type: "AskQuestionInteractionResponse" }, + { id: 4, name: "switch_mode_request_response", type: "SwitchModeRequestResponse" }, + { id: 7, name: "create_plan_request_response", type: "CreatePlanRequestResponse" }, + { id: 8, name: "setup_vm_environment_result", type: "SetupVmEnvironmentResult" }, + { id: 9, name: "web_fetch_request_response", type: "WebFetchRequestResponse" }, + { id: 10, name: "pr_management_result", type: "PrManagementResult" }, + { id: 11, name: "mcp_auth_request_response", type: "McpAuthRequestResponse" }, + { id: 12, name: "generate_image_request_response", type: "GenerateImageRequestResponse" }, + { id: 13, name: "replace_env_result", type: "ReplaceEnvResult" }, + { id: 14, name: "connect_scm_request_response", type: "ConnectScmRequestResponse" }, + ], [{ + name: "result", + fields: [ + "web_search_request_response", "ask_question_interaction_response", + "switch_mode_request_response", "create_plan_request_response", + "setup_vm_environment_result", "web_fetch_request_response", + "pr_management_result", "mcp_auth_request_response", + "generate_image_request_response", "replace_env_result", + "connect_scm_request_response", + ], + }]); + // ── KV blob store (cursor moves large payloads out-of-band via this channel) ── + // Server sends KvServerMessage (AgentServerMessage #4); client MUST reply with + // KvClientMessage (AgentClientMessage #3) on the same Run stream, echoing `id`. + // If we don't ack set_blob / answer get_blob, the server hangs (endless + // heartbeats, no response) — this was the "no response" root cause. + addType(root, "GetBlobArgs", [ + { id: 1, name: "blob_id", type: "bytes" }, + ]); + addType(root, "GetBlobResult", [ + { id: 1, name: "blob_data", type: "bytes" }, + ]); + addType(root, "SetBlobArgs", [ + { id: 1, name: "blob_id", type: "bytes" }, + { id: 2, name: "blob_data", type: "bytes" }, + ]); + addType(root, "SetBlobResult", [ + { id: 1, name: "error", type: "string" }, + ]); + addType(root, "KvServerMessage", [ + { id: 1, name: "id", type: "uint32" }, + { id: 2, name: "get_blob_args", type: "GetBlobArgs" }, + { id: 3, name: "set_blob_args", type: "SetBlobArgs" }, + ], [{ name: "message", fields: ["get_blob_args", "set_blob_args"] }]); + addType(root, "KvClientMessage", [ + { id: 1, name: "id", type: "uint32" }, + { id: 2, name: "get_blob_result", type: "GetBlobResult" }, + { id: 3, name: "set_blob_result", type: "SetBlobResult" }, + ], [{ name: "message", fields: ["get_blob_result", "set_blob_result"] }]); + // ── AgentClientMessage ── + addType(root, "AgentClientMessage", [ + { id: 1, name: "run_request", type: "AgentRunRequest" }, + { id: 2, name: "exec_client_message", type: "ExecClientMessage" }, + { id: 3, name: "kv_client_message", type: "KvClientMessage" }, + { id: 4, name: "conversation_action", type: "ConversationAction" }, + { id: 5, name: "exec_client_control_message", type: "ExecClientControlMessage" }, + { id: 6, name: "interaction_response", type: "InteractionResponse" }, + { id: 7, name: "client_heartbeat", type: "ClientHeartbeat" }, + ], [{ + name: "message", + fields: [ + "run_request", + "exec_client_message", + "kv_client_message", + "conversation_action", + "exec_client_control_message", + "interaction_response", + "client_heartbeat", + ], + }]); + // ── AgentServerMessage ── + addType(root, "AgentServerMessage", [ + { id: 1, name: "interaction_update", type: "InteractionUpdate" }, + { id: 2, name: "exec_server_message", type: "ExecServerMessage" }, + { id: 3, name: "conversation_checkpoint_update", type: "bytes" }, + { id: 4, name: "kv_server_message", type: "KvServerMessage" }, + { id: 5, name: "exec_server_control_message", type: "ExecServerControlMessage" }, + { id: 7, name: "interaction_query", type: "InteractionQuery" }, + ], [{ name: "message", fields: ["interaction_update", "exec_server_message", "conversation_checkpoint_update", "kv_server_message", "exec_server_control_message", "interaction_query"] }]); + // ── AvailableModels types ── + addType(root, "AvailableModelsRequest", []); + addType(root, "AvailableModelParameterDefinition", [ + { id: 1, name: "id", type: "string" }, + { id: 2, name: "values", type: "string", repeated: true }, + ]); + addType(root, "AvailableModelParameterValue", [ + { id: 1, name: "id", type: "string" }, + { id: 2, name: "value", type: "string" }, + ]); + addType(root, "AvailableModelVariant", [ + { id: 1, name: "display_name", type: "string" }, + { id: 2, name: "is_max_mode", type: "bool" }, + { id: 3, name: "is_default_max_config", type: "bool" }, + { id: 4, name: "is_default_non_max_config", type: "bool" }, + { id: 5, name: "parameter_values", type: "AvailableModelParameterValue", repeated: true }, + ]); + addType(root, "AvailableModelEntry", [ + { id: 1, name: "name", type: "string" }, + { id: 2, name: "default_on", type: "bool" }, + { id: 5, name: "supports_agent", type: "bool" }, + { id: 9, name: "supports_thinking", type: "bool" }, + { id: 10, name: "supports_images", type: "bool" }, + { id: 14, name: "supports_max_mode", type: "bool" }, + { id: 15, name: "context_token_limit", type: "uint32" }, + { id: 17, name: "client_display_name", type: "string" }, + { id: 18, name: "server_model_name", type: "string" }, + { id: 29, name: "parameter_definitions", type: "AvailableModelParameterDefinition", repeated: true }, + { id: 30, name: "variants", type: "AvailableModelVariant", repeated: true }, + ]); + addType(root, "AvailableModelsResponse", [ + { id: 1, name: "models", type: "AvailableModelEntry", repeated: true }, + ]); + return root; +} +// ── Singleton root ── +let _root = null; +export function getMessageTypes() { + if (!_root) { + _root = createMessageTypes(); + } + return _root; +} +export function encodeMessage(typeName, message) { + const root = getMessageTypes(); + const type = root.lookupType(typeName); + const err = type.verify(message); + if (err) + throw new Error(`Invalid message for ${typeName}: ${err}`); + return type.encode(type.fromObject(message)).finish(); +} +export function decodeMessage(typeName, data) { + const root = getMessageTypes(); + const type = root.lookupType(typeName); + const decoded = type.decode(data); + return type.toObject(decoded, { defaults: true, json: true, longs: Number }); +} +/** Decode without inventing absent proto defaults (used for byte-stable snapshots). */ +export function decodeMessageSparse(typeName, data) { + const root = getMessageTypes(); + const type = root.lookupType(typeName); + const decoded = type.decode(data); + return type.toObject(decoded, { json: true, longs: Number }); +} +/** Schema-free walk of one message's top-level wire fields (no nested descent). */ +function readRawFields(buf) { + const reader = protobuf.Reader.create(buf); + const fields = []; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const field = tag >>> 3; + const wireType = tag & 7; + switch (wireType) { + case 0: + fields.push({ field, wireType, varint: reader.uint64().toString() }); + break; + case 1: + fields.push({ field, wireType, fixed: reader.fixed64().toString() }); + break; + case 2: + fields.push({ field, wireType, bytes: reader.bytes() }); + break; + case 5: + fields.push({ field, wireType, fixed: reader.fixed32() }); + break; + default: + // Groups (3/4) aren't used on this wire; bail rather than desync the reader. + return fields; + } + } + return fields; +} +/** + * Diagnostic-only: walk AgentServerMessage.interaction_update(1).turn_ended(14) + * without going through our hand-written TurnEnded schema, so a `cache_write`/ + * `reasoning_tokens` question can be checked against the raw wire instead of + * guessed. Confirmed field ids 1-5 (input/output/cache_read/cache_write/ + * reasoning) decode correctly. Live captures routinely include `f4` explicitly + * as `0` while `f3` (cache_read) and `f5` (reasoning) are non-zero — Cursor's + * agent TurnEnded populates the field but does not report write counts here. + */ +export function debugWalkTurnEnded(payload) { + try { + const top = readRawFields(payload); + const iuField = top.find((f) => f.field === 1 && f.wireType === 2); + if (!iuField?.bytes) + return "(no interaction_update field)"; + const iu = readRawFields(iuField.bytes); + const teField = iu.find((f) => f.field === 14 && f.wireType === 2); + if (!teField?.bytes) + return "(no turn_ended field in interaction_update)"; + const te = readRawFields(teField.bytes); + return te + .map((f) => `f${f.field}:wt${f.wireType}=${f.varint ?? f.fixed ?? (f.bytes ? `bytes[${f.bytes.length}]` : "?")}`) + .join(" "); + } + catch (e) { + return `(walk failed: ${e.message})`; + } +} +/** + * Decode a protobuf sub-message from a frame payload that wraps it in + * a top-level field key + length varint. Skips the outer wrapper and + * decodes the inner body as `typeName`. + */ +export function decodeWrappedMessage(typeName, data) { + let offset = 1; // skip field key + // skip varint length + while (offset < data.length) { + const b = data[offset]; + offset++; + if (!(b & 0x80)) + break; + } + return decodeMessage(typeName, data.subarray(offset)); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/progress-continuation.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/progress-continuation.d.ts new file mode 100644 index 00000000..19edc5f5 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/progress-continuation.d.ts @@ -0,0 +1,25 @@ +/** + * Prefixes that can introduce a progress fragment. These are not complete + * answers by themselves; `isProgressOnlyAssistantText` still requires a + * fragment (no sentence punctuation) and, for most verbs, an inspection object. + */ +export declare const PROGRESS_ONLY_PATTERNS: readonly RegExp[]; +/** + * True only for a short progress *fragment* that ended the turn without tools. + * + * Complete answers that happen to start with a progress verb ("Looking at the + * code, the bug is in X", "Reviewing this PR: LGTM") must not match. A trailing + * ellipsis is allowed; any other `.!?`, colon, semicolon, or comma is not. + * Inspection verbs also need an inspection object ("the workspace", "this PR"), + * not copulas such as "is not needed". + */ +export declare function isProgressOnlyAssistantText(text: string): boolean; +export declare function shouldContinueProgressOnlyTurn(input: { + allowTools: boolean; + advertisedToolCount: number; + assistantText: string; + emittedHostTools: number; + continuationAttempts: number; + pendingExecs: number; +}): boolean; +export declare function progressOnlyContinuationPrompt(workspaceRoot?: string): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/progress-continuation.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/progress-continuation.js new file mode 100644 index 00000000..dcb460ec --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/progress-continuation.js @@ -0,0 +1,92 @@ +import { appendWorkspaceRootGrounding } from "./workspace-grounding.js"; +/** + * Prefixes that can introduce a progress fragment. These are not complete + * answers by themselves; `isProgressOnlyAssistantText` still requires a + * fragment (no sentence punctuation) and, for most verbs, an inspection object. + */ +export const PROGRESS_ONLY_PATTERNS = [ + /^checking\b/i, + /^inspecting\b/i, + /^let me check\b/i, + /^looking into\b/i, + /^looking at\b/i, + /^reviewing\b/i, + /^scanning\b/i, + /^verifying\b/i, + /^examining\b/i, + /^investigating\b/i, +]; +const PREFIXES_WITH_OBJECT = [ + /^checking\b/i, + /^inspecting\b/i, + /^reviewing\b/i, + /^scanning\b/i, + /^verifying\b/i, + /^examining\b/i, + /^investigating\b/i, +]; +const INSPECTION_OBJECT = /^(?:the|how|whether|if|for|on|this|that|it)\b/i; +const MAX_PROGRESS_FRAGMENT_WORDS = 8; +function normalizeWhitespace(text) { + return text.trim().replace(/\s+/g, " "); +} +function restAfterPrefix(text, prefix) { + return text.slice(prefix.length).trim(); +} +/** + * True only for a short progress *fragment* that ended the turn without tools. + * + * Complete answers that happen to start with a progress verb ("Looking at the + * code, the bug is in X", "Reviewing this PR: LGTM") must not match. A trailing + * ellipsis is allowed; any other `.!?`, colon, semicolon, or comma is not. + * Inspection verbs also need an inspection object ("the workspace", "this PR"), + * not copulas such as "is not needed". + */ +export function isProgressOnlyAssistantText(text) { + const trimmed = text.trim(); + if (!trimmed) + return false; + if (trimmed.length > 240) + return false; + if (/\n\s*\n/.test(trimmed)) + return false; + const normalized = normalizeWhitespace(trimmed); + const withoutEllipsis = normalized.replace(/(?:\.{3}|…)+\s*$/u, "").trim(); + if (!withoutEllipsis) + return false; + if (/[.!?;:]/.test(withoutEllipsis)) + return false; + if (withoutEllipsis.includes(",")) + return false; + if (withoutEllipsis.split(" ").length > MAX_PROGRESS_FRAGMENT_WORDS) + return false; + if (!PROGRESS_ONLY_PATTERNS.some((pattern) => pattern.test(withoutEllipsis))) + return false; + const lower = withoutEllipsis.toLowerCase(); + if (lower.startsWith("let me check")) { + return withoutEllipsis.length > "let me check".length; + } + if (lower.startsWith("looking into") || lower.startsWith("looking at")) { + const prefix = lower.startsWith("looking into") ? "looking into" : "looking at"; + return INSPECTION_OBJECT.test(restAfterPrefix(withoutEllipsis, prefix)); + } + if (PREFIXES_WITH_OBJECT.some((pattern) => pattern.test(withoutEllipsis))) { + const rest = withoutEllipsis.replace(/^\S+\s+/i, ""); + return INSPECTION_OBJECT.test(rest); + } + return false; +} +export function shouldContinueProgressOnlyTurn(input) { + return (input.allowTools && + input.advertisedToolCount > 0 && + input.pendingExecs === 0 && + input.emittedHostTools === 0 && + input.continuationAttempts < 1 && + isProgressOnlyAssistantText(input.assistantText)); +} +export function progressOnlyContinuationPrompt(workspaceRoot) { + const base = "Continue the same turn now. Your previous assistant message was only progress narration and ended without a tool call or a complete answer. Do not repeat the progress update. If evidence is still needed, call an available listed tool immediately; otherwise provide the complete user-facing answer now. Do not finish until the requested work or answer is complete."; + if (!workspaceRoot || !workspaceRoot.trim()) + return base; + return appendWorkspaceRootGrounding(base, workspaceRoot); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/request.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/request.d.ts new file mode 100644 index 00000000..b446029e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/request.d.ts @@ -0,0 +1,70 @@ +import { type OpencodeToolDef } from "./tools.js"; +import type { CursorImageInput } from "../image-input.js"; +export type SeedHistoryMessage = { + role: "system" | "user" | "assistant"; + content: string; +}; +export type RunRequestInput = { + text: string; + images?: CursorImageInput[]; + modelId: string; + conversationId: string; + /** Stable parent group; unlike conversationId, this survives compaction/rebase. */ + conversationGroupId?: string; + systemPrompt?: string; + /** + * Prior chat turns for a seed ConversationStateStructure (no checkpoint). + * Tool outputs, when required for compaction/recovery, are represented as + * user-role OpenCode host observations rather than assistant-authored prose. + */ + history?: SeedHistoryMessage[]; + /** + * Opaque ConversationStateStructure bytes from the last + * conversation_checkpoint_update for this conversation_id. When set, echoed + * as AgentRunRequest.conversation_state (CLI parity). When absent, a seed + * state with the system prompt (or empty) is built for turn 1. + */ + conversationState?: Uint8Array; + parameterValues?: Array<{ + id: string; + value: string; + }>; + maxMode?: boolean; + messageId?: string; + tools?: OpencodeToolDef[]; + /** Pre-resolved descriptors (including config-backed MCP server identity). */ + toolDescriptors?: Array>; + /** Prebuilt RequestContext (OpenCode-sourced). */ + requestContext?: Record; + /** Resume the supplied checkpoint instead of submitting another user turn. */ + action?: "user" | "resume"; +}; +/** + * Seed ConversationStateStructure for the first turn (no checkpoint yet). + * + * OpenCode needs a system prompt channel; we put it in root_prompt_messages_json + * as a JSON chat message. After the first checkpoint arrives we stop inventing + * state and echo the server's opaque structure instead (CLI behavior). + * + * Compaction resets also use this seed, with `history` carrying OpenCode's + * compacted prompt turns so Cursor can summarize without the old checkpoint. + * + * We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that + * field is the internal `--system-prompt` CLI override and the server rejects + * it for normal accounts. + */ +export declare function buildSeedConversationState(input?: { + systemPrompt?: string; + history?: SeedHistoryMessage[]; +}): Uint8Array; +/** + * Build an AgentClientMessage{run_request} for a conversation turn. + * + * Live `user_message.text` is the current prompt only — same as Cursor CLI. + * Cross-turn history is the last server checkpoint re-sent as conversation_state. + */ +export declare function buildRunRequest(input: RunRequestInput): Uint8Array; +/** + * Build a heartbeat message. + */ +export declare function buildHeartbeat(): Uint8Array; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/request.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/request.js new file mode 100644 index 00000000..6ab45365 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/request.js @@ -0,0 +1,108 @@ +import { encodeMessage, getMessageTypes } from "./messages.js"; +import { toolsToDescriptors } from "./tools.js"; +/** + * Seed ConversationStateStructure for the first turn (no checkpoint yet). + * + * OpenCode needs a system prompt channel; we put it in root_prompt_messages_json + * as a JSON chat message. After the first checkpoint arrives we stop inventing + * state and echo the server's opaque structure instead (CLI behavior). + * + * Compaction resets also use this seed, with `history` carrying OpenCode's + * compacted prompt turns so Cursor can summarize without the old checkpoint. + * + * We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that + * field is the internal `--system-prompt` CLI override and the server rejects + * it for normal accounts. + */ +export function buildSeedConversationState(input) { + const root = getMessageTypes(); + const type = root.lookupType("ConversationStateStructure"); + const messages = []; + if (input?.systemPrompt && input.systemPrompt.length > 0) { + messages.push(JSON.stringify({ role: "system", content: input.systemPrompt })); + } + for (const entry of input?.history ?? []) { + if (!entry.content) + continue; + // Avoid duplicating the system prompt when history also carries one. + if (entry.role === "system" && input?.systemPrompt) + continue; + messages.push(JSON.stringify({ role: entry.role, content: entry.content })); + } + const obj = {}; + if (messages.length > 0) + obj.root_prompt_messages_json = messages; + return type.encode(type.fromObject(obj)).finish(); +} +/** + * Build an AgentClientMessage{run_request} for a conversation turn. + * + * Live `user_message.text` is the current prompt only — same as Cursor CLI. + * Cross-turn history is the last server checkpoint re-sent as conversation_state. + */ +export function buildRunRequest(input) { + const msgId = input.messageId ?? crypto.randomUUID(); + // Advertise opencode's tools on the LIVE path: UserMessageAction.request_context + // (#2). AgentRunRequest.mcp_tools (#4) is prewarm-only / empty on real turns — + // putting tools only there is why the model fell back to native Grep/Read. + const tools = input.tools ?? []; + const mcpTools = input.toolDescriptors ?? (tools.length > 0 ? toolsToDescriptors(tools) : []); + const requestContext = input.requestContext; + const userMessage = { + text: input.text, + message_id: msgId, + }; + if (input.images?.length) { + userMessage.selected_context = { + selected_images: input.images.map((image) => ({ + data: image.data, + uuid: crypto.randomUUID(), + path: image.filename, + mime_type: image.mimeType, + })), + }; + } + const userMessageAction = { user_message: userMessage }; + if (requestContext) + userMessageAction.request_context = requestContext; + const action = input.action === "resume" + ? { resume_action: {} } + : { user_message_action: userMessageAction }; + const conversationState = input.conversationState && input.conversationState.length > 0 + ? input.conversationState + : buildSeedConversationState({ + systemPrompt: input.systemPrompt, + history: input.history, + }); + const runRequest = { + conversation_id: input.conversationId, + conversation_group_id: input.conversationGroupId ?? input.conversationId, + run_id: msgId, + action, + requested_model: { + // The provider always selects a concrete model. Cursor's "default" + // pseudo-model (Auto) is never used here — we send the real id plus the + // chosen variant's parameter values. + model_id: input.modelId, + max_mode: input.maxMode ?? false, + parameters: input.parameterValues ?? [], + }, + conversation_state: conversationState, + // Keep #4 populated too (harmless on real turns; useful for prewarm / + // older server builds that still read it). + mcp_tools: { mcp_tools: mcpTools }, + unknown_flag: 0, + field_12: 0, + }; + return encodeMessage("AgentClientMessage", { + run_request: runRequest, + }); +} +/** + * Build a heartbeat message. + */ +export function buildHeartbeat() { + return encodeMessage("AgentClientMessage", { + client_heartbeat: {}, + }); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/stream.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/stream.d.ts new file mode 100644 index 00000000..b624e993 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/stream.d.ts @@ -0,0 +1,38 @@ +export type StreamEvent = { + type: "text-delta"; + text: string; +} | { + type: "reasoning-delta"; + text: string; +} | { + type: "finish"; + usage: { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + }; + finishReason: string; +} | { + type: "tool-call-started"; + callId: string; + toolName: string; + args: string; +} | { + type: "tool-call-completed"; + callId: string; + result: string; +} | { + type: "tool-input-delta"; + callId: string; + delta: string; +} | { + type: "heartbeat"; +} | { + type: "step"; +}; +/** + * Parse an AgentServerMessage frame payload into a stream event. + * Returns null for frames that should be skipped. + */ +export declare function parseInteractionUpdate(payload: Uint8Array): StreamEvent | null; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/stream.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/stream.js new file mode 100644 index 00000000..4efc9b09 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/stream.js @@ -0,0 +1,70 @@ +import { decodeMessage } from "./messages.js"; +/** + * Parse an AgentServerMessage frame payload into a stream event. + * Returns null for frames that should be skipped. + */ +export function parseInteractionUpdate(payload) { + const asm = decodeMessage("AgentServerMessage", payload); + const iu = asm.interaction_update; + if (!iu) + return null; + if (iu.heartbeat) + return { type: "heartbeat" }; + if (iu.text_delta) { + const td = iu.text_delta; + return { type: "text-delta", text: td.text ?? "" }; + } + if (iu.thinking_delta) { + const td = iu.thinking_delta; + return { type: "reasoning-delta", text: td.text ?? "" }; + } + if (iu.turn_ended) { + const te = iu.turn_ended; + return { + type: "finish", + usage: { + input: te.input_tokens ?? 0, + output: te.output_tokens ?? 0, + cacheRead: te.cache_read ?? 0, + cacheWrite: te.cache_write ?? 0, + }, + finishReason: "stop", + }; + } + if (iu.tool_call_started) { + const tc = iu.tool_call_started; + const toolCall = tc.tool_call; + const variant = toolCall && typeof toolCall === "object" + ? Object.keys(toolCall).find((k) => k.endsWith("_tool_call")) + : undefined; + const variantPayload = variant && toolCall ? toolCall[variant] : undefined; + const args = variantPayload?.args ?? variantPayload; + return { + type: "tool-call-started", + callId: tc.call_id ?? "", + toolName: variant ?? "", + args: JSON.stringify(args ?? {}), + }; + } + if (iu.tool_call_completed) { + const tc = iu.tool_call_completed; + const toolCall = tc.tool_call; + return { + type: "tool-call-completed", + callId: tc.call_id ?? "", + result: JSON.stringify(toolCall ?? {}), + }; + } + if (iu.partial_tool_call) { + const ptc = iu.partial_tool_call; + return { + type: "tool-input-delta", + callId: ptc.call_id ?? "", + delta: ptc.args_text_delta ?? "", + }; + } + if (iu.step_started || iu.step_completed) { + return { type: "step" }; + } + return null; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/struct.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/struct.d.ts new file mode 100644 index 00000000..888ff6a1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/struct.d.ts @@ -0,0 +1,37 @@ +export type RawField = { + fn: number; + wt: number; + varint: number; + bytes?: Uint8Array; + i64?: Uint8Array; +}; +/** Walk a protobuf message's top-level fields off the raw wire bytes. */ +export declare function readAllFields(b: Uint8Array): RawField[]; +export type StrictRawField = { + fn: number; + wt: number; + varint?: bigint; + bytes?: Uint8Array; + fixed64?: Uint8Array; + fixed32?: Uint8Array; +}; +/** + * Strictly walk a protobuf message for security/replay decisions. + * + * Unlike `readAllFields`, this parser consumes the complete input, preserves + * uint64 varints as bigint, represents fixed-width fields, and rejects invalid + * tags, truncation, overflow, groups, and unsupported wire types. It deliberately + * returns `undefined` instead of a partial result: callers must fail closed when + * deciding whether replay is safe. + */ +export declare function readAllFieldsStrict(bytes: Uint8Array): StrictRawField[] | undefined; +/** Decode a google.protobuf.Value message (bytes) back into a JSON value. */ +export declare function decodeValueToJson(bytes: Uint8Array): unknown; +/** + * Decode a `map` field that was captured as repeated map-entry + * messages (each `{1 key, 2 value}`) into a plain JSON object. Used for + * `McpArgs.args`, which arrives as repeated field #2 on the wire. + */ +export declare function decodeStructEntriesToJson(entries: Uint8Array[]): Record; +/** Encode an arbitrary JSON value as google.protobuf.Value bytes. */ +export declare function encodeJsonAsValue(v: unknown): Uint8Array; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/struct.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/struct.js new file mode 100644 index 00000000..e1e4d1c8 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/struct.js @@ -0,0 +1,268 @@ +// Minimal encoder for google.protobuf.Value / Struct / ListValue. +// +// Cursor's per-turn tool descriptors carry the tool `input_schema` (a JSON +// Schema object) as a `google.protobuf.Value` whose `struct_value` holds the +// schema — NOT a JSON string. Verified against a live capture +// (request_context #7 → McpToolDefinition #3): +// 2a 9a07 0a10 0a04 74797065 1208 1a06 6f626a656374 ... +// = Value{#5 struct_value: Struct{#1 fields: {"type": Value{#3 string "object"}}}} +// +// Field numbers (well-known types): +// Value: 1 null_value, 2 number_value(double), 3 string_value, +// 4 bool_value, 5 struct_value(Struct), 6 list_value(ListValue) +// Struct: 1 fields (map) — each entry msg{1 key, 2 value} +// ListValue: 1 values (repeated Value) +function writeVarint(out, n) { + let v = n >>> 0; + while (v > 0x7f) { + out.push((v & 0x7f) | 0x80); + v >>>= 7; + } + out.push(v); +} +function writeTag(out, field, wire) { + writeVarint(out, (field << 3) | wire); +} +function writeLengthDelimited(out, field, bytes) { + writeTag(out, field, 2); + writeVarint(out, bytes.length); + for (let i = 0; i < bytes.length; i++) + out.push(bytes[i]); +} +function writeString(out, field, str) { + writeLengthDelimited(out, field, new TextEncoder().encode(str)); +} +function writeDouble(out, field, num) { + writeTag(out, field, 1); // 64-bit + const buf = new ArrayBuffer(8); + new DataView(buf).setFloat64(0, num, true); // little-endian + const view = new Uint8Array(buf); + for (let i = 0; i < 8; i++) + out.push(view[i]); +} +function readVarintAt(b, i) { + let r = 0, s = 0; + while (i < b.length) { + const x = b[i++]; + r |= (x & 0x7f) << s; + if (!(x & 0x80)) + break; + s += 7; + } + return [r >>> 0, i]; +} +/** Walk a protobuf message's top-level fields off the raw wire bytes. */ +export function readAllFields(b) { + let i = 0; + const out = []; + while (i < b.length) { + let key; + [key, i] = readVarintAt(b, i); + const fn = key >>> 3, wt = key & 7; + if (wt === 0) { + let v; + [v, i] = readVarintAt(b, i); + out.push({ fn, wt, varint: v }); + } + else if (wt === 2) { + let len; + [len, i] = readVarintAt(b, i); + out.push({ fn, wt, varint: 0, bytes: b.subarray(i, i + len) }); + i += len; + } + else if (wt === 1) { + out.push({ fn, wt, varint: 0, i64: b.subarray(i, i + 8) }); + i += 8; + } + else if (wt === 5) { + i += 4; // i32 — not used by Value + } + else + break; + } + return out; +} +const MAX_UINT32 = 0xffffffffn; +const MAX_UINT64 = 0xffffffffffffffffn; +const MAX_PROTOBUF_FIELD_NUMBER = 0x1fff_ffff; +/** + * Decode one unsigned protobuf varint without JavaScript's 32-bit bitwise + * truncation. `maximum` also enforces the protocol width: tags and lengths are + * uint32, while wire-type 0 scalar values may use the full uint64 range. + */ +function readBoundedVarint(bytes, offset, maximum) { + let value = 0n; + let shift = 0n; + for (let index = offset; index < bytes.length && index < offset + 10; index++) { + const byte = bytes[index]; + value |= BigInt(byte & 0x7f) << shift; + if (value > maximum) + return undefined; + if ((byte & 0x80) === 0) + return { value, nextOffset: index + 1 }; + shift += 7n; + } + return undefined; +} +/** + * Strictly walk a protobuf message for security/replay decisions. + * + * Unlike `readAllFields`, this parser consumes the complete input, preserves + * uint64 varints as bigint, represents fixed-width fields, and rejects invalid + * tags, truncation, overflow, groups, and unsupported wire types. It deliberately + * returns `undefined` instead of a partial result: callers must fail closed when + * deciding whether replay is safe. + */ +export function readAllFieldsStrict(bytes) { + let offset = 0; + const fields = []; + while (offset < bytes.length) { + const tag = readBoundedVarint(bytes, offset, MAX_UINT32); + if (!tag) + return undefined; + offset = tag.nextOffset; + const fieldNumber = Number(tag.value >> 3n); + const wireType = Number(tag.value & 7n); + if (fieldNumber === 0 || fieldNumber > MAX_PROTOBUF_FIELD_NUMBER) + return undefined; + if (wireType === 0) { + const scalar = readBoundedVarint(bytes, offset, MAX_UINT64); + if (!scalar) + return undefined; + fields.push({ fn: fieldNumber, wt: wireType, varint: scalar.value }); + offset = scalar.nextOffset; + continue; + } + if (wireType === 1) { + if (offset + 8 > bytes.length) + return undefined; + fields.push({ fn: fieldNumber, wt: wireType, fixed64: bytes.subarray(offset, offset + 8) }); + offset += 8; + continue; + } + if (wireType === 2) { + const encodedLength = readBoundedVarint(bytes, offset, MAX_UINT32); + if (!encodedLength) + return undefined; + offset = encodedLength.nextOffset; + const length = Number(encodedLength.value); + if (offset + length > bytes.length) + return undefined; + fields.push({ fn: fieldNumber, wt: wireType, bytes: bytes.subarray(offset, offset + length) }); + offset += length; + continue; + } + if (wireType === 5) { + if (offset + 4 > bytes.length) + return undefined; + fields.push({ fn: fieldNumber, wt: wireType, fixed32: bytes.subarray(offset, offset + 4) }); + offset += 4; + continue; + } + // Groups (3/4) are deprecated and recursive; no replay-safe frame uses them. + return undefined; + } + return fields; +} +function decodeStructBytes(b) { + const obj = {}; + for (const f of readAllFields(b)) { + if (f.fn !== 1 || !f.bytes) + continue; // Struct.fields (map) entries + const { key, valBytes } = readMapEntry(f.bytes); + if (key !== undefined) + obj[key] = valBytes ? decodeValueToJson(valBytes) : null; + } + return obj; +} +function decodeListBytes(b) { + const arr = []; + for (const f of readAllFields(b)) { + if (f.fn === 1 && f.bytes) + arr.push(decodeValueToJson(f.bytes)); + } + return arr; +} +function readMapEntry(b) { + let key; + let valBytes; + for (const e of readAllFields(b)) { + if (e.fn === 1 && e.bytes) + key = new TextDecoder().decode(e.bytes); + else if (e.fn === 2 && e.bytes) + valBytes = e.bytes; + } + return { key, valBytes }; +} +/** Decode a google.protobuf.Value message (bytes) back into a JSON value. */ +export function decodeValueToJson(bytes) { + const fs = readAllFields(bytes); + if (fs.length === 0) + return null; + const f = fs[0]; + switch (f.fn) { + case 1: return null; // null_value + case 2: return f.i64 ? new DataView(f.i64.buffer, f.i64.byteOffset, 8).getFloat64(0, true) : 0; + case 3: return f.bytes ? new TextDecoder().decode(f.bytes) : ""; + case 4: return f.varint !== 0; + case 5: return f.bytes ? decodeStructBytes(f.bytes) : {}; + case 6: return f.bytes ? decodeListBytes(f.bytes) : []; + default: return null; + } +} +/** + * Decode a `map` field that was captured as repeated map-entry + * messages (each `{1 key, 2 value}`) into a plain JSON object. Used for + * `McpArgs.args`, which arrives as repeated field #2 on the wire. + */ +export function decodeStructEntriesToJson(entries) { + const obj = {}; + for (const entry of entries) { + const { key, valBytes } = readMapEntry(entry); + if (key !== undefined) + obj[key] = valBytes ? decodeValueToJson(valBytes) : null; + } + return obj; +} +/** Encode an arbitrary JSON value as google.protobuf.Value bytes. */ +export function encodeJsonAsValue(v) { + const out = []; + if (v === null || v === undefined) { + writeTag(out, 1, 0); // null_value = 0 + writeVarint(out, 0); + } + else if (typeof v === "number") { + writeDouble(out, 2, v); + } + else if (typeof v === "boolean") { + writeTag(out, 4, 0); + writeVarint(out, v ? 1 : 0); + } + else if (typeof v === "string") { + writeString(out, 3, v); + } + else if (Array.isArray(v)) { + const lv = []; + for (const item of v) + writeLengthDelimited(lv, 1, encodeJsonAsValue(item)); + writeLengthDelimited(out, 6, new Uint8Array(lv)); + } + else if (typeof v === "object") { + const st = []; + for (const [key, val] of Object.entries(v).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) { + if (val === undefined) + continue; + const entry = []; + writeString(entry, 1, key); + writeLengthDelimited(entry, 2, encodeJsonAsValue(val)); + writeLengthDelimited(st, 1, new Uint8Array(entry)); + } + writeLengthDelimited(out, 5, new Uint8Array(st)); + } + else { + // Fallback: treat as null. + writeTag(out, 1, 0); + writeVarint(out, 0); + } + return new Uint8Array(out); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/switch-mode.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/switch-mode.d.ts new file mode 100644 index 00000000..9912d82c --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/switch-mode.d.ts @@ -0,0 +1,178 @@ +/** + * Cursor-native SwitchMode ⇄ OpenCode `plan_enter` / `plan_exit`. + * + * Cursor raises `switch_mode_request_query` (#4) with SwitchModeArgs and blocks + * until the client replies approved{} or rejected{reason} (no async variant). + * Cursor CLI prompts the user, then flips unifiedMode and injects a + * `` describing how to behave in that mode. + * Display `switch_mode_tool_call` (#25) is a separate transcript record and is + * not replayed as a host tool. + * + * OpenCode 1.x mapping (advertisement-gated): + * - plan, spec → plan_enter + * - every other non-empty target → plan_exit (leave plan for OpenCode build), + * then inject the Cursor CLI-shaped mode reminder for that target + * + * Upstream OpenCode's plan tools are not mode flags. Each one asks the user + * through the Question service and, on "Yes", injects a synthetic user message + * carrying `agent: "plan" | "build"`, which `createUserMessage` turns into a + * `setAgentModel` primary-agent switch (opencode `tool/plan.ts`, + * `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two + * observable halves — the approval gate and the behavioural contract — are both + * reachable, so the protocol bridge degrades instead of refusing when the host + * tool is missing. OpenCode 2 additionally exposes `session.switchAgent`; its + * entrypoint installs a structural callback that selects the native `plan` or + * `build` primary agent after the owning Cursor Run becomes terminal: + * + * - entering plan/spec needs no host tool at all — approve immediately and let + * the injected carry the contract, exactly as Cursor CLI's + * own plan mode is prompt-enforced. + * - leaving plan mode is the gate the user must actually see, so it falls back + * to the host `question` tool and only rejects when that is absent too. + * + * Resolution is keyed solely on the advertised catalog under canonical + * OpenCode tool names — never on any external identity or model id. + * + * CLI reject reason for user declines: "Mode switch rejected by user" + * (chunk-7076/dist/ui.js onSwitchModeReject). + */ +import { type OpencodeQuestionInput } from "./ask-question.js"; +/** + * `PendingExec.resultField` for a held-open SwitchMode InteractionQuery. + * Continuation writes an InteractionResponse, not an ExecClientMessage. + */ +export declare const SWITCH_MODE_RESULT_FIELD = "switch_mode_request_response"; +/** CLI verbatim when the user rejects the mode switch. */ +export declare const USER_REJECTED_REASON = "Mode switch rejected by user"; +export declare const MISSING_QUERY_REASON = "Missing switch-mode query"; +export declare const MISSING_ARGS_REASON = "Missing switch-mode arguments"; +export declare const MISSING_TARGET_REASON = "Missing targetModeId"; +/** + * A turn that cannot call host tools is a lifecycle turn (title generation, + * compaction, summarization). OpenCode opens one alongside the real Run, and it + * must not mutate session state — approving there flips plan mode twice and + * lets the throwaway turn write its own plan file. + * + * Soft-ack with `approved{}` instead of `rejected{reason}`: a hard reject lands + * in the transcript and the real turn narrates "mode switches are blocked" + * even though the next Run will succeed. CreatePlan already soft-acks the same + * case; SwitchMode must match. + */ +export declare const PLAN_EXIT_UNAVAILABLE_REASON: string; +/** Upstream `PlanExitTool` wording, minus a plan path the provider cannot verify. */ +export declare const SWITCH_MODE_EXIT_QUESTION = "Planning is complete. Would you like to switch to the build agent and start implementing?"; +export declare const SWITCH_MODE_EXIT_HEADER = "Build Agent"; +export type SwitchModeHostTool = "plan_enter" | "plan_exit"; +/** + * How a SwitchMode query is satisfied this turn. + * + * - `native` the host advertises the matching plan tool; bridge to it. + * - `approve` entering plan mode needs no host tool — approve immediately. + * - `ack` lifecycle turn (`allowTools=false`): approve on the wire without + * mutating session mode (same soft-ack pattern as CreatePlan). + * - `question` leaving plan mode falls back to the host `question` prompt. + * - `reject` nothing can satisfy it; `reason` names the real cause. + */ +export type SwitchModeBridge = { + kind: "native"; + toolName: SwitchModeHostTool; +} | { + kind: "approve"; +} | { + kind: "ack"; +} | { + kind: "question"; + input: OpencodeQuestionInput; +} | { + kind: "reject"; + reason: string; +}; +export type CursorSwitchModeArgs = { + targetModeId: string; + explanation: string; + toolCallId: string; +}; +export type DecodedSwitchModeQuery = { + args: CursorSwitchModeArgs; + toolCallId: string; +}; +export type SwitchModeMapping = { + ok: true; + toolName: SwitchModeHostTool; +} | { + ok: false; + reason: string; +}; +/** Normalize Cursor mode ids for comparison. */ +export declare function normalizeSwitchModeId(targetModeId: string): string; +/** + * Map a Cursor unified-mode id onto an OpenCode plan enter/exit tool. + * Does not check advertisement — callers gate on the host catalog. + * + * plan/spec enter plan; every other non-empty target leaves plan via plan_exit + * (host-portable primary switch) and relies on {@link cursorModeSystemReminder} + * for CLI-shaped behavioral guidance. + */ +export declare function mapSwitchModeTarget(targetModeId: string): SwitchModeMapping; +/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */ +export declare function switchModeExitQuestionInput(): OpencodeQuestionInput; +/** + * Resolve how this SwitchMode target is satisfied, keyed only on the advertised + * catalog under canonical tool names. + * + * A missing plan tool is not a refusal: entering plan mode needs no host tool, + * and leaving it degrades to the `question` prompt so the user still approves + * before any execution starts. + */ +export declare function resolveSwitchModeBridge(targetModeId: string, options: { + allowTools: boolean; + advertised: ReadonlySet | Iterable; + /** Cursor unified mode currently recorded for this session, if any. */ + activeModeId?: string; +}): SwitchModeBridge; +/** Decode a `switch_mode_request_query` body, or undefined when unusable. */ +export declare function decodeSwitchModeQuery(queryBytes: Uint8Array): DecodedSwitchModeQuery | undefined; +/** OpenCode plan_enter / plan_exit advertise empty input. */ +export declare function switchModeToolInput(): Record; +/** Shape `approved{}` / `rejected{reason}` for encodeMessage. */ +export declare function switchModeApprovedResult(): Record; +export declare function switchModeRejectedResult(reason: string): Record; +/** + * Host tool outcome → Cursor SwitchModeRequestResponse. + * Question/permission declines and empty failures use the CLI user-reject string. + */ +export declare function switchModeResultFromToolOutput(output: string, isError: boolean): Record; +/** + * Host `question` outcome → Cursor SwitchModeRequestResponse, for the emulated + * plan exit. Only an explicit "Yes" approves; an unanswered or dismissed prompt + * keeps the model in plan mode rather than silently starting execution. + */ +export declare function switchModeResultFromQuestionOutput(output: string, isError: boolean): Record; +/** Record the approved Cursor mode for later system-reminder injection. */ +export declare function setActiveCursorMode(sessionKey: string | undefined, targetModeId: string, options?: { + bridgedPlanEntered?: boolean; +}): void; +export declare function getActiveCursorMode(sessionKey: string | undefined): string | undefined; +/** True while the provider has an approved Cursor plan/spec mode for this session. */ +export declare function isCursorPlanModeActive(sessionKey: string | undefined): boolean; +/** True after a bridged plan/spec Run has observed plan_enter removed by the host. */ +export declare function isBridgedCursorPlanModeActive(sessionKey: string | undefined): boolean; +export declare function clearActiveCursorMode(sessionKey: string | undefined): void; +export declare function resetActiveCursorModesForTests(): void; +/** + * Cursor CLI-shaped mode reminder for the active unified mode. + * Tool names are adapted to OpenCode (`task`, `question`) where the CLI names + * Cursor-only tools; the behavioral contract matches the CLI reminders. + */ +export declare function cursorModeSystemReminder(targetModeId: string, options?: { + firstTurn?: boolean; + planExitAdvertised?: boolean; + planStageAdvertised?: boolean; +}): string | undefined; +/** + * Consume the active-mode reminder for this OpenCode session. + * Marks the mode as no longer first-turn after the first successful read. + */ +export declare function takeActiveCursorModeReminder(sessionKey: string | undefined, options?: { + advertisedTools?: ReadonlySet | Iterable; +}): string | undefined; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/switch-mode.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/switch-mode.js new file mode 100644 index 00000000..2b6122b7 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/switch-mode.js @@ -0,0 +1,434 @@ +/** + * Cursor-native SwitchMode ⇄ OpenCode `plan_enter` / `plan_exit`. + * + * Cursor raises `switch_mode_request_query` (#4) with SwitchModeArgs and blocks + * until the client replies approved{} or rejected{reason} (no async variant). + * Cursor CLI prompts the user, then flips unifiedMode and injects a + * `` describing how to behave in that mode. + * Display `switch_mode_tool_call` (#25) is a separate transcript record and is + * not replayed as a host tool. + * + * OpenCode 1.x mapping (advertisement-gated): + * - plan, spec → plan_enter + * - every other non-empty target → plan_exit (leave plan for OpenCode build), + * then inject the Cursor CLI-shaped mode reminder for that target + * + * Upstream OpenCode's plan tools are not mode flags. Each one asks the user + * through the Question service and, on "Yes", injects a synthetic user message + * carrying `agent: "plan" | "build"`, which `createUserMessage` turns into a + * `setAgentModel` primary-agent switch (opencode `tool/plan.ts`, + * `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two + * observable halves — the approval gate and the behavioural contract — are both + * reachable, so the protocol bridge degrades instead of refusing when the host + * tool is missing. OpenCode 2 additionally exposes `session.switchAgent`; its + * entrypoint installs a structural callback that selects the native `plan` or + * `build` primary agent after the owning Cursor Run becomes terminal: + * + * - entering plan/spec needs no host tool at all — approve immediately and let + * the injected carry the contract, exactly as Cursor CLI's + * own plan mode is prompt-enforced. + * - leaving plan mode is the gate the user must actually see, so it falls back + * to the host `question` tool and only rejects when that is absent too. + * + * Resolution is keyed solely on the advertised catalog under canonical + * OpenCode tool names — never on any external identity or model id. + * + * CLI reject reason for user declines: "Mode switch rejected by user" + * (chunk-7076/dist/ui.js onSwitchModeReject). + */ +import { parseAnswerSegments, } from "./ask-question.js"; +import { decodeMessageSparse } from "./messages.js"; +/** + * `PendingExec.resultField` for a held-open SwitchMode InteractionQuery. + * Continuation writes an InteractionResponse, not an ExecClientMessage. + */ +export const SWITCH_MODE_RESULT_FIELD = "switch_mode_request_response"; +/** CLI verbatim when the user rejects the mode switch. */ +export const USER_REJECTED_REASON = "Mode switch rejected by user"; +export const MISSING_QUERY_REASON = "Missing switch-mode query"; +export const MISSING_ARGS_REASON = "Missing switch-mode arguments"; +export const MISSING_TARGET_REASON = "Missing targetModeId"; +/** + * A turn that cannot call host tools is a lifecycle turn (title generation, + * compaction, summarization). OpenCode opens one alongside the real Run, and it + * must not mutate session state — approving there flips plan mode twice and + * lets the throwaway turn write its own plan file. + * + * Soft-ack with `approved{}` instead of `rejected{reason}`: a hard reject lands + * in the transcript and the real turn narrates "mode switches are blocked" + * even though the next Run will succeed. CreatePlan already soft-acks the same + * case; SwitchMode must match. + */ +export const PLAN_EXIT_UNAVAILABLE_REASON = "Neither the OpenCode `plan_exit` tool nor the `question` tool is available to the " + + "current agent, so leaving plan mode cannot be approved this turn."; +/** Upstream `PlanExitTool` wording, minus a plan path the provider cannot verify. */ +export const SWITCH_MODE_EXIT_QUESTION = "Planning is complete. Would you like to switch to the build agent and start implementing?"; +export const SWITCH_MODE_EXIT_HEADER = "Build Agent"; +const SWITCH_MODE_EXIT_YES = "Yes"; +const SWITCH_MODE_EXIT_NO = "No"; +const activeCursorModeBySession = new Map(); +const MAX_ACTIVE_CURSOR_MODES = 256; +function asRecord(value) { + return value && typeof value === "object" && !Array.isArray(value) + ? value + : undefined; +} +function str(value) { + return typeof value === "string" ? value : ""; +} +/** Normalize Cursor mode ids for comparison. */ +export function normalizeSwitchModeId(targetModeId) { + return targetModeId.trim().toLowerCase(); +} +/** + * Map a Cursor unified-mode id onto an OpenCode plan enter/exit tool. + * Does not check advertisement — callers gate on the host catalog. + * + * plan/spec enter plan; every other non-empty target leaves plan via plan_exit + * (host-portable primary switch) and relies on {@link cursorModeSystemReminder} + * for CLI-shaped behavioral guidance. + */ +export function mapSwitchModeTarget(targetModeId) { + const id = normalizeSwitchModeId(targetModeId); + if (!id) + return { ok: false, reason: MISSING_TARGET_REASON }; + if (id === "plan" || id === "spec") { + return { ok: true, toolName: "plan_enter" }; + } + return { ok: true, toolName: "plan_exit" }; +} +/** The synthetic AskQuestion item backing a question-emulated plan exit. */ +function switchModeExitQuestionItem() { + return { + id: "switch_mode_exit", + prompt: SWITCH_MODE_EXIT_QUESTION, + options: [ + { id: "yes", label: SWITCH_MODE_EXIT_YES }, + { id: "no", label: SWITCH_MODE_EXIT_NO }, + ], + allowMultiple: false, + }; +} +/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */ +export function switchModeExitQuestionInput() { + return { + questions: [ + { + question: SWITCH_MODE_EXIT_QUESTION, + header: SWITCH_MODE_EXIT_HEADER, + options: [ + { + label: SWITCH_MODE_EXIT_YES, + description: "Switch to build agent and start implementing the plan", + }, + { + label: SWITCH_MODE_EXIT_NO, + description: "Stay with plan agent to continue refining the plan", + }, + ], + }, + ], + }; +} +/** + * Resolve how this SwitchMode target is satisfied, keyed only on the advertised + * catalog under canonical tool names. + * + * A missing plan tool is not a refusal: entering plan mode needs no host tool, + * and leaving it degrades to the `question` prompt so the user still approves + * before any execution starts. + */ +export function resolveSwitchModeBridge(targetModeId, options) { + const mapped = mapSwitchModeTarget(targetModeId); + if (!mapped.ok) + return { kind: "reject", reason: mapped.reason }; + // Lifecycle turn: soft-ack on the wire, no session mutation. A hard reject + // would enter the transcript and make the real turn narrate a blocked switch. + if (!options.allowTools) + return { kind: "ack" }; + // Already in the target mode: nothing to approve. Cursor's own IDE handler + // auto-approves here too, and without this a model that asks to leave plan + // mode after the plan write already prompted would ask the user twice. + if (options.activeModeId + && normalizeSwitchModeId(options.activeModeId) === normalizeSwitchModeId(targetModeId)) { + return { kind: "approve" }; + } + const names = options.advertised instanceof Set + ? options.advertised + : new Set(options.advertised); + if (names.has(mapped.toolName)) + return { kind: "native", toolName: mapped.toolName }; + // Entering plan mode is provider-owned: the injected is the + // whole contract, exactly as Cursor CLI's own plan mode is prompt-enforced. + if (mapped.toolName === "plan_enter") + return { kind: "approve" }; + // Leaving plan mode starts real work, so it keeps a user-visible gate. + if (names.has("question")) { + return { kind: "question", input: switchModeExitQuestionInput() }; + } + return { kind: "reject", reason: PLAN_EXIT_UNAVAILABLE_REASON }; +} +/** Decode a `switch_mode_request_query` body, or undefined when unusable. */ +export function decodeSwitchModeQuery(queryBytes) { + let decoded; + try { + decoded = decodeMessageSparse("SwitchModeRequestQuery", queryBytes); + } + catch { + return undefined; + } + const argsRecord = asRecord(decoded.args); + if (!argsRecord) + return undefined; + const targetModeId = str(argsRecord.target_mode_id); + if (!targetModeId.trim()) + return undefined; + const toolCallId = str(argsRecord.tool_call_id) || str(decoded.tool_call_id); + return { + args: { + targetModeId, + explanation: str(argsRecord.explanation), + toolCallId, + }, + toolCallId, + }; +} +/** OpenCode plan_enter / plan_exit advertise empty input. */ +export function switchModeToolInput() { + return {}; +} +/** Shape `approved{}` / `rejected{reason}` for encodeMessage. */ +export function switchModeApprovedResult() { + return { approved: {} }; +} +export function switchModeRejectedResult(reason) { + return { rejected: { reason } }; +} +/** + * Host tool outcome → Cursor SwitchModeRequestResponse. + * Question/permission declines and empty failures use the CLI user-reject string. + */ +export function switchModeResultFromToolOutput(output, isError) { + if (!isError) + return switchModeApprovedResult(); + const trimmed = output.trim(); + // OpenCode plan tools throw Question.RejectedError on "No" — treat as user reject. + if (!trimmed + || /reject/i.test(trimmed) + || /denied/i.test(trimmed) + || /permission/i.test(trimmed) + || /cancelled|canceled|dismissed/i.test(trimmed)) { + return switchModeRejectedResult(USER_REJECTED_REASON); + } + return switchModeRejectedResult(trimmed); +} +/** + * Host `question` outcome → Cursor SwitchModeRequestResponse, for the emulated + * plan exit. Only an explicit "Yes" approves; an unanswered or dismissed prompt + * keeps the model in plan mode rather than silently starting execution. + */ +export function switchModeResultFromQuestionOutput(output, isError) { + if (isError) + return switchModeResultFromToolOutput(output, true); + const [segment] = parseAnswerSegments([switchModeExitQuestionItem()], output); + const answer = (segment ?? "").trim().toLowerCase(); + if (answer === SWITCH_MODE_EXIT_YES.toLowerCase()) + return switchModeApprovedResult(); + return switchModeRejectedResult(USER_REJECTED_REASON); +} +/** Record the approved Cursor mode for later system-reminder injection. */ +export function setActiveCursorMode(sessionKey, targetModeId, options = {}) { + if (!sessionKey) + return; + const modeId = normalizeSwitchModeId(targetModeId); + if (!modeId) + return; + activeCursorModeBySession.set(sessionKey, { + modeId, + firstTurn: true, + bridgedPlanEntered: options.bridgedPlanEntered === true, + }); + while (activeCursorModeBySession.size > MAX_ACTIVE_CURSOR_MODES) { + const oldest = activeCursorModeBySession.keys().next().value; + if (!oldest) + break; + activeCursorModeBySession.delete(oldest); + } +} +export function getActiveCursorMode(sessionKey) { + if (!sessionKey) + return undefined; + return activeCursorModeBySession.get(sessionKey)?.modeId; +} +/** True while the provider has an approved Cursor plan/spec mode for this session. */ +export function isCursorPlanModeActive(sessionKey) { + const mode = getActiveCursorMode(sessionKey); + return mode === "plan" || mode === "spec"; +} +/** True after a bridged plan/spec Run has observed plan_enter removed by the host. */ +export function isBridgedCursorPlanModeActive(sessionKey) { + if (!sessionKey) + return false; + const state = activeCursorModeBySession.get(sessionKey); + return (state?.modeId === "plan" || state?.modeId === "spec") && state.bridgedPlanEntered; +} +export function clearActiveCursorMode(sessionKey) { + if (!sessionKey) + return; + activeCursorModeBySession.delete(sessionKey); +} +export function resetActiveCursorModesForTests() { + activeCursorModeBySession.clear(); +} +function wrapReminder(body) { + return `\n${body.trim()}\n`; +} +/** + * Cursor CLI-shaped mode reminder for the active unified mode. + * Tool names are adapted to OpenCode (`task`, `question`) where the CLI names + * Cursor-only tools; the behavioral contract matches the CLI reminders. + */ +export function cursorModeSystemReminder(targetModeId, options = {}) { + const id = normalizeSwitchModeId(targetModeId); + if (!id) + return undefined; + const first = options.firstTurn !== false; + // A host plan-stage tool writes the artifact and its result names the + // follow-up that requests approval. plan_exit only leaves plan mode. + // Without either tool, recording the plan is the gate and the provider asks + // whether to start implementing. Naming an unavailable tool would strand the model. + const leavePlan = options.planStageAdvertised + ? "record the finished plan with Cursor CreatePlan. The host stage tool waits for the host plan review and does not return until the user accepts or declines. Do not call `plan_exit` to submit or skip that review, and do not implement until the tool returns success" + : options.planExitAdvertised === false + ? "record the finished plan (Cursor CreatePlan). Writing it needs no approval, and the user is then asked whether to start implementing; if they decline, refine the plan and record it again" + : "record the finished plan, then call OpenCode `plan_exit` so the user can approve leaving plan mode"; + if (id === "plan" || id === "spec") { + return wrapReminder(first + ? `Plan mode is active. The user does not want execution yet -- you MUST NOT make edits, run non-readonly tools (including changing configs or making commits), or otherwise modify system state. This supersedes any conflicting instruction. + +1. Research enough to make an accurate plan. +2. Before finishing, resolve decisions that would materially change the implementation path, touched files, architecture, user-visible behavior, data model, or validation strategy. If investigation cannot resolve one, ask clarifying questions in small batches (use the OpenCode \`question\` tool when available). +3. Do not put choices in the plan for the user to resolve. The plan must present one recommended approach, not unresolved questions or "choose A or B" options. +4. When ready, ${leavePlan}. +5. Do not execute the plan until the user confirms it.` + : `Plan mode is still active. You MUST NOT make edits, run non-readonly tools (including changing configs or making commits), or otherwise modify system state. This supersedes any conflicting instruction.`); + } + if (id === "chat") { + return wrapReminder(first + ? `Ask mode is active. The user wants you to answer questions about their codebase or coding in general. You MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits). + +Your role in Ask mode: + +1. Answer the user's questions comprehensively and accurately. Focus on providing clear, detailed explanations. +2. Use readonly tools to explore the codebase and gather information needed to answer the user's questions. +3. Provide code examples and references when helpful, citing specific file paths and line numbers. +4. If you need more information, ask the user for clarification (OpenCode \`question\` when available). +5. You may provide suggestions about how to implement something, but you MUST NOT actually implement it yourself. +6. If the user asks you to make changes or implement something, politely remind them that you're in Ask mode and can only provide information and guidance. Suggest they switch to Agent mode (OpenCode \`plan_exit\` / SwitchMode target agent) if they want you to make changes.` + : `Ask mode is still active. You MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).`); + } + if (id === "debug") { + return wrapReminder(first + ? `You are now in **DEBUG MODE**. You must debug with **runtime evidence**. + +**Why this approach:** Traditional AI agents jump to fixes claiming high confidence, but fail due to lacking runtime information. You cannot and must NOT fix bugs from code-only guesses — you need actual runtime data. + +**Your systematic workflow:** +1. Generate 3-5 precise hypotheses about WHY the bug occurs. +2. Instrument code with logs to test hypotheses in parallel. +3. Ask the user to reproduce the bug. Conclude with a ... block when the user must run something (mandatory unless the issue is fully confirmed fixed). +4. Analyze logs, accept/reject hypotheses, and only then implement a fix. +5. Verify the fix with runtime evidence before claiming success. + +Prefer reproduction, runtime logs, and end-to-end verification over speculative refactors.` + : `Debug mode is still active. You must debug with **runtime evidence**. + +**During fixes:** Do NOT remove instrumentation until post-fix verification logs prove success or the user explicitly asks you to remove it. +**Testing:** Prefer reproduction, runtime logs, and end-to-end verification; run tests when they directly exercise a hypothesis or confirm the final fix. +**Reproduction steps (MANDATORY):** Unless the issue is fully confirmed fixed, conclude with a ... block when the user must reproduce. +**If fix failed:** Generate NEW hypotheses from different subsystems and add more instrumentation.`); + } + if (id === "multitask") { + return wrapReminder(`Multitask Mode is active. You are a coordinator who pushes meaningful work to asynchronous workers through the OpenCode \`task\` tool (prefer background=true when the host supports it). + +- For most non-trivial requests, launch or resume one coherent worker and let that worker handle the investigation/implementation. +- After delegating the only coherent worker task, do not redo the same work in the foreground. Only coordinate, answer a new independent question, or synthesize after multiple workers return. +- NEVER await or sleep while waiting for a running subagent — end your response; you will be notified when it completes. +- Do NOT aggressively decompose small or medium tasks into many sibling agents. Multitask Mode is about moving substantial work out of the foreground, not maximizing parallel agents. +- For trivial requests (zero or one tool call), fulfill them directly and disregard these Multitask instructions.`); + } + if (id === "triage") { + return wrapReminder(first + ? `Triage mode is active. Your job is to coordinate long-horizon, multi-step work by delegating to subagents and integrating their progress. + +1. Break the user's task into well-scoped subtasks and launch subagents with the OpenCode \`task\` tool. Provide clear objectives and context so each subagent can make measurable progress. +2. Routinely inspect subagent output and synthesize results. +3. Decide next steps and iterate: launch additional agents, request revisions, or merge work when ready. +4. Throughout triage mode, maintain a global plan, document your decisions, and ensure the combined work moves the user toward their goal.` + : `Triage mode is still active. You must continue to coordinate long-horizon, multi-step work by delegating to subagents and integrating their progress.`); + } + if (id === "project") { + return wrapReminder(`You are Project Agent Mode: a long-running, high-level planner and orchestrator for complex software projects. + +Your mandate is to convert user intent into a correct, high-quality implementation by delegating nearly all work to subagents and coordinating them safely over long horizons. + +## Non-Negotiable Rules +1) Orchestrate, don't execute — you are NOT an implementer. Workspace modifications MUST be performed by OpenCode \`task\` subagents. +2) Task-first for everything — default to spawning subagents for research, exploration, implementation, validation, and review. Use your own read-only tools only for quick triage and synthesizing plans. +3) No work without clarity — if ambiguity remains, stop and ask clarifying questions (OpenCode \`question\` when available). +4) Phase-gated workflow — Clarify → Research → Plan → User Review → Implement → Review → Iterate → Finalize. Do not skip phases. +5) Externalize state — assume chat context may be condensed; keep durable progress notes so work can resume from written artifacts.`); + } + if (id === "background") { + return wrapReminder(`Cloud / background mode intent is active. Prefer long-running and environment/browser automation work via OpenCode \`task\` subagents (background=true when advertised) rather than blocking the foreground turn. + +- Useful when the task needs browser automation, multi-service environments, or should keep running if the local session is interrupted. +- After launching background work, end your response promptly; do not busy-wait. +- For short local unit/lint/typecheck work, stay in ordinary agent execution instead.`); + } + // agent / build / edit / unknown leave-plan targets: implementation mode. + return wrapReminder(first + ? `Agent mode is active (OpenCode build / agents). You have left plan mode and may implement: edit files, run tools, and execute the agreed approach. + +The plan-mode restriction is over. An earlier reminder that forbade edits no longer applies. + +- Change file contents with the advertised \`write\` and \`edit\` tools. +- Prefer making progress with the advertised host tools. +- If the task is large or ambiguous again, switch back to plan mode (OpenCode \`plan_enter\` / SwitchMode target plan) before a large rewrite.` + : `Agent mode is still active. Continue implementing with the advertised host tools, and change file contents with \`write\` and \`edit\`. Switch back to plan mode only when a new large/ambiguous design decision appears.`); +} +/** + * Consume the active-mode reminder for this OpenCode session. + * Marks the mode as no longer first-turn after the first successful read. + */ +export function takeActiveCursorModeReminder(sessionKey, options = {}) { + if (!sessionKey) + return undefined; + const state = activeCursorModeBySession.get(sessionKey); + if (!state) + return undefined; + const advertised = options.advertisedTools + ? options.advertisedTools instanceof Set + ? options.advertisedTools + : new Set(options.advertisedTools) + : undefined; + const isPlanMode = state.modeId === "plan" || state.modeId === "spec"; + if (isPlanMode && state.bridgedPlanEntered && advertised) { + if (advertised.has("plan_enter")) { + activeCursorModeBySession.delete(sessionKey); + return cursorModeSystemReminder("agent", { firstTurn: true }); + } + } + const reminder = cursorModeSystemReminder(state.modeId, { + firstTurn: state.firstTurn, + ...(advertised + ? { + planExitAdvertised: advertised.has("plan_exit"), + planStageAdvertised: advertised.has("cursor_plan_stage"), + } + : {}), + }); + if (state.firstTurn) + state.firstTurn = false; + return reminder; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/thinking.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/thinking.d.ts new file mode 100644 index 00000000..bd1e27ac --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/thinking.d.ts @@ -0,0 +1,15 @@ +export type EffortConfig = { + reasoningEffort?: string; + maxMode?: boolean; +}; +/** + * Build RequestedModel parameter list from a model's variant parameters + * and optional overrides from providerOptions. + */ +export declare function buildRequestedModelParams(variantParameters: Array<{ + id: string; + value: string; +}>, options?: EffortConfig): Array<{ + id: string; + value: string; +}>; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/thinking.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/thinking.js new file mode 100644 index 00000000..d28d2acb --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/thinking.js @@ -0,0 +1,17 @@ +// Reasoning effort / max mode → RequestedModel parameters +/** + * Build RequestedModel parameter list from a model's variant parameters + * and optional overrides from providerOptions. + */ +export function buildRequestedModelParams(variantParameters, options) { + // Start with variant's parameters as base + const params = variantParameters.map((p) => ({ ...p })); + // Override effort if specified + if (options?.reasoningEffort) { + const effortIdx = params.findIndex((p) => p.id === "effort" || p.id === "reasoning"); + if (effortIdx >= 0) { + params[effortIdx] = { ...params[effortIdx], value: options.reasoningEffort }; + } + } + return params; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/token-details.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/token-details.d.ts new file mode 100644 index 00000000..9c8bb307 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/token-details.d.ts @@ -0,0 +1,33 @@ +export type CursorPromptTokenCategory = { + id: string; + label: string; + estimatedTokens: number; + characterCount?: number; +}; +export type CursorPromptTokenBreakdown = { + totalUsedTokens: number; + maxTokens: number; + categories: CursorPromptTokenCategory[]; +}; +export type CursorConversationTokenDetails = { + usedTokens: number; + maxTokens: number; + breakdown?: CursorPromptTokenBreakdown; +}; +export type CursorContextUsageSource = "checkpoint-current-run" | "checkpoint-previous-turn"; +/** + * Read field #5 (`ConversationTokenDetails`) from Cursor's opaque + * `ConversationStateStructure` checkpoint without decoding or re-encoding the + * rest of the state. Unknown checkpoint fields therefore remain byte-exact. + */ +export declare function decodeConversationTokenDetails(checkpoint: Uint8Array | undefined): CursorConversationTokenDetails | undefined; +export declare function cursorContextUsageMetadata(details: CursorConversationTokenDetails, source?: CursorContextUsageSource): { + contextUsageVersion: number; + source: CursorContextUsageSource; + stale: boolean; + usedTokens: number; + maxTokens: number; + remainingTokens: number; + usedPercent?: number; + breakdown?: CursorPromptTokenBreakdown; +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/token-details.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/token-details.js new file mode 100644 index 00000000..dc723d62 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/token-details.js @@ -0,0 +1,155 @@ +import protobuf from "protobufjs"; +const MAX_BREAKDOWN_CATEGORIES = 128; +function requireWireType(actual, expected, field) { + if (actual !== expected) + throw new Error(`invalid ${field} wire type`); +} +function decodeCategory(data) { + const reader = protobuf.Reader.create(data); + let id = ""; + let label = ""; + let estimatedTokens = 0; + let characterCount; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const wireType = tag & 7; + switch (tag >>> 3) { + case 1: + requireWireType(wireType, 2, "category id"); + id = reader.string(); + break; + case 2: + requireWireType(wireType, 2, "category label"); + label = reader.string(); + break; + case 3: + requireWireType(wireType, 0, "category token count"); + estimatedTokens = reader.uint32(); + break; + case 4: + requireWireType(wireType, 0, "category character count"); + characterCount = reader.uint32(); + break; + default: + reader.skipType(wireType); + } + } + return { + id, + label, + estimatedTokens, + ...(characterCount === undefined ? {} : { characterCount }), + }; +} +function decodeBreakdown(data) { + const reader = protobuf.Reader.create(data); + let totalUsedTokens = 0; + let maxTokens = 0; + const categories = []; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const wireType = tag & 7; + switch (tag >>> 3) { + case 1: + requireWireType(wireType, 0, "breakdown used tokens"); + totalUsedTokens = reader.uint32(); + break; + case 2: + requireWireType(wireType, 0, "breakdown max tokens"); + maxTokens = reader.uint32(); + break; + case 3: + requireWireType(wireType, 2, "breakdown category"); + if (categories.length < MAX_BREAKDOWN_CATEGORIES) { + categories.push(decodeCategory(reader.bytes())); + } + else { + reader.bytes(); + } + break; + default: + reader.skipType(wireType); + } + } + return { totalUsedTokens, maxTokens, categories }; +} +function decodeTokenDetails(data) { + const reader = protobuf.Reader.create(data); + let usedTokens = 0; + let maxTokens = 0; + let sawUsedTokens = false; + let sawMaxTokens = false; + let breakdown; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const wireType = tag & 7; + switch (tag >>> 3) { + case 1: + requireWireType(wireType, 0, "used tokens"); + usedTokens = reader.uint32(); + sawUsedTokens = true; + break; + case 2: + requireWireType(wireType, 0, "max tokens"); + maxTokens = reader.uint32(); + sawMaxTokens = true; + break; + case 3: + requireWireType(wireType, 2, "token breakdown"); + breakdown = decodeBreakdown(reader.bytes()); + break; + default: + reader.skipType(wireType); + } + } + if (!sawUsedTokens && !sawMaxTokens && !breakdown) + return undefined; + return { + usedTokens, + maxTokens, + ...(breakdown ? { breakdown } : {}), + }; +} +/** + * Read field #5 (`ConversationTokenDetails`) from Cursor's opaque + * `ConversationStateStructure` checkpoint without decoding or re-encoding the + * rest of the state. Unknown checkpoint fields therefore remain byte-exact. + */ +export function decodeConversationTokenDetails(checkpoint) { + if (!checkpoint?.length) + return undefined; + try { + const reader = protobuf.Reader.create(checkpoint); + let details; + while (reader.pos < reader.len) { + const tag = reader.uint32(); + const wireType = tag & 7; + if ((tag >>> 3) === 5) { + requireWireType(wireType, 2, "conversation token details"); + details = decodeTokenDetails(reader.bytes()); + } + else { + reader.skipType(wireType); + } + } + return details; + } + catch { + return undefined; + } +} +export function cursorContextUsageMetadata(details, source = "checkpoint-current-run") { + const usedPercent = details.maxTokens > 0 + ? Math.max(0, Math.min(100, Math.round(details.usedTokens / details.maxTokens * 1_000) / 10)) + : undefined; + return { + contextUsageVersion: 2, + source, + stale: source === "checkpoint-previous-turn", + usedTokens: details.usedTokens, + maxTokens: details.maxTokens, + remainingTokens: Math.max(0, details.maxTokens - details.usedTokens), + ...(usedPercent === undefined ? {} : { usedPercent }), + ...(details.breakdown ? { breakdown: details.breakdown } : {}), + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tool-call-bridge.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tool-call-bridge.d.ts new file mode 100644 index 00000000..480b5886 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tool-call-bridge.d.ts @@ -0,0 +1,75 @@ +import { type HostToolDialect } from "./tools.js"; +/** + * Cursor's interaction_update.tool_call_* carries a typed ToolCall oneof + * (shell_tool_call, update_todos_tool_call, mcp_tool_call, …). Some of those + * never become ExecServerMessage requests — Cursor completes them display-only + * — so OpenCode would never see a tool-call unless we bridge them here. + */ +export type DisplayToolCall = { + callId: string; + variant: string; + /** Preferred OpenCode tool id before advertisement checks. */ + preferredToolName: string; + args: Record; + /** False when the Cursor call cannot be represented safely in OpenCode. */ + bridgeable?: boolean; +}; +export type BridgedOpenCodeToolCall = { + toolName: string; + args: Record; + callId: string; + variant: string; +}; +/** Display variants Cursor executes itself (mcp_state + optional write spill). */ +export declare function isNativeDisplayToolCall(variant: string): boolean; +/** + * Normalize a full todo snapshot for `mirroredTodos` storage. Returns + * undefined when the input is not a todo array (nothing to learn). Drops the + * synthetic "plan" id minted for create_plan mirrors so later merges never + * resurrect it. + */ +export declare function snapshotMirroredTodos(todos: unknown): Array> | undefined; +/** + * Refresh the mirrored snapshot from a host `todoread` tool result. The host + * list is authoritative; returns undefined when the output is not recognizable + * todo JSON (prose, caps, errors) so a stale-but-useful snapshot survives. + */ +export declare function snapshotMirroredTodosFromReadOutput(output: string): Array> | undefined; +/** + * Overlay Cursor merge patches onto a previously mirrored full todo list. + * Preserves prior order; appends newly introduced ids in patch order. + * + * Matching is by id first, then by exact (trimmed) content. The content + * fallback covers mixed flows where the prior snapshot came from a direct + * host `todowrite` (positional ids) while the patch references Cursor-side + * ids for the same tasks. Patch items with neither an id/content match nor + * their own content are skipped — a status-only update for an unknown item + * cannot be mapped onto the host list. + */ +export declare function applyTodoMerge(prior: ReadonlyArray>, patch: unknown): Array>; +/** + * Decode a Cursor ToolCall oneof into a display tool call we can bridge. + * + * @param priorMirroredTodos Last full todo snapshot mirrored this Run; used to + * expand Cursor `merge: true` updates that omit ToolCallCompleted.success.todos. + */ +export declare function parseDisplayToolCall(callId: string, toolCall: Record | undefined, priorMirroredTodos?: ReadonlyArray>): DisplayToolCall | undefined; +/** + * Pick an advertised OpenCode tool for a display call, remapping args as needed. + * Returns undefined when nothing compatible is advertised this turn. + */ +export declare function resolveBridgedOpenCodeToolCall(display: DisplayToolCall, advertised: Iterable, dialect?: HostToolDialect): BridgedOpenCodeToolCall | undefined; +/** Advertised OpenCode tool ids from Cursor McpToolDefinition descriptors. */ +export declare function advertisedToolNamesFromDescriptors(descriptors: Array>): string[]; +/** Walk a protobuf message and return top-level field numbers present. */ +export declare function listProtobufFieldNumbers(buf: Uint8Array): number[]; +/** + * Extract nested bytes for path of field numbers (each must be wire type 2). + * Returns undefined if any segment is missing. + */ +export declare function extractProtobufSubmessage(buf: Uint8Array, path: number[]): Uint8Array | undefined; +/** + * Pull Cursor's tool_call_id out of a raw ExecServerMessage args variant so we + * can correlate display tool_call_started with the authoritative exec path. + */ +export declare function extractExecDisplayCallId(execMsg: Record): string | undefined; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tool-call-bridge.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tool-call-bridge.js new file mode 100644 index 00000000..1df42bd6 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tool-call-bridge.js @@ -0,0 +1,719 @@ +import { OPENCODE_1_TOOL_DIALECT, mapCursorArgsToOpencode, mapCursorSubagentTypeToOpenCode, mcpRealToolName, } from "./tools.js"; +import { decodeStructEntriesToJson } from "./struct.js"; +import { mapSwitchModeTarget } from "./switch-mode.js"; +const TODO_STATUS = { + 0: "pending", + 1: "pending", + 2: "in_progress", + 3: "completed", + 4: "cancelled", +}; +/** + * Cursor ToolCall oneof field → default OpenCode tool id. + * + * F11: `delete_tool_call` has no OpenCode builtin; it remaps to bash (see the + * delete_tool_call branch below for `rm -f -- `). + */ +const VARIANT_TO_OPENCODE = { + shell_tool_call: "bash", + delete_tool_call: "bash", + glob_tool_call: "glob", + grep_tool_call: "grep", + read_tool_call: "read", + update_todos_tool_call: "todowrite", + read_todos_tool_call: "todoread", + edit_tool_call: "write", + ls_tool_call: "read", + mcp_tool_call: "mcp", + create_plan_tool_call: "todowrite", + web_search_tool_call: "websearch", + task_tool_call: "task", + ask_question_tool_call: "question", + fetch_tool_call: "webfetch", + web_fetch_tool_call: "webfetch", + switch_mode_tool_call: "plan_enter", + generate_image_tool_call: "generateimage", + await_tool_call: "await", + get_mcp_tools_tool_call: "get_mcp_tools", + pi_read_tool_call: "read", + pi_bash_tool_call: "bash", + pi_edit_tool_call: "edit", + pi_write_tool_call: "write", + pi_grep_tool_call: "grep", + pi_find_tool_call: "glob", + pi_ls_tool_call: "read", +}; +const TOOL_CALL_VARIANTS = Object.keys(VARIANT_TO_OPENCODE); +// ToolCallCompleted is a notification, not an execution request. Only mirror +// client-visible state whose authoritative final value is already present in +// the completed payload (or reconstructable from a prior mirrored snapshot for +// Cursor todo merges). Data-returning, interactive, and side-effecting calls +// must use an exec/interaction request channel where Cursor can receive their +// actual result. +// +// `get_mcp_tools_tool_call` is Cursor-native discovery (prompt name +// GetDynamicTools when dynamic namespaces are on). The agent executes it +// server-side after mcp_state; oversized catalogs spill through write_args. +const DISPLAY_STATE_MIRROR_VARIANTS = new Set([ + "update_todos_tool_call", + "read_todos_tool_call", + "create_plan_tool_call", +]); +const NATIVE_DISPLAY_VARIANTS = new Set([ + "get_mcp_tools_tool_call", +]); +/** Display variants Cursor executes itself (mcp_state + optional write spill). */ +export function isNativeDisplayToolCall(variant) { + return NATIVE_DISPLAY_VARIANTS.has(variant); +} +function asRecord(v) { + return v && typeof v === "object" && !Array.isArray(v) ? v : undefined; +} +function findToolVariant(toolCall) { + for (const key of TOOL_CALL_VARIANTS) { + if (toolCall[key] != null) + return key; + } + for (const [key, value] of Object.entries(toolCall)) { + if (!key.endsWith("_tool_call")) + continue; + if (value && typeof value === "object") + return key; + } + return undefined; +} +function mapTodoStatus(status) { + if (typeof status === "string" && status.length > 0) { + const s = status.toLowerCase().replace(/^todo_status_/, ""); + if (s === "unspecified") + return "pending"; + return s; + } + if (typeof status === "number" && TODO_STATUS[status]) + return TODO_STATUS[status]; + return "pending"; +} +function mapTodos(raw) { + if (!Array.isArray(raw)) + return []; + return raw.map((item, index) => { + const t = asRecord(item) ?? {}; + const content = typeof t.content === "string" ? t.content : ""; + const id = typeof t.id === "string" && t.id.length > 0 + ? t.id + : `todo_${index + 1}`; + return { + id, + content, + status: mapTodoStatus(t.status), + priority: typeof t.priority === "string" && t.priority ? t.priority : "medium", + }; + }); +} +/** + * Normalize a full todo snapshot for `mirroredTodos` storage. Returns + * undefined when the input is not a todo array (nothing to learn). Drops the + * synthetic "plan" id minted for create_plan mirrors so later merges never + * resurrect it. + */ +export function snapshotMirroredTodos(todos) { + if (!Array.isArray(todos)) + return undefined; + return mapTodos(todos).filter((t) => { + const id = typeof t.id === "string" ? t.id : ""; + return id.length > 0 && id !== "plan"; + }); +} +/** + * Refresh the mirrored snapshot from a host `todoread` tool result. The host + * list is authoritative; returns undefined when the output is not recognizable + * todo JSON (prose, caps, errors) so a stale-but-useful snapshot survives. + */ +export function snapshotMirroredTodosFromReadOutput(output) { + let parsed; + try { + parsed = JSON.parse(output); + } + catch { + return undefined; + } + const list = Array.isArray(parsed) ? parsed : (asRecord(parsed)?.todos ?? undefined); + if (!Array.isArray(list)) + return undefined; + return snapshotMirroredTodos(list); +} +/** + * Overlay Cursor merge patches onto a previously mirrored full todo list. + * Preserves prior order; appends newly introduced ids in patch order. + * + * Matching is by id first, then by exact (trimmed) content. The content + * fallback covers mixed flows where the prior snapshot came from a direct + * host `todowrite` (positional ids) while the patch references Cursor-side + * ids for the same tasks. Patch items with neither an id/content match nor + * their own content are skipped — a status-only update for an unknown item + * cannot be mapped onto the host list. + */ +export function applyTodoMerge(prior, patch) { + const rawPatch = Array.isArray(patch) ? patch : []; + const mappedPatch = mapTodos(patch); + const byId = new Map(); + const byContent = new Map(); + // Patch ids already folded into a prior record via content match: never + // re-emit them as new items in the output phase. + const consumedPatchIds = new Set(); + // Patch ids accepted as genuinely new content: the only patch ids the + // output phase may append. Orphans skipped above (unknown id, no content) + // must not leak back in here. + const appendedPatchIds = new Set(); + for (const item of prior) { + const id = typeof item.id === "string" && item.id.length > 0 ? item.id : ""; + if (!id) + continue; + const normalized = { + id, + content: typeof item.content === "string" ? item.content : "", + status: typeof item.status === "string" ? item.status : mapTodoStatus(item.status), + priority: typeof item.priority === "string" && item.priority ? item.priority : "medium", + }; + byId.set(id, normalized); + const key = normalized.content.trim(); + if (key && !byContent.has(key)) + byContent.set(key, normalized); + } + const overlay = (target, item, raw) => { + // Only overwrite fields the patch actually carries. mapTodos fills + // defaults ("" content, "pending" status, "medium" priority) that must not + // clobber a prior item when Cursor sends an id+status-only patch. + const next = { ...target, id: target.id }; + if (typeof raw.content === "string" && raw.content.length > 0) { + next.content = raw.content; + } + if (raw.status !== undefined && raw.status !== null) { + next.status = String(item.status); + } + if (typeof raw.priority === "string" && raw.priority) { + next.priority = raw.priority; + } + return next; + }; + for (let index = 0; index < mappedPatch.length; index++) { + const item = mappedPatch[index]; + const raw = asRecord(rawPatch[index]) ?? {}; + const id = String(item.id); + const byIdMatch = byId.get(id); + if (byIdMatch) { + const merged = overlay(byIdMatch, item, raw); + byId.set(id, merged); + // Keep the content index pointing at the fresh record so later + // content-fallback matches never merge onto a stale copy. + const oldKey = typeof byIdMatch.content === "string" ? byIdMatch.content.trim() : ""; + const newKey = typeof merged.content === "string" ? merged.content.trim() : ""; + if (oldKey && byContent.get(oldKey) === byIdMatch) + byContent.delete(oldKey); + if (newKey && !byContent.has(newKey)) + byContent.set(newKey, merged); + continue; + } + const contentKey = typeof raw.content === "string" && raw.content.trim().length > 0 + ? raw.content.trim() + : ""; + const byContentMatch = contentKey ? byContent.get(contentKey) : undefined; + if (byContentMatch) { + // Same task, different id space (host positional ids vs Cursor ids). + // Keep the prior id so snapshot identity stays stable and order is + // preserved; adopt the patch's status/priority/content. + const merged = overlay(byContentMatch, item, raw); + byId.set(String(byContentMatch.id), merged); + if (contentKey) + byContent.set(contentKey, merged); + consumedPatchIds.add(id); + continue; + } + if (!contentKey) + continue; + byId.set(id, item); + byContent.set(contentKey, item); + appendedPatchIds.add(id); + } + const out = []; + const seen = new Set(); + for (const item of prior) { + const id = typeof item.id === "string" ? item.id : ""; + if (!id || seen.has(id)) + continue; + const next = byId.get(id); + if (next) { + out.push(next); + seen.add(id); + } + } + for (const item of mappedPatch) { + const id = String(item.id); + // Id-matched items are already emitted under the prior id; content-matched + // items were folded into the prior record (not appended under the foreign + // patch id); orphans were skipped outright. Only genuinely new content + // reaches the host list. + if (seen.has(id) || consumedPatchIds.has(id)) + continue; + if (!appendedPatchIds.has(id)) + continue; + out.push(item); + seen.add(id); + } + return out; +} +function unwrapArgs(variantPayload) { + const nested = asRecord(variantPayload.args); + return nested ?? variantPayload; +} +/** + * Decode a Cursor ToolCall oneof into a display tool call we can bridge. + * + * @param priorMirroredTodos Last full todo snapshot mirrored this Run; used to + * expand Cursor `merge: true` updates that omit ToolCallCompleted.success.todos. + */ +export function parseDisplayToolCall(callId, toolCall, priorMirroredTodos) { + if (!toolCall || !callId) + return undefined; + const variant = findToolVariant(toolCall); + if (!variant) + return undefined; + const payload = asRecord(toolCall[variant]); + if (!payload) + return undefined; + const args = unwrapArgs(payload); + if (variant === "mcp_tool_call") { + const name = mcpRealToolName(args); + let mcpArgs = asRecord(args.args) ?? {}; + if (Array.isArray(args.args)) { + mcpArgs = decodeStructEntriesToJson(args.args); + } + return { + callId, + variant, + preferredToolName: name, + args: mcpArgs, + }; + } + // create_plan_tool_call here is display-state mirroring into todowrite. + // Separately, InteractionQuery create_plan_request_query writes a host plan + // file via protocol/create-plan.ts (hostPlansDir) — that write is not an + // execution of this display payload. + if (variant === "update_todos_tool_call" || variant === "create_plan_tool_call") { + const result = asRecord(payload.result); + const success = asRecord(result?.success); + // Cursor's completed frame may materialize proto-default `todos: []` even + // when the real final snapshot is present on args.todos. An empty default + // must not erase that named completion snapshot. A non-empty completed + // payload remains authoritative (especially for merge calls). + const completedTodos = Array.isArray(success?.todos) && success.todos.length > 0 + ? success.todos + : undefined; + const isMerge = variant === "update_todos_tool_call" && args.merge === true; + // OpenCode todowrite replaces the whole list. Prefer the completed payload; + // otherwise a non-merge args list; otherwise merge against the Run snapshot. + const sourceTodos = completedTodos ?? (!isMerge ? args.todos : undefined); + const canMergeFromPrior = sourceTodos === undefined && + isMerge && + Array.isArray(priorMirroredTodos) && + priorMirroredTodos.length > 0; + const todos = canMergeFromPrior + ? applyTodoMerge(priorMirroredTodos, args.todos) + : mapTodos(sourceTodos); + if (variant === "create_plan_tool_call") { + const overview = typeof args.overview === "string" ? args.overview.trim() : ""; + const plan = typeof args.plan === "string" ? args.plan.trim() : ""; + const name = typeof args.name === "string" ? args.name.trim() : ""; + if (overview || plan || name) { + todos.unshift({ + id: "plan", + content: [name && `Plan: ${name}`, overview, plan].filter(Boolean).join("\n").slice(0, 2000), + // The plan file is already written by the time this display mirror + // runs. Leaving it `pending` parks a forever-open row on hosts that + // keep todowrite as the user-visible checklist. + status: "completed", + priority: "high", + }); + } + } + return { + callId, + variant, + preferredToolName: "todowrite", + args: { todos }, + bridgeable: variant === "create_plan_tool_call" + ? todos.length > 0 + : Array.isArray(sourceTodos) || canMergeFromPrior, + }; + } + // Cursor TodoRead is the same display-only channel as TodoWrite (agent.v1 + // read_todos_tool_call). OpenCode todoread takes an empty body; host filters + // are not part of the canonical schema, so drop status_filter / id_filter. + if (variant === "read_todos_tool_call") { + return { + callId, + variant, + preferredToolName: "todoread", + args: {}, + bridgeable: true, + }; + } + if (variant === "edit_tool_call") { + const path = typeof args.path === "string" ? args.path : ""; + const content = typeof args.stream_content === "string" ? args.stream_content : undefined; + return { + callId, + variant, + preferredToolName: "write", + args: { path, content }, + bridgeable: path.length > 0 && content !== undefined, + }; + } + if (variant === "pi_edit_tool_call") { + const path = typeof args.path === "string" ? args.path : ""; + const edits = Array.isArray(args.edits) ? args.edits : []; + const replacement = edits.length === 1 ? asRecord(edits[0]) : undefined; + const oldText = replacement?.old_text; + const newText = replacement?.new_text; + return { + callId, + variant, + preferredToolName: "edit", + args: { path, old_string: oldText, new_string: newText }, + bridgeable: path.length > 0 && + typeof oldText === "string" && oldText.length > 0 && + typeof newText === "string", + }; + } + if (variant === "task_tool_call") { + const subagentType = openCodeSubagentType(args.subagent_type); + const description = typeof args.description === "string" ? args.description : ""; + const prompt = typeof args.prompt === "string" ? args.prompt : ""; + const taskArgs = { + description, + prompt, + subagent_type: subagentType, + }; + if (typeof args.resume === "string" && args.resume) + taskArgs.task_id = args.resume; + return { + callId, + variant, + preferredToolName: "task", + args: taskArgs, + bridgeable: description.length > 0 && prompt.length > 0 && !!subagentType, + }; + } + if (variant === "ask_question_tool_call") { + const title = typeof args.title === "string" ? args.title : ""; + const questions = Array.isArray(args.questions) + ? args.questions.map((q) => { + const qq = asRecord(q) ?? {}; + const prompt = typeof qq.prompt === "string" ? qq.prompt : ""; + const options = Array.isArray(qq.options) + ? qq.options.map((o) => { + const oo = asRecord(o) ?? {}; + return { + label: typeof oo.label === "string" ? oo.label : String(oo.id ?? ""), + description: typeof oo.description === "string" ? oo.description : "", + }; + }) + : []; + return { + question: prompt, + header: (typeof qq.id === "string" && qq.id) || title || "Question", + options, + multiple: qq.allow_multiple === true, + }; + }) + : []; + return { + callId, + variant, + preferredToolName: "question", + args: { questions }, + }; + } + if (variant === "switch_mode_tool_call") { + // Display-path mirror of InteractionQuery #4. Same mapping as switch-mode.ts: + // plan/spec → plan_enter; every other non-empty target → plan_exit. + // Display completions stay non-replayed regardless of mapping. + const target = typeof args.target_mode_id === "string" ? args.target_mode_id : ""; + const mapped = mapSwitchModeTarget(target); + return { + callId, + variant, + preferredToolName: mapped.ok ? mapped.toolName : "plan_exit", + // OpenCode plan_enter / plan_exit both advertise an empty input schema. + args: {}, + bridgeable: mapped.ok, + }; + } + // F11: display-path delete → bash `rm -f` (OpenCode has no delete tool). + if (variant === "delete_tool_call") { + const path = typeof args.path === "string" ? args.path : ""; + return { + callId, + variant, + preferredToolName: "bash", + args: path ? { command: `rm -f -- ${shellQuote(path)}` } : { command: "true" }, + }; + } + const preferred = VARIANT_TO_OPENCODE[variant] ?? variant.replace(/_tool_call$/, ""); + return { + callId, + variant, + preferredToolName: preferred, + args, + }; +} +function shellQuote(s) { + return `'${s.replace(/'/g, `'\\''`)}'`; +} +/** + * Pick an advertised OpenCode tool for a display call, remapping args as needed. + * Returns undefined when nothing compatible is advertised this turn. + */ +export function resolveBridgedOpenCodeToolCall(display, advertised, dialect = OPENCODE_1_TOOL_DIALECT) { + if (isNativeDisplayToolCall(display.variant)) + return undefined; + if (!DISPLAY_STATE_MIRROR_VARIANTS.has(display.variant)) + return undefined; + if (display.bridgeable === false) + return undefined; + const names = new Set([...advertised].filter(Boolean)); + if (names.size === 0) + return undefined; + const candidates = candidateToolNames(display, names); + for (const toolName of candidates) { + if (!names.has(toolName)) + continue; + const mapped = mapCursorArgsToOpencode(toolName, display.args, undefined, dialect); + if (toolName === "todowrite") { + mapped.args = { + todos: mapTodos(display.args.todos ?? mapped.args.todos), + }; + } + if (toolName === "todoread") { + mapped.args = {}; + } + return { + toolName: mapped.toolName, + args: mapped.args, + callId: display.callId, + variant: display.variant, + }; + } + return undefined; +} +function candidateToolNames(display, advertised) { + const out = []; + const add = (n) => { + if (n && !out.includes(n)) + out.push(n); + }; + add(display.preferredToolName); + if (display.variant === "create_plan_tool_call") + add("todowrite"); + if (display.variant === "update_todos_tool_call") + add("todowrite"); + if (display.variant === "read_todos_tool_call") + add("todoread"); + if (display.variant === "ask_question_tool_call") + add("question"); + if (display.variant === "web_search_tool_call") + add("websearch"); + if (display.variant === "fetch_tool_call" || display.variant === "web_fetch_tool_call") { + add("webfetch"); + } + for (const name of advertised) { + if (name.toLowerCase() === display.preferredToolName.toLowerCase()) + add(name); + } + return out; +} +/** Map only Cursor subagent variants with a safe OpenCode identity. */ +function openCodeSubagentType(raw) { + const subtype = asRecord(raw); + if (!subtype) + return undefined; + const custom = asRecord(subtype.custom); + if (typeof custom?.name === "string" && custom.name.length > 0) + return custom.name; + if (subtype.explore != null) + return mapCursorSubagentTypeToOpenCode("explore"); + if (subtype.cursor_guide != null) + return mapCursorSubagentTypeToOpenCode("cursor_guide"); + if (subtype.bash != null) + return mapCursorSubagentTypeToOpenCode("bash"); + if (subtype.shell != null) + return mapCursorSubagentTypeToOpenCode("shell"); + if (subtype.debug != null) + return mapCursorSubagentTypeToOpenCode("debug"); + if (subtype.computer_use != null) + return mapCursorSubagentTypeToOpenCode("computer_use"); + if (subtype.browser_use != null) + return mapCursorSubagentTypeToOpenCode("browser_use"); + if (subtype.media_review != null) + return mapCursorSubagentTypeToOpenCode("media_review"); + if (subtype.watch_video != null) + return mapCursorSubagentTypeToOpenCode("watch_video"); + if (subtype.vm_setup_helper != null) + return mapCursorSubagentTypeToOpenCode("vm_setup_helper"); + if (subtype.unspecified != null) + return mapCursorSubagentTypeToOpenCode("unspecified"); + return undefined; +} +/** Advertised OpenCode tool ids from Cursor McpToolDefinition descriptors. */ +export function advertisedToolNamesFromDescriptors(descriptors) { + const out = []; + for (const d of descriptors) { + const toolName = typeof d.tool_name === "string" ? d.tool_name : undefined; + const provider = typeof d.provider_identifier === "string" ? d.provider_identifier : "opencode"; + if (!toolName) + continue; + if (provider && provider !== "opencode") + out.push(`${provider}_${toolName}`); + else + out.push(toolName); + } + return out; +} +/** Walk a protobuf message and return top-level field numbers present. */ +export function listProtobufFieldNumbers(buf) { + const fields = []; + let i = 0; + while (i < buf.length) { + let key = 0; + let shift = 0; + while (i < buf.length) { + const byte = buf[i++]; + key |= (byte & 0x7f) << shift; + if ((byte & 0x80) === 0) + break; + shift += 7; + if (shift > 35) + return fields; + } + const field = key >>> 3; + const wire = key & 7; + fields.push(field); + if (wire === 0) { + // varint + while (i < buf.length && (buf[i++] & 0x80) !== 0) { /* consume */ } + } + else if (wire === 1) { + i += 8; + } + else if (wire === 2) { + let len = 0; + shift = 0; + while (i < buf.length) { + const byte = buf[i++]; + len |= (byte & 0x7f) << shift; + if ((byte & 0x80) === 0) + break; + shift += 7; + if (shift > 35) + return fields; + } + i += len; + } + else if (wire === 5) { + i += 4; + } + else { + break; + } + if (i > buf.length) + break; + } + return fields; +} +/** + * Extract nested bytes for path of field numbers (each must be wire type 2). + * Returns undefined if any segment is missing. + */ +export function extractProtobufSubmessage(buf, path) { + let cur = buf; + for (const want of path) { + if (!cur) + return undefined; + let i = 0; + let found; + while (i < cur.length) { + let key = 0; + let shift = 0; + while (i < cur.length) { + const byte = cur[i++]; + key |= (byte & 0x7f) << shift; + if ((byte & 0x80) === 0) + break; + shift += 7; + if (shift > 35) + return undefined; + } + const field = key >>> 3; + const wire = key & 7; + if (wire === 0) { + while (i < cur.length && (cur[i++] & 0x80) !== 0) { /* consume */ } + } + else if (wire === 1) { + i += 8; + } + else if (wire === 2) { + let len = 0; + shift = 0; + while (i < cur.length) { + const byte = cur[i++]; + len |= (byte & 0x7f) << shift; + if ((byte & 0x80) === 0) + break; + shift += 7; + if (shift > 35) + return undefined; + } + const slice = cur.subarray(i, i + len); + i += len; + if (field === want) + found = slice; + } + else if (wire === 5) { + i += 4; + } + else { + return undefined; + } + if (i > cur.length) + return undefined; + } + cur = found; + } + return cur; +} +/** + * Pull Cursor's tool_call_id out of a raw ExecServerMessage args variant so we + * can correlate display tool_call_started with the authoritative exec path. + */ +export function extractExecDisplayCallId(execMsg) { + for (const key of [ + "read_args", + "write_args", + "pi_write_args", + "grep_args", + "ls_args", + "delete_args", + "shell_stream_args", + "background_shell_spawn_args", + "mcp_args", + "subagent_args", + ]) { + const args = asRecord(execMsg[key]); + if (!args) + continue; + if (typeof args.tool_call_id === "string" && args.tool_call_id.length > 0) { + return args.tool_call_id; + } + } + return undefined; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tools.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tools.d.ts new file mode 100644 index 00000000..cf1ea291 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tools.d.ts @@ -0,0 +1,356 @@ +import { type CursorExecVariant } from "./exec-variants.js"; +import { type CursorShellOutcome } from "../shell-timeout.js"; +export declare const REQUEST_CONTEXT_RESULT_FIELD = 10; +export type OpencodeToolDef = { + name: string; + description?: string; + inputSchema?: unknown; + /** Original flattened identity when `name` is a Cursor-facing alias. */ + sourceName?: string; +}; +/** Host file-tool argument key. OpenCode 1.x uses `filePath`; 2.0 uses `path`. */ +export type HostFilePathKey = "path" | "filePath"; +/** Host shell tool id. OpenCode 1.x uses `bash`; 2.0 uses `shell`. */ +export type HostShellTool = "bash" | "shell"; +export type HostToolDialect = { + filePathKey: HostFilePathKey; + shellTool: HostShellTool; +}; +export declare const OPENCODE_1_TOOL_DIALECT: HostToolDialect; +export declare const OPENCODE_2_TOOL_DIALECT: HostToolDialect; +/** Prefer `filePath`, then `path` / `file_path` — Cursor and both OpenCode majors. */ +export declare function opencodePathArg(args: Record | undefined): string | undefined; +/** + * Infer the host tool dialect from advertised AI SDK schemas. + * OpenCode 2.0 read/edit/write require `path` and rename bash → `shell`. + */ +export declare function hostToolDialectFromTools(tools: readonly { + name?: string; + inputSchema?: unknown; +}[], defaultDialect?: HostToolDialect): HostToolDialect; +export declare const CUSTOM_WEBSEARCH_TOOL = "custom_websearch"; +export declare const CUSTOM_WEBFETCH_TOOL = "custom_webfetch"; +export declare const CUSTOM_LIST_MCP_RESOURCES_TOOL = "custom_list_mcp_resources"; +export declare const CUSTOM_READ_MCP_RESOURCE_TOOL = "custom_read_mcp_resource"; +/** Cursor-facing alias → exact host tool name accepted by the AI SDK call. */ +export type ToolAliasRegistry = ReadonlyMap; +export type AliasedToolCatalog = { + advertisedTools: OpencodeToolDef[]; + aliases: ToolAliasRegistry; + ambiguous: ReadonlyMap; +}; +export type ToolServerIdentity = { + /** Cursor MCP server id / provider_identifier (e.g. opencode, github). */ + server: string; + /** Bare tool name inside that server (Cursor McpArgs.tool_name). */ + toolName: string; + /** Full OpenCode tool id used for local execution (e.g. github_create_pull_request). */ + opencodeName: string; +}; +/** Match OpenCode's McpCatalog.sanitize for config server ids. */ +export declare function sanitizeMcpServerId(value: string): string; +/** + * Resolve an OpenCode tool id using known MCP server ids from merged config. + * Unknown names remain under the synthetic default server: a flattened tool + * name alone cannot distinguish plugin/custom tools from `_`. + */ +export declare function resolveToolServerIdentity(opencodeName: string, defaultServer?: string, knownMcpServers?: Iterable): ToolServerIdentity; +/** + * Convert opencode's per-turn tool list into Cursor `McpToolDefinition` + * entries for `request_context.tools` (#7) and `AgentRunRequest.mcp_tools`. + * + * Builtins and unknown plugin/custom tools are advertised under the synthetic + * default server (`opencode`). Tools whose prefixes match configured MCP + * servers keep those server ids (`github`, …). Composite `name` is + * `-`; local execution still uses the full OpenCode id + * reconstructed in `mcpRealToolName`. Emit in advertised order — the epoch + * catalog owns canonicalization; sorting here would insert on grow. + */ +export declare function toolsToDescriptors(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable): Array>; +/** + * Give collision-prone web capabilities names Cursor will not confuse with its + * native UI-bound WebSearch/WebFetch interactions. Exact host tools win; a + * flattened MCP suffix is accepted only when it identifies one unique tool. + */ +export declare function buildCustomWebToolAliases(tools: OpencodeToolDef[]): AliasedToolCatalog; +export declare function resolveCustomWebToolAlias(toolName: string, aliases: ToolAliasRegistry | undefined): string; +/** + * Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by + * requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins + * and unknown tools under the synthetic default; configured MCP tools under + * their upstream server id). + */ +export declare function toolsToMcpDescriptors(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable): Array>; +/** + * @deprecated Prefer `buildRequestContext` from `../context/build.js`. + * Kept as a sync tools-only fallback for unit tests that don't need collectors. + */ +export declare function buildLiveRequestContext(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable): Record; +export declare function mapExecServerToToolName(execField: string): string | undefined; +export declare function mapToolNameToExecField(toolName: string): string | undefined; +export type HostSubagentDefinition = { + name: string; + description?: string; +}; +export type HostSubagentCatalog = { + executor?: "task" | "subagent"; + agents: HostSubagentDefinition[]; + /** True when the OpenCode task/subagent tool supplied its complete, permission-filtered catalog. */ + complete: boolean; +}; +/** Extract the current host's permission-filtered Task/Actor recipient catalog. */ +export declare function extractHostSubagentCatalog(tools: OpencodeToolDef[]): HostSubagentCatalog; +/** Resolve a Cursor subtype against the agents the host can spawn this turn. */ +export declare function resolveCursorSubagentType(subagentType: string, catalog?: HostSubagentCatalog): string | undefined; +/** + * Cursor's native Task/subagent protocol uses Cursor-owned subtype names while + * OpenCode-family hosts execute named agents. Map known Cursor built-ins to + * the closest host agent and preserve unknown values so user-defined OpenCode + * agents can still be called by exact name. + */ +export declare function mapCursorSubagentTypeToOpenCode(subagentType: string): string; +/** Resolve a native Cursor subagent request to this turn's executor and catalog. */ +export declare function remapNativeSubagentForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable, catalog?: HostSubagentCatalog): void; +/** + * Complete the private read inside Cursor's legacy edit transaction without + * routing it through OpenCode's user-facing `read` tool. The latter caps every + * result at 50 KB, while Cursor's editor needs the complete file to calculate + * the correlated whole-file `write_args` that follows. + * + * The caller is responsible for proving that `filePath` is the same regular + * file named by an active `edit_tool_call` and is contained by the workspace. + */ +export declare function buildCompleteEditReadMessages(execId: number, sourcePath: string, resultPath?: string): Uint8Array[] | undefined; +/** + * Preserve a legacy Cursor edit's intent when its internal executor follows + * `edit_tool_call` with a whole-file `write_args` request. + * + * The result field intentionally remains `write_result`: that is the response + * Cursor is awaiting even though OpenCode executes the mutation via `edit`. + */ +export declare function remapCorrelatedEditWriteForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable, editPath: string, workspaceRoot?: string): boolean; +/** + * Express a native Cursor write/edit as `apply_patch` when the host swapped the + * edit-tool family out from under us. + * + * OpenCode 1.x removes `edit` and `write` from the catalog for GPT models and + * advertises `apply_patch` in their place — see the module comment in + * `./apply-patch.ts` for the upstream reasoning and citations. Cursor keeps + * using its native write/edit exec channel regardless, so without this the + * request is refused as an unavailable tool and the model loses the ability to + * change files at all. + * + * Keyed purely off the advertised set: this is inert whenever the host offers + * `write`/`edit` normally, and equally inert when it offers neither those nor + * `apply_patch` (the caller's unavailable-tool rejection still applies). + */ +export declare function remapEditToolsForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable, workspaceRoot?: string): void; +export type ParsedExecRequest = { + id: number; + execId: string; + toolName: string; + args: Record; + /** ExecClientMessage result field to reply with (matches the request variant). */ + resultField: string; + /** Typed error to return without asking OpenCode to execute invalid args. */ + localError?: string; + /** Original request values needed by typed Cursor result messages. */ + resultMetadata?: Record; +}; +/** + * A partial-read notice is for Cursor's model, never for a file. Refuse only + * whole-file mutation forms that echo it. A targeted edit or Update File patch + * cannot truncate an unseen tail, and may legitimately edit source that quotes + * this provider's warning text. + */ +export declare function rejectPartialReadMutation(parsed: ParsedExecRequest): void; +export declare function parseExecServerMessage(msg: Record, dialect?: HostToolDialect): ParsedExecRequest | undefined; +/** + * Remap Cursor exec / MCP arg shapes onto OpenCode's Effect Schema keys. + * Without this, OpenCode rejects calls with InvalidArgumentsError + * (e.g. `path` instead of `filePath`, `file_text` instead of `content`). + */ +export declare function mapCursorArgsToOpencode(toolName: string, raw: Record, execVariant?: string, dialect?: HostToolDialect): { + toolName: string; + args: Record; + binaryBytes?: Uint8Array; +}; +/** + * Decode `WriteArgs.file_bytes`. `encoding_hint` names the file's original + * encoding; OpenCode's `write` takes a string, so an unknown encoding label + * falls back to UTF-8 rather than dropping the write. + * + * Returns undefined when the bytes are not text in that encoding. A non-fatal + * decode would replace every undecodable sequence with U+FFFD and hand the host + * a corrupted file that still reports success, so binary content must take the + * byte-preserving path instead (see `isBinaryWriteRequest`). + */ +export declare function decodeWriteBytes(bytes: Uint8Array, encodingHint?: string): string | undefined; +/** + * True when a parsed `write` request carries bytes that are not text. Such a + * request cannot go to OpenCode's `write` tool at all — it takes a string, and + * additionally BOM-splits, diffs, and runs `Format.file()` on what it writes. + */ +export declare function binaryWritePayload(parsed: ParsedExecRequest): { + path: string; + data: Uint8Array; +} | undefined; +/** + * Replace the prompt-derived task title with Cursor's TaskToolCall description + * when the display call is correlated via tool_call_id. SubagentArgs itself has + * no description field, so without this OpenCode shows a cut-off first-five-words + * slice of the prompt. + */ +export declare function preferCorrelatedTaskDescription(parsed: ParsedExecRequest, description: string | undefined): void; +/** + * Map Cursor McpArgs back to the OpenCode tool id. + * Prefers provider_identifier + bare tool_name (github + create_pull_request + * → github_create_pull_request). Builtins under the default server stay bare. + */ +export declare function mcpRealToolName(mcpArgs: Record, defaultServer?: string): string; +/** + * A read target addressed by URI scheme rather than by local filesystem path. + * + * Cursor's LocalReadExecutor only ever reads local files, so this provider + * mirrors its pre-execution validation. An advertised host read tool may also + * accept URLs or internal URI schemes. Resolving those against the workspace + * root mangles the URI and produces a false `file_not_found`, so URI targets + * are forwarded untouched and left to the advertised executor. + * + * Requires a scheme of two or more characters followed by `://`, so Windows + * drive letters (`C:\src`, `C:/src`) stay local paths. + */ +export declare function isUriReadTarget(requested: string): boolean; +/** + * Resolve a read target the way Cursor's LocalReadExecutor does before it + * stats the file: untildify, then resolve a relative path against the workspace + * root (`env.workspace_paths[0]`), else resolve as-is. Kept in lockstep with + * agent utils `resolvePath(path, workspaceRoot)`. + * + * Only meaningful for local paths; check {@link isUriReadTarget} first. + */ +export declare function resolveReadTargetPath(requested: string, workspaceRoot: string): string; +/** + * A typed ReadResult oneof case for a read target that fails Cursor's + * pre-execution validation, or undefined when the path is a readable file the + * tool should actually read. Mirrors LocalReadExecutor exactly: + * - missing path (ENOENT/ENOTDIR) → file_not_found + * - directory → invalid_file "Path is a directory, not a file" + * - socket/fifo/etc (not a file) → invalid_file "Path is neither a file nor a directory" + * EACCES/EPERM (exists but unreadable) and any other stat error return undefined + * so the call proceeds to OpenCode and a genuine permission decision is never + * masked. Never rejects a non-existent *write/edit* target — this is read-only. + */ +export declare function classifyMissingReadTarget(absolutePath: string): Record | undefined; +/** + * Encode a pre-execution read rejection as the exact frames Cursor's client + * emits: the ExecClientMessage carrying the typed ReadResult oneof case, then + * the ACM #5 stream_close for that id. `readResult` is the case object from + * classifyMissingReadTarget (e.g. `{ file_not_found: { path } }`). + */ +export declare function buildReadRejectionMessages(execId: number, readResult: Record): Uint8Array[]; +export type ToolResultInput = { + execId: number; + /** ExecClientMessage result field (from ParsedExecRequest.resultField). */ + resultField: string; + output: string; + error?: string; + executionTimeMs?: number; + /** + * Resolved opencode tool name (read/write/grep/…). Gates read-envelope + * unwrapping on the mcp_result path so non-read MCP output is never + * rewritten. read_result is always a read, so it unwraps regardless. + */ + toolName?: string; + /** Original request fields required by a typed result (background shell). */ + resultMetadata?: Record; + /** Structured shell completion captured by the OpenCode plugin hook. */ + shellOutcome?: CursorShellOutcome; + /** + * Workspace root for result shapes that must name a directory + * (grep_result workspace_results keys, ls_result directory_tree_root). + * Must not fall back to process.cwd() — the OpenCode 2.0 daemon's cwd is + * often $HOME and would re-advertise the wrong folder to the model. + */ + workspaceRoot?: string; +}; +/** + * Build one or more ExecClientMessage frames for a tool result. + * Shell replies are a sequence of ShellStream oneofs under the same id — + * Start → stdout/stderr → exit — then an ACM #5 stream_close so the server + * knows the client finished streaming (CLI always sends this; without it + * shell execs hang on heartbeats forever). + */ +export declare function buildExecClientMessages(input: ToolResultInput): Uint8Array[]; +/** ACM #5 exec_client_control_message { stream_close { id } }. */ +export declare function buildExecStreamClose(execId: number): Uint8Array; +export declare function buildUnsupportedExecDeny(input: { + execId: number; + variant: CursorExecVariant; + reason: string; +}): Uint8Array[]; +/** + * Strip opencode's `read` envelope, leaving raw file content. + * + * OpenCode 1.x (`tool/read.ts`) wraps content in an XML-ish envelope its own + * models are trained on, but Cursor's are not: + * {abs}\nfile\n\n{N}: {line}\n…\n\n{footer}\n + * OpenCode 2 replaced that with `Read file , lines -` plus + * the same `N: ` prefixes and `[Output truncated. Continue reading with offset: N]`. + * Forwarding either envelope verbatim made Cursor's model treat the wrapper as + * literal file content and write tags or line prefixes back into files. + * + * Returns the raw file body (line numbers + footer + `` dropped). + * + * Deliberately exception-safe: if the expected envelope is absent — non-read + * output, already-raw text, or a future opencode format change — it returns the + * input unchanged, so a result is never broken and we never throw mid-turn. + * Callers must still gate `mcp_result` on `toolName === "read"`; this helper + * alone is not a tool-identity check. + */ +export declare function unwrapReadOutput(output: string): string; +/** + * Map OpenCode tool text into the agent.v1 result oneof for each exec variant. + * OpenCode returns free-form text; we wrap it in the minimal success shape the + * server accepts (verified against agent.v1 wire captures). + */ +export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record, shellOutcome?: CursorShellOutcome, workspaceRoot?: string): Record; +export declare function buildToolCallPart(execMsg: ParsedExecRequest, sessionId: string): { + toolCallId: string; + toolName: string; + input: string; +}; +export declare function parseExecIdFromToolCallId(toolCallId: string): { + sessionId: string; + execId: number; +} | undefined; +/** + * Find the exec variant field number from the raw (gunzipped) AgentServerMessage + * payload: peel field #2 (exec_server_message), then return the first + * message-typed field that isn't id(#1)/exec_id(#15)/span_context(#19). + * Returns undefined if there is no exec_server_message or no variant set. + */ +export declare function detectExecVariantField(agentServerPayload: Uint8Array): number | undefined; +/** + * Encode exec #10 request_context_result from a prebuilt RequestContext payload. + */ +export declare function buildRequestContextResult(execId: number, requestContext: Record): Uint8Array; +/** + * Answer Cursor's exec #36 MCP-state probe from the same descriptors advertised + * in RequestContext. OpenCode remains the executor; this only confirms that the + * provider's virtual MCP servers and their tools are available. + */ +export declare function buildMcpStateResult(execId: number, args: Record, requestContext: Record): Uint8Array; +/** + * Total fallback for Cursor's native MCP-resource exec channel (agent.v1 + * fields #17/#18, tasks/plans/fix-cursor-mcp-resource-exec.md). Under Option B + * `list_mcp_resources`/`read_mcp_resource` execute through the ordinary + * field-11 MCP path via the alias rules above, so any 17/18 that still reaches + * this provider is Cursor emitting the native variant unsolicited (its own + * client owns both executors unconditionally — there is no descriptor to + * un-advertise). Cursor's executors are total; mirror that here rather than + * guessing a generic success, which risks an endless heartbeat loop. + */ +export declare function buildListMcpResourcesFallback(execId: number): Uint8Array; +/** See buildListMcpResourcesFallback. Always an `error`, even for `download_path`. */ +export declare function buildReadMcpResourceFallback(execId: number, server: string, uri: string): Uint8Array; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tools.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tools.js new file mode 100644 index 00000000..c6a4f391 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/tools.js @@ -0,0 +1,3286 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { encodeMessage } from "./messages.js"; +import { encodeJsonAsValue, decodeStructEntriesToJson, readAllFields } from "./struct.js"; +import { buildEnv } from "../context/env.js"; +import { ensureOpencodeProjectDir } from "../context/paths.js"; +import { trace, traceRequestContextPaths } from "../debug.js"; +import { cursorExecVariantByRequestName, FORCE_BACKGROUND_STATUS_ERROR, } from "./exec-variants.js"; +import { APPLY_PATCH_TOOL, buildAddFilePatch, buildUpdateFilePatch, planSubstringEdit, } from "./apply-patch.js"; +import { BACKGROUND_SHELL_MARKER, buildBackgroundShellCommand, } from "../shell-timeout.js"; +// Exec variant field number whose reply is the server-initiated request_context +// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a +// field number for every exec variant, so this is also the result field. +export const REQUEST_CONTEXT_RESULT_FIELD = 10; +export const OPENCODE_1_TOOL_DIALECT = { + filePathKey: "filePath", + shellTool: "bash", +}; +export const OPENCODE_2_TOOL_DIALECT = { + filePathKey: "path", + shellTool: "shell", +}; +function jsonSchemaProperties(schema) { + if (!schema || typeof schema !== "object" || Array.isArray(schema)) + return undefined; + const obj = schema; + if (obj.properties && typeof obj.properties === "object" && !Array.isArray(obj.properties)) { + return obj.properties; + } + const nested = obj.jsonSchema ?? obj.parameters ?? obj.schema; + if (nested && typeof nested === "object" && !Array.isArray(nested)) { + const nestedProps = nested.properties; + if (nestedProps && typeof nestedProps === "object" && !Array.isArray(nestedProps)) { + return nestedProps; + } + } + return undefined; +} +/** Prefer `filePath`, then `path` / `file_path` — Cursor and both OpenCode majors. */ +export function opencodePathArg(args) { + if (!args) + return undefined; + return str(args.filePath) ?? str(args.path) ?? str(args.file_path); +} +/** + * Infer the host tool dialect from advertised AI SDK schemas. + * OpenCode 2.0 read/edit/write require `path` and rename bash → `shell`. + */ +export function hostToolDialectFromTools(tools, defaultDialect = OPENCODE_1_TOOL_DIALECT) { + let filePathKey; + for (const name of ["read", "write", "edit"]) { + const tool = tools.find((candidate) => candidate.name === name); + const props = jsonSchemaProperties(tool?.inputSchema); + if (!props) + continue; + if ("path" in props && !("filePath" in props)) { + filePathKey = "path"; + break; + } + if ("filePath" in props) { + filePathKey = "filePath"; + break; + } + } + const names = new Set(tools.map((tool) => tool.name).filter((name) => typeof name === "string")); + const shellTool = names.has("shell") && !names.has("bash") + ? "shell" + : (names.has("bash") ? "bash" : defaultDialect.shellTool); + if (!filePathKey) { + // OpenCode 2 hosts advertise `shell` and `path` together; use that when schemas are opaque. + filePathKey = shellTool === "shell" ? "path" : defaultDialect.filePathKey; + } + return { filePathKey, shellTool }; +} +function assignHostFilePath(args, filePath, dialect) { + delete args.filePath; + delete args.path; + delete args.file_path; + if (filePath) + args[dialect.filePathKey] = filePath; +} +export const CUSTOM_WEBSEARCH_TOOL = "custom_websearch"; +export const CUSTOM_WEBFETCH_TOOL = "custom_webfetch"; +export const CUSTOM_LIST_MCP_RESOURCES_TOOL = "custom_list_mcp_resources"; +export const CUSTOM_READ_MCP_RESOURCE_TOOL = "custom_read_mcp_resource"; +/** Match OpenCode's McpCatalog.sanitize for config server ids. */ +export function sanitizeMcpServerId(value) { + return value.replace(/[^a-zA-Z0-9_-]/g, "_"); +} +/** + * Resolve an OpenCode tool id using known MCP server ids from merged config. + * Unknown names remain under the synthetic default server: a flattened tool + * name alone cannot distinguish plugin/custom tools from `_`. + */ +export function resolveToolServerIdentity(opencodeName, defaultServer = "opencode", knownMcpServers = []) { + if (!opencodeName) { + return { server: defaultServer, toolName: "mcp", opencodeName: "mcp" }; + } + // Longest first handles configured ids where one is a prefix of another + // (e.g. "git" and "git_hub"). OpenCode flattens with the sanitized id. + const servers = [...new Set([...knownMcpServers].map(sanitizeMcpServerId).filter(Boolean))] + .sort((a, b) => b.length - a.length); + for (const server of servers) { + const prefix = `${server}_`; + if (!opencodeName.startsWith(prefix) || opencodeName.length === prefix.length) + continue; + return { + server, + toolName: opencodeName.slice(prefix.length), + opencodeName, + }; + } + return { server: defaultServer, toolName: opencodeName, opencodeName }; +} +/** + * Convert opencode's per-turn tool list into Cursor `McpToolDefinition` + * entries for `request_context.tools` (#7) and `AgentRunRequest.mcp_tools`. + * + * Builtins and unknown plugin/custom tools are advertised under the synthetic + * default server (`opencode`). Tools whose prefixes match configured MCP + * servers keep those server ids (`github`, …). Composite `name` is + * `-`; local execution still uses the full OpenCode id + * reconstructed in `mcpRealToolName`. Emit in advertised order — the epoch + * catalog owns canonicalization; sorting here would insert on grow. + */ +export function toolsToDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) { + // Advertised order is the prefix. The epoch catalog sorts once on first + // freeze and appends later names at the tail; re-sorting here would insert + // a new tool in front of `z` and invalidate the tools bytes. + return tools.map((t) => { + const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers); + const collisionSafeAlias = COLLISION_SAFE_ALIASES.has(t.name); + return { + // Keep the collision-safe public name exact. Prefixing it with the + // synthetic default server weakens the distinction from Cursor-native + // capabilities (web search/fetch, MCP resource list/read) in the + // model-visible catalog. + name: collisionSafeAlias ? t.name : `${id.server}-${id.toolName}`, + description: t.description ?? "", + input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)), + provider_identifier: id.server, + tool_name: t.sourceName ? t.name : id.toolName, + }; + }); +} +function normalizeInputSchema(schema) { + if (schema && typeof schema === "object" && !Array.isArray(schema)) { + return schema; + } + return { type: "object", properties: {} }; +} +// Despite the name, these rules cover any advertised OpenCode tool whose bare +// name collides with a Cursor-native capability, not only web search/fetch — +// see the MCP resource entries below and +// tasks/plans/fix-cursor-mcp-resource-exec.md Phase 0/2. +const WEB_ALIAS_RULES = [ + { + alias: CUSTOM_WEBSEARCH_TOOL, + exact: ["websearch", "web_search"], + suffixes: ["_web_search", "-web_search", "_websearch", "-websearch"], + }, + { + alias: CUSTOM_WEBFETCH_TOOL, + exact: ["webfetch", "web_fetch"], + suffixes: ["_web_fetch", "-web_fetch", "_webfetch", "-webfetch"], + }, + { + alias: CUSTOM_LIST_MCP_RESOURCES_TOOL, + exact: ["list_mcp_resources"], + suffixes: ["_list_mcp_resources", "-list_mcp_resources"], + }, + { + alias: CUSTOM_READ_MCP_RESOURCE_TOOL, + exact: ["read_mcp_resource"], + suffixes: ["_read_mcp_resource", "-read_mcp_resource"], + }, +]; +const COLLISION_SAFE_ALIASES = new Set([ + CUSTOM_WEBSEARCH_TOOL, + CUSTOM_WEBFETCH_TOOL, + CUSTOM_LIST_MCP_RESOURCES_TOOL, + CUSTOM_READ_MCP_RESOURCE_TOOL, +]); +/** + * Give collision-prone web capabilities names Cursor will not confuse with its + * native UI-bound WebSearch/WebFetch interactions. Exact host tools win; a + * flattened MCP suffix is accepted only when it identifies one unique tool. + */ +export function buildCustomWebToolAliases(tools) { + const aliases = new Map(); + const ambiguous = new Map(); + const replacements = new Map(); + for (const rule of WEB_ALIAS_RULES) { + // A host/plugin may already expose the collision-safe public name. Keep it + // authoritative instead of hiding it behind a second mapping. + if (tools.some((tool) => tool.name === rule.alias)) + continue; + const exact = tools.filter((tool) => rule.exact.includes(tool.name.toLowerCase())); + const candidates = exact.length > 0 + ? exact + : tools.filter((tool) => { + const name = tool.name.toLowerCase(); + return rule.suffixes.some((suffix) => name.endsWith(suffix)); + }); + if (candidates.length !== 1) { + if (candidates.length > 1) + ambiguous.set(rule.alias, candidates.map((tool) => tool.name)); + continue; + } + const original = candidates[0].name; + aliases.set(rule.alias, original); + replacements.set(original, rule.alias); + } + return { + advertisedTools: tools.map((tool) => { + const alias = replacements.get(tool.name); + return alias ? { ...tool, name: alias, sourceName: tool.name } : { ...tool }; + }), + aliases, + ambiguous, + }; +} +export function resolveCustomWebToolAlias(toolName, aliases) { + const direct = aliases?.get(toolName); + if (direct) + return direct; + for (const [alias, original] of aliases ?? []) { + if (toolName.endsWith(`_${alias}`)) + return original; + } + return toolName; +} +/** + * Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by + * requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins + * and unknown tools under the synthetic default; configured MCP tools under + * their upstream server id). + */ +export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) { + if (tools.length === 0) + return []; + const byServer = new Map(); + // Walk advertised order: first-seen server, then append tools onto that + // server. Same-set host reorder is resolved by `resolveTurnToolState` + // before encode so this walk stays epoch-stable. + for (const t of tools) { + const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers); + let list = byServer.get(id.server); + if (!list) { + list = []; + byServer.set(id.server, list); + } + list.push({ + tool_name: t.sourceName ? t.name : id.toolName, + description: t.description ?? "", + input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)), + }); + } + return [...byServer.keys()].map((server) => ({ + server_name: server, + server_identifier: server, + tools: byServer.get(server), + })); +} +/** + * @deprecated Prefer `buildRequestContext` from `../context/build.js`. + * Kept as a sync tools-only fallback for unit tests that don't need collectors. + */ +export function buildLiveRequestContext(tools, providerIdentifier = "opencode", knownMcpServers = []) { + const flat = toolsToDescriptors(tools, providerIdentifier, knownMcpServers); + const nested = toolsToMcpDescriptors(tools, providerIdentifier, knownMcpServers); + const cwd = process.cwd(); + const ctx = { + env: buildEnv(cwd), + tools: flat, + mcp_file_system_options: { + enabled: true, + workspace_project_dir: ensureOpencodeProjectDir(cwd), + mcp_descriptors: nested, + }, + mcp_meta_tool_options: { + enabled: true, + mcp_descriptors: nested, + }, + web_search_enabled: false, + web_fetch_enabled: false, + rules_info_complete: true, + env_info_complete: true, + repository_info_complete: true, + mcp_file_system_info_complete: true, + git_status_info_complete: true, + }; + traceRequestContextPaths("buildLiveRequestContext", ctx); + return ctx; +} +// ── Cursor exec-variant → opencode tool name ── +// Native ExecServerMessage variants only (agent.v1). There is no edit_args or +// glob_args on the exec channel — Cursor's EditToolCall is display-only, and +// glob/edit from opencode are advertised as MCP tools and arrive as mcp_args. +// +// Design note (F11): Cursor has native delete / background-shell tools; OpenCode +// does not. This provider intentionally remaps: +// - delete_args → bash `rm -f -- ` +// - background_shell_spawn_args → bash (original command; plugin wraps nohup) +// - shell_args (#2) → bash, same timeout/background path as shell_stream; the +// reply stays ShellResult (not the streaming oneof). +// Permissions still flow through OpenCode's advertised `bash` tool. Soft- +// background / detached children can outlive the OpenCode tool call; leftover +// process cleanup is left to the user / OS. Arg key remapping happens below. +const cursorToolToOpencode = { + read_args: "read", + write_args: "write", + pi_read_args: "read", + pi_bash_args: "bash", + pi_edit_args: "edit", + pi_write_args: "write", + pi_grep_args: "grep", + pi_find_args: "glob", + pi_ls_args: "read", + grep_args: "grep", + ls_args: "read", + delete_args: "bash", + shell_args: "bash", + shell_stream_args: "bash", + background_shell_spawn_args: "bash", + subagent_args: "task", + mcp_args: "mcp", +}; +const opencodeToolToCursor = { + read: "read_args", + write: "write_args", + grep: "grep_args", + bash: "shell_stream_args", + shell: "shell_stream_args", + task: "subagent_args", + mcp: "mcp_args", +}; +/** Cursor-only fields that must not be forwarded as OpenCode tool input. */ +const CURSOR_INTERNAL_KEYS = new Set([ + "tool_call_id", + "toolCallId", + "exec_id", + "span", + "sandbox_policy", + "requested_sandbox_policy", + "smart_mode_approval", + "smart_mode_approval_only", + "skip_approval", + "parsing_result", + "classifier_result", + "hook_approval_requirement", + "conversation_id", + "simple_commands", + "has_input_redirect", + "has_output_redirect", + "is_background", + "timeout_behavior", + "hard_timeout", + "close_stdin", + "output_notification", + "file_output_threshold_bytes", + // WriteArgs #4. A request for WriteSuccess.file_content_after_write, which we + // do not populate; harmless to omit, but it is not tool input. + "return_file_content_after_write", + "ignore", +]); +// WriteArgs #5/#6 are deliberately NOT internal: `file_bytes` carries the file +// content itself whenever Cursor sends it instead of `file_text` (its own +// LocalWriteExecutor prefers bytes when non-empty), and `encoding_hint` is how +// that payload is decoded. The `write` mapping consumes both explicitly. +/** Required content fields where an empty string is meaningful (for example, truncating a file). */ +const PRESERVE_EMPTY_STRING_KEYS = new Set([ + "content", + "file_text", + "fileText", + "stream_content", + // Some advertised edit schemas use a single textual `input` payload. Preserve + // an empty value so the executor reports the real edit error rather than a + // misleading missing-property error. + "input", + "oldString", + "old_string", + "newString", + "new_string", +]); +export function mapExecServerToToolName(execField) { + return cursorToolToOpencode[execField]; +} +export function mapToolNameToExecField(toolName) { + return opencodeToolToCursor[toolName]; +} +const CURSOR_SUBAGENT_TYPE_TO_OPENCODE = { + generalPurpose: "general", + "general-purpose": "general", + general_purpose: "general", + general: "general", + unspecified: "general", + "cursor-guide": "explore", + cursor_guide: "explore", + "best-of-n-runner": "general", + best_of_n_runner: "general", + bash: "general", + shell: "general", + debug: "general", + computer_use: "general", + computerUse: "general", + browser_use: "general", + browserUse: "general", + media_review: "general", + mediaReview: "general", + watch_video: "general", + watchVideo: "general", + vm_setup_helper: "general", + vmSetupHelper: "general", + explore: "explore", + bugbot: "explore", + "security-review": "explore", + security_review: "explore", +}; +const SUBAGENT_CATALOG_MARKER = "Available agent types and the tools they have access to:"; +const SUBAGENT_INLINE_MARKER = "Available subagents:"; +function parseSubagentDescriptionCatalog(description) { + if (!description) + return { found: false, agents: [] }; + const marker = description.indexOf(SUBAGENT_CATALOG_MARKER); + if (marker >= 0) { + const agents = []; + const lines = description.slice(marker + SUBAGENT_CATALOG_MARKER.length).split(/\r?\n/); + let started = false; + for (const line of lines) { + const match = line.match(/^\s*-\s+([^:]+):\s*(.*)$/); + if (!match) { + if (started && line.trim()) + break; + continue; + } + started = true; + const name = match[1].trim().replace(/^`|`$/g, ""); + if (!name) + continue; + const agentDescription = match[2].trim(); + agents.push({ + name, + ...(agentDescription ? { description: agentDescription } : {}), + }); + } + return { found: true, agents }; + } + // OpenCode 2 `subagent` inlines the list after "Available subagents:". + const inlineAt = description.indexOf(SUBAGENT_INLINE_MARKER); + if (inlineAt < 0) + return { found: false, agents: [] }; + const rest = description.slice(inlineAt + SUBAGENT_INLINE_MARKER.length); + const agents = []; + const matches = rest.matchAll(/-\s+([A-Za-z0-9_-]+):\s*/g); + const hits = [...matches]; + for (let i = 0; i < hits.length; i++) { + const hit = hits[i]; + const name = hit[1]; + const start = (hit.index ?? 0) + hit[0].length; + const end = i + 1 < hits.length ? (hits[i + 1].index ?? rest.length) : rest.length; + const agentDescription = rest.slice(start, end).trim(); + agents.push({ + name, + ...(agentDescription ? { description: agentDescription } : {}), + }); + } + return { found: agents.length > 0, agents }; +} +function subagentTypeEnumValues(schema) { + const out = new Set(); + const seen = new Set(); + const visit = (value, propertyName) => { + if (!value || typeof value !== "object") + return; + if (seen.has(value)) + return; + seen.add(value); + if (Array.isArray(value)) { + for (const item of value) + visit(item, propertyName); + return; + } + const record = value; + if ((propertyName === "subagent_type" || propertyName === "agent") + && Array.isArray(record.enum)) { + for (const item of record.enum) { + if (typeof item === "string" && item) + out.add(item); + } + } + for (const [key, child] of Object.entries(record)) + visit(child, key); + }; + visit(schema); + return [...out]; +} +/** Extract the current host's permission-filtered Task/Actor recipient catalog. */ +export function extractHostSubagentCatalog(tools) { + const executorTool = tools.find((tool) => tool.name === "task") + ?? tools.find((tool) => tool.name === "subagent"); + if (!executorTool) + return { agents: [], complete: true }; + const described = parseSubagentDescriptionCatalog(executorTool.description); + const enumNames = subagentTypeEnumValues(executorTool.inputSchema); + const descriptions = new Map(described.agents.map((agent) => [agent.name, agent.description])); + // A structured subagent_type/agent enum is stricter than prose, so it is authoritative. + const names = new Set(enumNames.length > 0 ? enumNames : described.agents.map((agent) => agent.name)); + return { + executor: executorTool.name === "subagent" ? "subagent" : "task", + agents: [...names].map((name) => ({ + name, + ...(descriptions.get(name) ? { description: descriptions.get(name) } : {}), + })), + complete: enumNames.length > 0 || described.found, + }; +} +const GENERIC_CURSOR_SUBAGENT_TYPES = new Set([ + "generalPurpose", + "general-purpose", + "general_purpose", + "general", + "unspecified", +]); +function cursorSubagentCandidates(subagentType) { + if (GENERIC_CURSOR_SUBAGENT_TYPES.has(subagentType)) + return ["general"]; + if (subagentType === "cursor-guide" || subagentType === "cursor_guide") { + return ["scout", "explore", "general"]; + } + if (subagentType === "explore" || + subagentType === "bugbot" || + subagentType === "security-review" || + subagentType === "security_review") { + return ["explore", "general"]; + } + return ["general"]; +} +/** Resolve a Cursor subtype against the agents the host can spawn this turn. */ +export function resolveCursorSubagentType(subagentType, catalog) { + const available = new Set(catalog?.agents.map((agent) => agent.name) ?? []); + // Explicit host/custom names win. `unspecified` and general aliases are + // intentionally excluded so they always select the host's generic agent. + if (!GENERIC_CURSOR_SUBAGENT_TYPES.has(subagentType) && available.has(subagentType)) { + return subagentType; + } + const candidates = cursorSubagentCandidates(subagentType); + for (const candidate of candidates) { + if (available.has(candidate)) + return candidate; + } + if (catalog?.complete) + return undefined; + return candidates[0]; +} +/** + * Cursor's native Task/subagent protocol uses Cursor-owned subtype names while + * OpenCode-family hosts execute named agents. Map known Cursor built-ins to + * the closest host agent and preserve unknown values so user-defined OpenCode + * agents can still be called by exact name. + */ +export function mapCursorSubagentTypeToOpenCode(subagentType) { + return CURSOR_SUBAGENT_TYPE_TO_OPENCODE[subagentType] ?? subagentType; +} +/** Resolve a native Cursor subagent request to this turn's executor and catalog. */ +export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catalog) { + if (parsed.resultField !== "subagent_result" || parsed.toolName !== "task") + return; + const advertised = new Set(advertisedToolNames); + const executor = advertised.has("task") + ? "task" + : advertised.has("subagent") + ? "subagent" + : undefined; + if (!executor) + return; + const description = str(parsed.args.description) ?? ""; + const prompt = str(parsed.args.prompt) ?? ""; + const cursorSubagentType = str(parsed.resultMetadata?.cursor_subagent_type) ?? + str(parsed.args.subagent_type) ?? + str(parsed.args.agent) ?? + ""; + const subagentType = resolveCursorSubagentType(cursorSubagentType, catalog); + if (!subagentType) { + const available = catalog?.agents.map((agent) => agent.name).join(", ") || "none"; + parsed.localError = + `Cursor subagent '${cursorSubagentType}' has no compatible host agent. ` + + `Available subagents: ${available}.`; + return; + } + const resumeAgentId = str(parsed.args.task_id) ?? str(parsed.args.sessionID); + parsed.toolName = executor; + parsed.args = executor === "subagent" + ? { + agent: subagentType, + description, + prompt, + ...(resumeAgentId ? { sessionID: resumeAgentId } : {}), + ...(parsed.args.background === true ? { background: true } : {}), + } + : { + description, + prompt, + subagent_type: subagentType, + ...(resumeAgentId ? { task_id: resumeAgentId } : {}), + ...(parsed.args.background === true ? { background: true } : {}), + }; +} +/** Matches Cursor's own pre-write read threshold (`local-exec` 52428800). */ +const MAX_EDIT_SOURCE_BYTES = 50 * 1024 * 1024; +/** + * Complete the private read inside Cursor's legacy edit transaction without + * routing it through OpenCode's user-facing `read` tool. The latter caps every + * result at 50 KB, while Cursor's editor needs the complete file to calculate + * the correlated whole-file `write_args` that follows. + * + * The caller is responsible for proving that `filePath` is the same regular + * file named by an active `edit_tool_call` and is contained by the workspace. + */ +export function buildCompleteEditReadMessages(execId, sourcePath, resultPath = sourcePath) { + try { + const stat = fs.statSync(sourcePath); + if (!stat.isFile() || stat.size > MAX_EDIT_SOURCE_BYTES) + return undefined; + const content = fs.readFileSync(sourcePath, "utf8"); + return [ + encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + read_result: { + success: { + path: resultPath, + content, + total_lines: countLines(content), + file_size: stat.size, + truncated: false, + range_applied: false, + }, + }, + }, + }), + buildExecStreamClose(execId), + ]; + } + catch { + return undefined; + } +} +/** Return the number of non-overlapping occurrences, stopping after ambiguity. */ +function replacementOccurrenceCount(source, needle) { + if (!needle) + return 0; + const first = source.indexOf(needle); + if (first === -1) + return 0; + return source.indexOf(needle, first + needle.length) === -1 ? 1 : 2; +} +function previousLineStart(source, start) { + if (start <= 0) + return 0; + const before = source[start - 1] === "\n" ? start - 2 : start - 1; + return source.lastIndexOf("\n", before) + 1; +} +function followingLineEnd(source, end) { + if (end >= source.length) + return source.length; + const newline = source.indexOf("\n", end); + return newline === -1 ? source.length : newline + 1; +} +/** + * Collapse a whole-file replacement into one unique, line-bounded substring + * edit. Cursor's legacy edit executor computes a complete new file internally; + * OpenCode's `edit` tool instead wants old/new substrings. + */ +function planWholeFileEdit(source, target) { + if (!source || source === target) + return undefined; + let prefix = 0; + const shared = Math.min(source.length, target.length); + while (prefix < shared && source[prefix] === target[prefix]) + prefix++; + let suffix = 0; + while (suffix < source.length - prefix + && suffix < target.length - prefix + && source[source.length - suffix - 1] === target[target.length - suffix - 1]) + suffix++; + const sourceChangeEnd = source.length - suffix; + const targetChangeEnd = target.length - suffix; + let start = prefix === 0 ? 0 : source.lastIndexOf("\n", prefix - 1) + 1; + let end = sourceChangeEnd >= source.length + ? source.length + : followingLineEnd(source, sourceChangeEnd); + // A pure insertion can initially select no source text. Include an adjacent + // line because OpenCode edit deliberately rejects an empty oldString. + if (start === end) { + if (start > 0) + start = previousLineStart(source, start); + else + end = followingLineEnd(source, end); + } + const replacement = () => ({ + oldString: source.slice(start, end), + newString: source.slice(start, prefix) + + target.slice(prefix, targetChangeEnd) + + source.slice(sourceChangeEnd, end), + }); + let result = replacement(); + while (replacementOccurrenceCount(source, result.oldString) !== 1) { + const priorStart = start; + const priorEnd = end; + if (start > 0) + start = previousLineStart(source, start); + if (end < source.length) + end = followingLineEnd(source, end); + if (start === priorStart && end === priorEnd) + return undefined; + result = replacement(); + } + return result; +} +/** + * Preserve a legacy Cursor edit's intent when its internal executor follows + * `edit_tool_call` with a whole-file `write_args` request. + * + * The result field intentionally remains `write_result`: that is the response + * Cursor is awaiting even though OpenCode executes the mutation via `edit`. + */ +export function remapCorrelatedEditWriteForCatalog(parsed, advertisedToolNames, editPath, workspaceRoot) { + if (parsed.resultField !== "write_result" || parsed.toolName !== "write") + return false; + const advertised = new Set(advertisedToolNames); + if (!advertised.has("edit") && !advertised.has(APPLY_PATCH_TOOL)) + return false; + const filePath = opencodePathArg(parsed.args); + const content = stringValue(parsed.args.content); + if (!filePath || content === undefined || !editPath) + return false; + const root = workspaceRoot ?? process.cwd(); + const absolute = path.resolve(root, filePath); + if (absolute !== path.resolve(root, editPath)) + return false; + let source; + try { + const stat = fs.statSync(absolute); + if (!stat.isFile() || stat.size > MAX_EDIT_SOURCE_BYTES) + return false; + if (Buffer.byteLength(content, "utf8") > MAX_EDIT_SOURCE_BYTES) + return false; + source = fs.readFileSync(absolute, "utf8"); + } + catch { + // Missing targets are creations and must stay writes. Other read failures + // also fall back to the original host permission path without guessing. + return false; + } + const replacement = planWholeFileEdit(source, content); + if (!replacement) + return false; + parsed.toolName = "edit"; + const filePathKey = typeof parsed.args.path === "string" && typeof parsed.args.filePath !== "string" + ? "path" + : "filePath"; + parsed.args = { + [filePathKey]: filePath, + oldString: replacement.oldString, + newString: replacement.newString, + }; + parsed.resultMetadata = { ...parsed.resultMetadata, path: filePath }; + return true; +} +/** + * Express a native Cursor write/edit as `apply_patch` when the host swapped the + * edit-tool family out from under us. + * + * OpenCode 1.x removes `edit` and `write` from the catalog for GPT models and + * advertises `apply_patch` in their place — see the module comment in + * `./apply-patch.ts` for the upstream reasoning and citations. Cursor keeps + * using its native write/edit exec channel regardless, so without this the + * request is refused as an unavailable tool and the model loses the ability to + * change files at all. + * + * Keyed purely off the advertised set: this is inert whenever the host offers + * `write`/`edit` normally, and equally inert when it offers neither those nor + * `apply_patch` (the caller's unavailable-tool rejection still applies). + */ +export function remapEditToolsForCatalog(parsed, advertisedToolNames, workspaceRoot) { + if (parsed.toolName !== "write" && parsed.toolName !== "edit") + return; + // `apply_patch` envelopes are text. A binary write has no patch form at all, + // so it must stay a write and reach the byte-preserving path — converting it + // here would drop the bytes and report a patch-translation failure instead of + // the image Cursor asked to save. + if (binaryWritePayload(parsed)) + return; + const advertised = new Set(advertisedToolNames); + if (advertised.has(parsed.toolName) || !advertised.has(APPLY_PATCH_TOOL)) + return; + const requested = parsed.toolName; + const filePath = opencodePathArg(parsed.args); + const refuse = (reason) => { + parsed.toolName = APPLY_PATCH_TOOL; + parsed.args = {}; + parsed.localError = + `Cursor ${requested} request cannot be expressed as an apply_patch call: ${reason}. ` + + "The host advertises `apply_patch` instead of `edit`/`write` for this model."; + }; + if (!filePath) { + refuse("no target path was provided"); + return; + } + let patchText; + if (requested === "write") { + const content = stringValue(parsed.args.content); + if (content === undefined) { + refuse("no file content was provided"); + return; + } + // `*** Add File:` overwrites an existing target, so a whole-file write maps + // directly without reading the current contents or diffing. + patchText = buildAddFilePatch(filePath, content); + } + else { + const oldString = stringValue(parsed.args.oldString); + const newString = stringValue(parsed.args.newString); + if (oldString === undefined || newString === undefined) { + refuse("the replacement is missing its old or new text"); + return; + } + // apply_patch matches whole lines, so a substring edit has to be widened to + // the lines it touches — which requires the file the model is editing. + const absolute = path.resolve(workspaceRoot ?? process.cwd(), filePath); + let source; + try { + // Bounded because this read is synchronous on the Run pump. Cursor's own + // executor uses the same 50 MB threshold before reading a file it is + // about to write. + const size = fs.statSync(absolute).size; + if (size > MAX_EDIT_SOURCE_BYTES) { + refuse(`the target file is ${Math.round(size / 1024 / 1024)} MB, too large to patch`); + return; + } + source = fs.readFileSync(absolute, "utf8"); + } + catch (e) { + refuse(`the target file could not be read (${e.message})`); + return; + } + const plan = planSubstringEdit(source, oldString, newString, parsed.args.replaceAll === true); + if (!plan.ok) { + refuse(plan.reason); + return; + } + patchText = buildUpdateFilePatch(filePath, plan.chunks); + } + parsed.toolName = APPLY_PATCH_TOOL; + parsed.args = { patchText }; + // apply_patch's output does not carry opencode `write`'s tag, so keep + // the requested path for the typed Cursor result. + parsed.resultMetadata = { ...parsed.resultMetadata, path: filePath }; +} +function readRequestResultMetadata(raw, mappedArgs) { + const requestedPath = str(raw.path) + ?? str(raw.filePath) + ?? str(raw.file_path) + ?? opencodePathArg(mappedArgs) + ?? ""; + return { + path: requestedPath, + ...(typeof mappedArgs.offset === "number" ? { offset: mappedArgs.offset } : {}), + ...(typeof mappedArgs.limit === "number" ? { limit: mappedArgs.limit } : {}), + }; +} +/** + * Cursor's native write executor echoes `WriteArgs.path` on `WriteSuccess.path`. + * Large GetMcpTools / GetDynamicTools catalogs spill through `write_args` into + * `agent-tools/.txt` and then tell the model `filePath` from that success + * path. Host write output is free-form prose (often no `` tag), so the + * request path is the only host-neutral identity we can report. + */ +function writeRequestResultMetadata(raw, mappedArgs) { + const requestedPath = str(raw.path) + ?? str(raw.filePath) + ?? str(raw.file_path) + ?? opencodePathArg(mappedArgs); + return requestedPath ? { path: requestedPath } : undefined; +} +/** + * A partial-read notice is for Cursor's model, never for a file. Refuse only + * whole-file mutation forms that echo it. A targeted edit or Update File patch + * cannot truncate an unseen tail, and may legitimately edit source that quotes + * this provider's warning text. + */ +export function rejectPartialReadMutation(parsed) { + if (parsed.localError) + return; + let content; + if (parsed.toolName === "write") { + content = parsed.args.content; + } + else if (parsed.toolName === APPLY_PATCH_TOOL) { + const patchText = parsed.args.patchText; + if (typeof patchText !== "string" || !patchText.includes("*** Add File:")) + return; + content = patchText; + } + else { + return; + } + if (typeof content !== "string" + || !content.includes("[Partial read:") + || !content.includes("It is NOT the complete file.")) + return; + const filePath = opencodePathArg(parsed.args) ?? "the target file"; + const nextOffset = /Continue with offset=(\d+)/.exec(content)?.[1]; + const longLine = content.includes("Use a byte-preserving read method"); + parsed.localError = + `NO FILE CHANGE WAS MADE. Refusing a whole-file mutation of ${JSON.stringify(filePath)} ` + + "because it contains the provider's partial-read notice. Do not retry the same mutation. " + + (nextOffset + ? `Read the file from offset=${nextOffset}, then use a targeted edit or Update File patch.` + : longLine + ? "Inspect the full line with a byte-preserving read method, then use a targeted edit or Update File patch." + : "Read the remaining file ranges, then use a targeted edit or Update File patch."); +} +export function parseExecServerMessage(msg, dialect = OPENCODE_1_TOOL_DIALECT) { + const id = msg.id; + if (id === undefined) + return undefined; + // Find which args variant is set + const execVariant = findOneOfVariant(msg, [ + "read_args", "write_args", + "pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args", + "pi_grep_args", "pi_find_args", "pi_ls_args", + "grep_args", "ls_args", + "delete_args", "shell_args", "shell_stream_args", "background_shell_spawn_args", "mcp_args", + "subagent_args", + ]); + if (!execVariant) + return undefined; + // Use the complete canonical request/result table. In particular, Pi request + // fields #45..#51 pair with result fields #46..#52 rather than matching ids. + const resultField = cursorExecVariantByRequestName(execVariant)?.resultName; + if (!resultField) + return undefined; + const execId = msg.exec_id ?? ""; + // F11: Cursor native background shell → OpenCode bash. Keep the wrapper + // self-contained so direct provider / hosts without shell.env still detach + // and return a PID. The classic plugin replaces this with its display-safe + // env or wrapper-file path before execution. + if (execVariant === "background_shell_spawn_args") { + const raw = msg.background_shell_spawn_args ?? {}; + const command = str(raw.command); + const workingDirectory = str(raw.working_directory) ?? ""; + const args = {}; + if (command) + args.command = buildBackgroundShellCommand(command); + if (workingDirectory) + args.workdir = workingDirectory; + return { + id, + execId, + toolName: dialect.shellTool, + args, + resultField, + resultMetadata: { + background_shell_spawn: true, + command: command ?? "", + working_directory: workingDirectory, + }, + localError: raw.enable_write_shell_stdin_tool === true + ? "Interactive background shells are not available through OpenCode's bash tool." + : command + ? undefined + : "Cursor background shell request is missing a command.", + }; + } + if (execVariant === "subagent_args") { + const raw = msg.subagent_args ?? {}; + const prompt = str(raw.prompt); + const cursorSubagentType = str(raw.subagent_type); + const subagentType = cursorSubagentType + ? mapCursorSubagentTypeToOpenCode(cursorSubagentType) + : undefined; + const args = { + description: describeSubagentTask(prompt, subagentType), + prompt: prompt ?? "", + subagent_type: subagentType ?? "", + }; + const resumeAgentId = str(raw.resume_agent_id); + if (resumeAgentId) + args.task_id = resumeAgentId; + // protobufjs materializes an absent proto3 optional bool as false in this + // reflection schema. OpenCode's foreground default is already false, so + // only forward the meaningful opt-in value. + if (raw.run_in_background === true) + args.background = true; + return { + id, + execId, + toolName: "task", + args, + resultField, + resultMetadata: cursorSubagentType + ? { cursor_subagent_type: cursorSubagentType } + : undefined, + localError: prompt && cursorSubagentType + ? undefined + : "Cursor subagent request is missing a required prompt or subagent type.", + }; + } + if (execVariant === "mcp_args") { + // An MCP call to one of the tools we advertised. Resolve the real opencode + // tool name (Cursor's model may have shortened "opencode-read" → "read") and + // decode the argument map back into JSON for opencode to execute. + const m = msg.mcp_args ?? {}; + const rawArgs = decodeMcpArgs(m.args); + const mapped = mapCursorArgsToOpencode(mcpRealToolName(m), rawArgs, "mcp_args", dialect); + return { + id, + execId, + toolName: mapped.toolName, + args: mapped.args, + resultField, + ...(mapped.toolName === "read" + ? { resultMetadata: readRequestResultMetadata(rawArgs, mapped.args) } + : {}), + }; + } + if (execVariant === "pi_edit_args") { + const raw = msg.pi_edit_args ?? {}; + const edits = Array.isArray(raw.edits) ? raw.edits : []; + const replacement = edits.length === 1 && edits[0] && typeof edits[0] === "object" + ? edits[0] + : undefined; + const path = str(raw.path); + const oldString = replacement ? stringValue(replacement.old_text) : undefined; + const newString = replacement ? stringValue(replacement.new_text) : undefined; + const args = {}; + assignHostFilePath(args, path, dialect); + if (oldString !== undefined) + args.oldString = oldString; + if (newString !== undefined) + args.newString = newString; + return { + id, + execId, + toolName: "edit", + args, + resultField, + localError: path && oldString && newString !== undefined + ? undefined + : "Cursor Pi edit cannot be represented safely: expected one non-empty replacement.", + }; + } + const toolName = cursorToolToOpencode[execVariant]; + if (!toolName) + return undefined; + const mapped = mapCursorArgsToOpencode(toolName, msg[execVariant] ?? {}, execVariant, dialect); + const rawArgs = msg[execVariant] ?? {}; + const resultMetadata = execVariant === "shell_stream_args" || execVariant === "shell_args" + ? shellStreamResultMetadata(rawArgs) + : execVariant === "read_args" || execVariant === "pi_read_args" + ? readRequestResultMetadata(rawArgs, mapped.args) + : execVariant === "write_args" || execVariant === "pi_write_args" + ? writeRequestResultMetadata(rawArgs, mapped.args) + : execVariant === "grep_args" + ? grepRequestResultMetadata(rawArgs) + : undefined; + if (resultMetadata + && resultMetadata.timeout_behavior !== 2 + && typeof resultMetadata.timeout_ms === "number") { + // Cursor's protobuf default (timeout=0) means 30 seconds for an ordinary + // foreground shell. OpenCode instead treats zero literally, so pass the + // effective native value rather than the raw protobuf default. + mapped.args.timeout = resultMetadata.timeout_ms; + } + return { + id, + execId, + toolName: mapped.toolName, + args: mapped.args, + resultField, + ...(resultMetadata || mapped.binaryBytes + ? { + resultMetadata: { + ...resultMetadata, + ...(mapped.binaryBytes + ? { + binaryWriteBytes: mapped.binaryBytes, + path: str(rawArgs.path) ?? str(rawArgs.file_path) ?? "", + } + : {}), + }, + } + : {}), + }; +} +function shellStreamResultMetadata(raw) { + const timeout = num(raw.timeout) ?? 0; + const timeoutBehavior = num(raw.timeout_behavior) ?? 0; + const hardTimeout = num(raw.hard_timeout); + // Cursor CLI: a nonzero timeout is used verbatim. A zero foreground timeout + // defaults to 30s; zero with background/hard-timeout semantics means an + // immediate soft handoff governed by the separate hard deadline. + const effectiveTimeout = timeout !== 0 + ? timeout + : (timeoutBehavior === 2 || (hardTimeout !== undefined && hardTimeout > 0) ? 0 : 30_000); + return { + shell_stream: true, + command: str(raw.command) ?? "", + working_directory: str(raw.working_directory) ?? "", + timeout_ms: effectiveTimeout, + timeout_behavior: timeoutBehavior, + ...(hardTimeout !== undefined && hardTimeout > 0 ? { hard_timeout_ms: hardTimeout } : {}), + }; +} +/** + * Remap Cursor exec / MCP arg shapes onto OpenCode's Effect Schema keys. + * Without this, OpenCode rejects calls with InvalidArgumentsError + * (e.g. `path` instead of `filePath`, `file_text` instead of `content`). + */ +export function mapCursorArgsToOpencode(toolName, raw, execVariant, dialect = OPENCODE_1_TOOL_DIALECT) { + const cleaned = {}; + for (const [k, v] of Object.entries(raw)) { + if (v === undefined || v === null) + continue; + if (CURSOR_INTERNAL_KEYS.has(k)) + continue; + // Drop empty strings from optional protobuf defaults, but retain required + // content fields where empty means a valid destructive edit/write. + if (typeof v === "string" && v.length === 0 && !PRESERVE_EMPTY_STRING_KEYS.has(k)) + continue; + cleaned[k] = v; + } + // Native ls_args → OpenCode read (directory listing via read). + if (execVariant === "ls_args") { + const filePath = str(cleaned.path) ?? str(cleaned.filePath); + return { toolName: "read", args: filePath ? { [dialect.filePathKey]: filePath } : {} }; + } + // F11: Cursor native delete_args → OpenCode bash. No delete builtin exists, so + // we emulate with `rm -f -- ` (shell-quoted). Same permission boundary + // as any other bash tool call. + if (execVariant === "delete_args") { + const target = str(cleaned.path) ?? str(cleaned.filePath); + return { + toolName: dialect.shellTool, + args: target ? { command: `rm -f -- ${shellQuote(target)}` } : { command: "true" }, + }; + } + switch (toolName) { + case "read": { + const args = {}; + const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path); + assignHostFilePath(args, filePath, dialect); + // Cursor often sends offset=0/limit=0 as protobuf defaults. OpenCode's + // read treats limit=0 as "read zero lines" (empty content) — omit zeros. + const offset = num(cleaned.offset); + if (offset !== undefined && offset > 0) + args.offset = offset; + const limit = num(cleaned.limit); + if (limit !== undefined && limit > 0) + args.limit = limit; + return { toolName: "read", args }; + } + case "write": { + const args = {}; + const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path); + assignHostFilePath(args, filePath, dialect); + // Cursor's LocalWriteExecutor prefers `file_bytes` whenever it is + // non-empty and only falls back to `file_text`; mirror that order so a + // byte-encoded write is not silently seen as empty content. + const bytes = bytesValue(cleaned.file_bytes) ?? bytesValue(cleaned.fileBytes); + let content; + if (bytes !== undefined) { + content = decodeWriteBytes(bytes, str(cleaned.encoding_hint) ?? str(cleaned.encodingHint)); + if (content === undefined) { + // Binary. Surface the raw bytes to the caller instead of a lossy + // decode; `binaryWritePayload` picks them up after parsing. + return { toolName: "write", args, binaryBytes: bytes }; + } + } + else { + content = stringValue(cleaned.content) + ?? stringValue(cleaned.file_text) + ?? stringValue(cleaned.fileText) + ?? stringValue(cleaned.contents) + ?? stringValue(cleaned.text); + } + if (content !== undefined) + args.content = content; + return { toolName: "write", args }; + } + case "edit": { + // Preserve an opaque textual edit payload when the advertised executor + // accepts that generic shape; rebuilding only OpenCode's targeted fields + // would silently turn a valid call into `{}`. + if (typeof cleaned.input === "string") { + return { toolName: "edit", args: { input: cleaned.input } }; + } + const args = {}; + const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path); + assignHostFilePath(args, filePath, dialect); + const oldString = stringValue(cleaned.oldString) ?? stringValue(cleaned.old_string); + if (oldString !== undefined) + args.oldString = oldString; + const newString = stringValue(cleaned.newString) ?? stringValue(cleaned.new_string); + if (newString !== undefined) + args.newString = newString; + if (typeof cleaned.replaceAll === "boolean") + args.replaceAll = cleaned.replaceAll; + return { toolName: "edit", args }; + } + case "bash": + case "shell": { + const args = {}; + const command = str(cleaned.command); + if (command) + args.command = command; + const workdir = str(cleaned.workdir) ?? str(cleaned.working_directory); + if (workdir) + args.workdir = workdir; + const timeout = num(cleaned.timeout); + if (timeout !== undefined) + args.timeout = timeout; + return { toolName: dialect.shellTool, args }; + } + case "grep": { + const pattern = str(cleaned.pattern); + const path = str(cleaned.path); + const include = str(cleaned.include) ?? str(cleaned.glob); + // Cursor's native Grep often arrives with an empty pattern + a glob + // (e.g. "**/*") when the model is really listing files. OpenCode's grep + // requires a non-empty string pattern — remap to glob instead of + // forwarding a call that will fail and loop. + if (!pattern) { + const args = { pattern: include ?? "**/*" }; + if (path) + args.path = path; + return { toolName: "glob", args }; + } + const args = { pattern }; + if (path) + args.path = path; + if (include) + args.include = include; + return { toolName: "grep", args }; + } + case "glob": { + const args = {}; + const pattern = str(cleaned.pattern) ?? str(cleaned.glob_pattern) ?? str(cleaned.globPattern); + if (pattern) + args.pattern = pattern; + const path = str(cleaned.path) ?? str(cleaned.target_directory) ?? str(cleaned.targetDirectory); + if (path) + args.path = path; + return { toolName: "glob", args }; + } + default: + return { toolName, args: cleaned }; + } +} +function str(v) { + return typeof v === "string" && v.length > 0 ? v : undefined; +} +function stringValue(v) { + return typeof v === "string" ? v : undefined; +} +/** A non-empty protobuf `bytes` field, or undefined when absent/empty. */ +function bytesValue(v) { + if (v instanceof Uint8Array) + return v.length > 0 ? v : undefined; + if (Array.isArray(v) && v.every((b) => typeof b === "number")) { + return v.length > 0 ? Uint8Array.from(v) : undefined; + } + return undefined; +} +/** UTF-16/32 embed NUL bytes in ordinary text; 8-bit encodings do not. */ +const WIDE_TEXT_ENCODING = /^utf-?(16|32)/; +/** + * Decode `WriteArgs.file_bytes`. `encoding_hint` names the file's original + * encoding; OpenCode's `write` takes a string, so an unknown encoding label + * falls back to UTF-8 rather than dropping the write. + * + * Returns undefined when the bytes are not text in that encoding. A non-fatal + * decode would replace every undecodable sequence with U+FFFD and hand the host + * a corrupted file that still reports success, so binary content must take the + * byte-preserving path instead (see `isBinaryWriteRequest`). + */ +export function decodeWriteBytes(bytes, encodingHint) { + const encoding = encodingHint?.trim().toLowerCase().replace(/[_ ]/g, "-"); + let decoder; + let label = "utf-8"; + if (encoding && encoding !== "utf-8" && encoding !== "utf8") { + try { + decoder = new TextDecoder(encoding, { fatal: true }); + label = encoding; + } + catch { + trace(`write: unsupported encoding_hint ${JSON.stringify(encodingHint)} — decoding as utf-8`); + } + } + if (!decoder) + decoder = new TextDecoder("utf-8", { fatal: true }); + // Single-byte encodings decode any byte sequence without error, so a decode + // that cannot fail is not on its own evidence of text. + if (!WIDE_TEXT_ENCODING.test(label) && bytes.includes(0)) + return undefined; + try { + return decoder.decode(bytes); + } + catch { + return undefined; + } +} +/** + * True when a parsed `write` request carries bytes that are not text. Such a + * request cannot go to OpenCode's `write` tool at all — it takes a string, and + * additionally BOM-splits, diffs, and runs `Format.file()` on what it writes. + */ +export function binaryWritePayload(parsed) { + if (parsed.toolName !== "write") + return undefined; + const data = parsed.resultMetadata?.binaryWriteBytes; + if (!(data instanceof Uint8Array)) + return undefined; + return { path: str(parsed.resultMetadata?.path) ?? "", data }; +} +function num(v) { + if (typeof v === "number" && Number.isFinite(v)) + return v; + if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) + return Number(v); + return undefined; +} +/** + * Fallback OpenCode task title when Cursor's SubagentArgs has no description. + * Prefer {@link preferCorrelatedTaskDescription} when a TaskToolCall is available — + * that carries Cursor's real 3–5 word title. This only slices the prompt. + */ +function describeSubagentTask(prompt, subagentType) { + const words = prompt?.replace(/\s+/g, " ").trim().split(" ").filter(Boolean).slice(0, 5); + if (words?.length) + return words.join(" "); + return `${subagentType || "Delegated"} task`; +} +/** + * Replace the prompt-derived task title with Cursor's TaskToolCall description + * when the display call is correlated via tool_call_id. SubagentArgs itself has + * no description field, so without this OpenCode shows a cut-off first-five-words + * slice of the prompt. + */ +export function preferCorrelatedTaskDescription(parsed, description) { + if (parsed.toolName !== "task" && parsed.toolName !== "subagent") + return; + if (parsed.resultField !== "subagent_result") + return; + const trimmed = description?.trim(); + if (!trimmed) + return; + parsed.args.description = trimmed; +} +function shellQuote(s) { + return `'${s.replace(/'/g, `'\\''`)}'`; +} +/** + * Map Cursor McpArgs back to the OpenCode tool id. + * Prefers provider_identifier + bare tool_name (github + create_pull_request + * → github_create_pull_request). Builtins under the default server stay bare. + */ +export function mcpRealToolName(mcpArgs, defaultServer = "opencode") { + const toolName = typeof mcpArgs.tool_name === "string" ? mcpArgs.tool_name : undefined; + const provider = typeof mcpArgs.provider_identifier === "string" ? mcpArgs.provider_identifier : undefined; + if (toolName) { + if (provider && provider !== defaultServer) { + // Already a full OpenCode id (legacy ads or model echo). + if (toolName.startsWith(`${provider}_`)) + return toolName; + return `${provider}_${toolName}`; + } + return toolName; + } + const name = typeof mcpArgs.name === "string" ? mcpArgs.name : ""; + const dash = name.indexOf("-"); + if (dash > 0) { + const server = name.slice(0, dash); + const bare = name.slice(dash + 1); + if (server && bare) { + if (server === defaultServer) + return bare; + return `${server}_${bare}`; + } + } + return name || "mcp"; +} +function decodeMcpArgs(raw) { + if (!Array.isArray(raw)) + return {}; + const entries = raw.map((a) => (typeof a === "string" ? b64ToBytes(a) : a)); + try { + return decodeStructEntriesToJson(entries); + } + catch { + return {}; + } +} +function b64ToBytes(s) { + const bin = atob(s); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) + out[i] = bin.charCodeAt(i); + return out; +} +function findOneOfVariant(msg, candidates) { + for (const key of candidates) { + if (msg[key] !== undefined && msg[key] !== null) { + return key; + } + } + return undefined; +} +// ── Pre-execution read validation (Cursor LocalReadExecutor parity) ── +/** Expand a leading `~` to the home directory, matching Cursor's untildify. */ +function untildify(input) { + return input.replace(/^~(?=$|\/|\\)/, os.homedir()); +} +/** + * A read target addressed by URI scheme rather than by local filesystem path. + * + * Cursor's LocalReadExecutor only ever reads local files, so this provider + * mirrors its pre-execution validation. An advertised host read tool may also + * accept URLs or internal URI schemes. Resolving those against the workspace + * root mangles the URI and produces a false `file_not_found`, so URI targets + * are forwarded untouched and left to the advertised executor. + * + * Requires a scheme of two or more characters followed by `://`, so Windows + * drive letters (`C:\src`, `C:/src`) stay local paths. + */ +export function isUriReadTarget(requested) { + return /^[a-zA-Z][a-zA-Z0-9+.-]+:\/\//.test(requested); +} +/** + * Resolve a read target the way Cursor's LocalReadExecutor does before it + * stats the file: untildify, then resolve a relative path against the workspace + * root (`env.workspace_paths[0]`), else resolve as-is. Kept in lockstep with + * agent utils `resolvePath(path, workspaceRoot)`. + * + * Only meaningful for local paths; check {@link isUriReadTarget} first. + */ +export function resolveReadTargetPath(requested, workspaceRoot) { + const expanded = untildify(requested); + if (isAbsoluteToolPath(expanded)) { + return isForeignAbsoluteToolPath(expanded) ? expanded : path.resolve(expanded); + } + if (workspaceRoot) + return joinToolPath(workspaceRoot, expanded); + return path.resolve(expanded); +} +/** + * A typed ReadResult oneof case for a read target that fails Cursor's + * pre-execution validation, or undefined when the path is a readable file the + * tool should actually read. Mirrors LocalReadExecutor exactly: + * - missing path (ENOENT/ENOTDIR) → file_not_found + * - directory → invalid_file "Path is a directory, not a file" + * - socket/fifo/etc (not a file) → invalid_file "Path is neither a file nor a directory" + * EACCES/EPERM (exists but unreadable) and any other stat error return undefined + * so the call proceeds to OpenCode and a genuine permission decision is never + * masked. Never rejects a non-existent *write/edit* target — this is read-only. + */ +export function classifyMissingReadTarget(absolutePath) { + let stat; + try { + stat = fs.statSync(absolutePath); + } + catch (e) { + const code = e.code; + if (code === "ENOENT" || code === "ENOTDIR") { + return { file_not_found: { path: absolutePath } }; + } + return undefined; + } + if (stat.isDirectory()) { + return { invalid_file: { path: absolutePath, reason: "Path is a directory, not a file" } }; + } + if (!stat.isFile()) { + return { + invalid_file: { path: absolutePath, reason: "Path is neither a file nor a directory" }, + }; + } + return undefined; +} +/** + * Encode a pre-execution read rejection as the exact frames Cursor's client + * emits: the ExecClientMessage carrying the typed ReadResult oneof case, then + * the ACM #5 stream_close for that id. `readResult` is the case object from + * classifyMissingReadTarget (e.g. `{ file_not_found: { path } }`). + */ +export function buildReadRejectionMessages(execId, readResult) { + return [ + encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + read_result: readResult, + }, + }), + buildExecStreamClose(execId), + ]; +} +/** + * Build one or more ExecClientMessage frames for a tool result. + * Shell replies are a sequence of ShellStream oneofs under the same id — + * Start → stdout/stderr → exit — then an ACM #5 stream_close so the server + * knows the client finished streaming (CLI always sends this; without it + * shell execs hang on heartbeats forever). + */ +export function buildExecClientMessages(input) { + const resultField = input.resultField || "mcp_result"; + const frames = []; + if (resultField === "shell_stream") { + const stdout = groundShellPathText(input.output, shellPathRoot(input.resultMetadata, input.workspaceRoot)); + // Real clients always emit Start → Stdout/Stderr* → Exit (capture/tests). + frames.push(encodeShellStream(input.execId, undefined, { start: {} })); + if (input.error) { + frames.push(encodeShellStream(input.execId, undefined, { stderr: { data: input.error } })); + frames.push(encodeShellStream(input.execId, input.executionTimeMs, { exit: { code: 1, aborted: false } })); + } + else { + if (stdout) { + frames.push(encodeShellStream(input.execId, undefined, { stdout: { data: stdout } })); + } + if (input.shellOutcome?.kind === "backgrounded") { + frames.push(encodeShellStream(input.execId, input.executionTimeMs, { + backgrounded: { + shell_id: input.shellOutcome.shellId, + command: input.shellOutcome.command, + working_directory: input.shellOutcome.workingDirectory, + pid: input.shellOutcome.pid, + ms_to_wait: input.shellOutcome.msToWait, + reason: input.shellOutcome.reason, + }, + })); + } + else if (input.shellOutcome?.kind === "timeout") { + frames.push(encodeShellStream(input.execId, input.executionTimeMs, { + // Native CLI represents timeout structurally. ShellAbortReason.TIMEOUT=2. + exit: { code: 0, aborted: true, abort_reason: 2 }, + })); + } + else { + const exitCode = input.shellOutcome?.kind === "exit" + ? Math.max(0, Math.min(0xffff_ffff, input.shellOutcome.code)) + : 0; + frames.push(encodeShellStream(input.execId, input.executionTimeMs, { + exit: { code: exitCode, aborted: false }, + })); + } + } + } + else { + const clientMsg = { + id: input.execId, + local_execution_time_ms: input.executionTimeMs ?? 0, + }; + clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName, input.resultMetadata, input.shellOutcome, input.workspaceRoot); + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: clientMsg, + })); + } + // Always close the exec stream — mirrors CLI agent-exec after every handler. + frames.push(buildExecStreamClose(input.execId)); + return frames; +} +/** ACM #5 exec_client_control_message { stream_close { id } }. */ +export function buildExecStreamClose(execId) { + return encodeMessage("AgentClientMessage", { + exec_client_control_message: { + stream_close: { id: execId }, + }, + }); +} +export function buildUnsupportedExecDeny(input) { + const { execId, variant, reason } = input; + const resultName = variant.resultName; + const frames = []; + const throwFrame = (msg) => encodeMessage("AgentClientMessage", { + exec_client_control_message: { + throw: { id: execId, error: msg }, + }, + }); + switch (resultName) { + case "shell_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + shell_result: { rejected: { reason } }, + }, + })); + break; + case "diagnostics_result": + case "canvas_diagnostics_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + [resultName]: { error: { path: "", error: reason } }, + }, + })); + break; + case "fetch_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + fetch_result: { error: { url: "", error: reason } }, + }, + })); + break; + case "record_screen_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + record_screen_result: { failure: { error: reason } }, + }, + })); + break; + case "computer_use_result": + case "write_shell_stdin_result": + case "smart_mode_classifier_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + [resultName]: { error: { error: reason } }, + }, + })); + break; + case "redacted_read_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + redacted_read_result: { error: { path: "", error: reason } }, + }, + })); + break; + case "subagent_await_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + subagent_await_result: { error: { error: reason } }, + }, + })); + break; + case "shell_allowlist_precheck_result": + case "mcp_allowlist_precheck_result": + case "web_fetch_allowlist_precheck_result": + // Typed precheck result is a bool, not a reason string. Encoding + // `{allowlisted:false}` selects the ExecClientMessage oneof; protobufjs + // writes field 1 as varint 0 so Cursor decodes the deny rather than an + // empty/unspecified precheck. + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + [resultName]: { allowlisted: false }, + }, + })); + break; + case "force_background_shell_result": + case "force_background_subagent_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + [resultName]: { status: FORCE_BACKGROUND_STATUS_ERROR }, + }, + })); + break; + case "mini_swe_agent_bash_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + mini_swe_agent_bash_result: { rejected: { reason } }, + }, + })); + break; + case "conversation_search_result": + // No local Cursor conversation index. Empty success is truthful and + // avoids a retry loop; the model can continue with listed tools. + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + conversation_search_result: { + success: { hits: [], truncated: false, partial: false, rebuilding: false }, + }, + }, + })); + break; + case "agent_store_conflict_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + agent_store_conflict_result: { error: { error: reason } }, + }, + })); + break; + case "adopt_result": + frames.push(encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + local_execution_time_ms: 0, + adopt_result: { error: reason }, + }, + })); + break; + case "execute_hook_result": + // agent.proto has no populated error oneof we can emit; + // Cursor CLI answers this through exec_client_control_message.throw. + frames.push(throwFrame(reason)); + frames.push(buildExecStreamClose(execId)); + return frames; + default: + // Fallback for any future unsupported variant without a dedicated shape: + // typed error when possible, else throw. Unknown shapes should hard-fail + // at the pump, so this path is not expected for known unsupported rows. + frames.push(throwFrame(reason)); + frames.push(buildExecStreamClose(execId)); + return frames; + } + frames.push(buildExecStreamClose(execId)); + return frames; +} +/** OpenCode 2 `read` prints this when a text page does not reach EOF. */ +const OPENCODE2_READ_TRUNCATION = /^\[Output truncated\. Continue reading with offset:\s*(\d+)\]\s*$/; +/** OpenCode 2 stops a read at 2,000 lines as well as 50 KB. */ +const OPENCODE2_READ_MAX_LINES = 2_000; +const OPENCODE2_READ_MAX_LINE_CHARS = 2_000; +const OPENCODE2_READ_LINE_TRUNCATION = `... (line truncated to ${OPENCODE2_READ_MAX_LINE_CHARS} chars)`; +/** + * `read-filesystem.ts` shortens each selected line to 2,000 UTF-16 code units. + * One code unit encodes to at most three UTF-8 bytes. Add the 34-byte + * `... (line truncated to 2000 chars)` suffix and one line separator when + * proving a page was close enough for the next line not to fit. + */ +const OPENCODE2_MAX_RENDERED_LINE_BYTES = (OPENCODE2_READ_MAX_LINE_CHARS * 3) + Buffer.byteLength(OPENCODE2_READ_LINE_TRUNCATION, "utf8") + 1; +function normalizeToolText(output) { + return output.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); +} +/** + * OpenCode 2 `ReadTool.toModelContent` for a file. The whole string must match; + * a file that merely quotes the header stays a numbered body line. + * + * Read file , lines - + * : + * [Output truncated. Continue reading with offset: ] + */ +function parseOpenCode2FileRead(output, resultMetadata) { + const normalized = normalizeToolText(output).replace(/\n+$/, ""); + if (!normalized.startsWith("Read file ")) + return undefined; + const lines = normalized.split("\n"); + const headerLine = (lines[0] ?? "").trim(); + const empty = /^Read file (.*), 0 lines$/.exec(headerLine); + if (empty) { + if (lines.length !== 1) + return undefined; + return { + path: empty[1] ?? "", + content: "", + totalLines: 0, + hostTruncated: false, + outputCapped: false, + truncatedLines: [], + }; + } + const header = /^Read file (.*), lines (\d+)-(\d+)$/.exec(headerLine); + if (!header) + return undefined; + const start = Number(header[2]); + const end = Number(header[3]); + if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) { + return undefined; + } + let bodyEnd = lines.length; + let nextOffset; + const banner = OPENCODE2_READ_TRUNCATION.exec(lines[lines.length - 1] ?? ""); + if (banner) { + nextOffset = Number(banner[1]); + if (!Number.isSafeInteger(nextOffset) || nextOffset < 1) + return undefined; + bodyEnd -= 1; + } + const expected = end - start + 1; + if (bodyEnd - 1 !== expected) + return undefined; + const raw = []; + const truncatedLines = []; + for (let index = 0; index < expected; index++) { + const match = /^(\d+):[ \t](.*)$/.exec(lines[index + 1] ?? ""); + if (!match || Number(match[1]) !== start + index) + return undefined; + const value = match[2] ?? ""; + if (value.length === OPENCODE2_READ_MAX_LINE_CHARS + OPENCODE2_READ_LINE_TRUNCATION.length + && value.endsWith(OPENCODE2_READ_LINE_TRUNCATION)) { + truncatedLines.push(start + index); + } + raw.push(value); + } + const content = raw.join("\n"); + const hostTruncated = nextOffset !== undefined; + const requestedLimit = num(resultMetadata?.limit); + const effectiveLineLimit = requestedLimit !== undefined && requestedLimit > 0 + ? Math.min(requestedLimit, OPENCODE2_READ_MAX_LINES) + : OPENCODE2_READ_MAX_LINES; + // OpenCode 2 ends a text page only for the requested/default line limit or + // the 50 KiB byte limit. Fewer lines than the effective line limit, together + // with a page close enough that one maximum-size rendered line may not fit, + // identifies the byte cap without the old ASCII-only 4 KiB guess. + const outputCapped = hostTruncated + && expected < effectiveLineLimit + && Buffer.byteLength(content, "utf8") > OPENCODE_READ_MAX_BYTES - OPENCODE2_MAX_RENDERED_LINE_BYTES; + if (hostTruncated) { + return { + path: header[1] ?? "", + content, + startLine: start, + endLine: end, + nextOffset, + hostTruncated: true, + outputCapped, + truncatedLines, + }; + } + // A page that reached EOF is complete. Record the span so a non-1 offset is + // not later mistaken for a silent cap, without flagging it truncated. + return { + path: header[1] ?? "", + content, + ...(start === 1 + ? { totalLines: end } + : { startLine: start, endLine: end, totalLines: end }), + hostTruncated: false, + outputCapped: false, + truncatedLines, + }; +} +/** + * OpenCode 2 directory reads list bare entry names. Join them onto the + * directory, then onto the workspace root, so the model can copy a real path + * instead of inventing an absolute prefix. + */ +function parseOpenCode2DirectoryListing(output, workspaceRoot) { + const normalized = normalizeToolText(output).replace(/\n+$/, ""); + if (!normalized.startsWith("Read directory ")) + return undefined; + const lines = normalized.split("\n"); + const header = /^Read directory (.*), (?:0 entries|entries (\d+)-(\d+))$/.exec((lines[0] ?? "").trim()); + if (!header) { + trace("parseOpenCode2DirectoryListing: OpenCode 2 directory header did not match — leaving output unchanged"); + return undefined; + } + const requested = header[1] ?? ""; + if (!isAbsoluteToolPath(requested) && !workspaceRoot) + return undefined; + let bodyEnd = lines.length; + let banner; + if (bodyEnd > 1 && OPENCODE2_READ_TRUNCATION.test(lines[bodyEnd - 1] ?? "")) { + banner = lines[bodyEnd - 1]; + bodyEnd -= 1; + } + const rawEntries = lines.slice(1, bodyEnd); + if (header[2] === undefined) { + if (rawEntries.length !== 0) + return undefined; + } + else { + const start = Number(header[2]); + const end = Number(header[3]); + if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end - start + 1 !== rawEntries.length) { + return undefined; + } + } + const directory = resolveToolPath(requested, workspaceRoot); + const directories = []; + const files = []; + const entries = rawEntries.map((entry) => { + const resolved = resolveListedEntry(directory, entry); + if (entry.endsWith("/") || entry.endsWith("\\")) + directories.push(resolved); + else + files.push(resolved); + return resolved; + }); + const headerLine = header[2] === undefined + ? `Read directory ${directory}, 0 entries` + : `Read directory ${directory}, entries ${header[2]}-${header[3]}`; + return { + directory, + entries, + directories, + files, + text: [headerLine, ...entries, ...(banner ? [banner] : [])].join("\n"), + }; +} +/** + * Absolute on this host, plus Windows drive and UNC paths. A posix agent must + * not prefix `C:/…` or `\\server\…` just because `path.isAbsolute` rejects them. + */ +function isAbsoluteToolPath(filePath) { + return path.isAbsolute(filePath) + || /^[A-Za-z]:[\\/]/.test(filePath) + || filePath.startsWith("\\\\"); +} +/** Windows drive or UNC path that `path.isAbsolute` rejects on this host. */ +function isForeignAbsoluteToolPath(filePath) { + return isAbsoluteToolPath(filePath) && !path.isAbsolute(filePath); +} +/** Separator already used by a drive or UNC path. Host paths keep `path.sep`. */ +function toolPathSeparator(filePath) { + if (path.isAbsolute(filePath)) + return path.sep; + if (filePath.startsWith("\\\\") || (filePath.includes("\\") && !filePath.includes("/"))) + return "\\"; + return "/"; +} +/** + * Join `relative` onto `root`. Node's `path.resolve` treats `C:/…` and + * `\\server\…` as relative on posix and prefixes the process cwd. Keep those + * roots intact, including `..`, and still use `path.resolve` for host paths. + */ +function joinToolPath(root, relative) { + // `path.resolve` drops a trailing separator. Glob and find use that separator + // to tell a directory from a file; putting it back keeps the marker. + const directory = relative.endsWith("/") || relative.endsWith("\\"); + const joined = joinResolvedToolPath(root, relative); + if (!directory || joined.endsWith("/") || joined.endsWith("\\")) + return joined; + return `${joined}${toolPathSeparator(joined)}`; +} +function joinResolvedToolPath(root, relative) { + if (!isForeignAbsoluteToolPath(root)) + return path.resolve(root, relative); + const sep = toolPathSeparator(root); + const base = splitForeignAbsolute(root, sep); + const parts = [...base.segments]; + for (const part of relative.split(/[\\/]+/)) { + if (!part || part === ".") + continue; + if (part === "..") { + if (parts.length > base.frozen) + parts.pop(); + continue; + } + parts.push(part); + } + return parts.length === 0 ? base.prefix : `${base.prefix}${parts.join(sep)}`; +} +function splitForeignAbsolute(filePath, sep) { + if (filePath.startsWith("\\\\")) { + const segments = filePath.slice(2).split(/[\\/]+/).filter(Boolean); + return { prefix: "\\\\", segments, frozen: Math.min(2, segments.length) }; + } + const rest = filePath.slice(2).replace(/^[\\/]+/, ""); + return { + prefix: `${filePath.slice(0, 2)}${sep}`, + segments: rest ? rest.split(/[\\/]+/).filter(Boolean) : [], + frozen: 0, + }; +} +function isRelativePathToken(token) { + if (!token || isAbsoluteToolPath(token)) + return false; + // A slash alone does not make arbitrary shell output a path. In particular, + // compact JSON, quoted strings, package ids, and shell syntax must remain + // byte-for-byte model-visible rather than being prefixed with the workspace. + if (/[\0"'`{}\[\]<>|;]/.test(token) || token.startsWith("@")) + return false; + if (token.startsWith("./") + || token.startsWith("../") + || token.startsWith(".\\") + || token.startsWith("..\\")) + return true; + return token.includes("/") || (path.sep === "\\" && token.includes("\\")); +} +function resolveListedEntry(directory, entry) { + if (!entry || isAbsoluteToolPath(entry)) + return entry; + if (entry === "~" || entry.startsWith("~/") || entry.startsWith("~\\")) + return entry; + const directoryEntry = entry.endsWith("/") || entry.endsWith("\\"); + const resolved = joinToolPath(directory, entry); + if (!directoryEntry || resolved.endsWith("/") || resolved.endsWith("\\")) + return resolved; + return `${resolved}${toolPathSeparator(resolved)}`; +} +function resolveToolPath(filePath, workspaceRoot) { + if (!filePath || isAbsoluteToolPath(filePath)) + return filePath; + if (filePath === "~" || filePath.startsWith("~/") || filePath.startsWith("~\\")) + return filePath; + if (!workspaceRoot) + return filePath; + return joinToolPath(workspaceRoot, filePath); +} +/** + * Rewrite OpenCode 2 grep/glob lines that are still project-relative. Absolute + * paths, indented match previews, and prose stay untouched. Grep file headers + * drop the trailing colon only in the structured file list, not in this text. + */ +/** + * Longer phrases first. "No files found" is a prefix of the pattern sentence, + * and a suffix check must not stop on the shorter one. + */ +const SEARCH_STATUS_MESSAGES = [ + "No files found matching pattern", + "No matches before timeout (scan incomplete)", + "No files found", + "No matches found", +]; +/** + * A glob/find miss is prose, not a path. Hosts sometimes still carry it as a + * path segment (`../../workspace/No files found matching pattern`) or fold that + * segment into a `# dir/` header plus a bare sentence. Either shape must stay + * the sentence: joining it onto the workspace is what lists a fake file. + */ +function searchStatusMessage(line) { + const trimmed = line.trim().replace(/^#+\s+/, ""); + for (const message of SEARCH_STATUS_MESSAGES) { + if (trimmed === message || trimmed.endsWith(`/${message}`) || trimmed.endsWith(`\\${message}`)) { + return message; + } + } + return undefined; +} +function collapseSearchStatus(output) { + return output.split("\n").map((line) => searchStatusMessage(line) ?? line).join("\n"); +} +function isSearchStatusReport(output) { + const lines = output.split("\n").map((line) => line.trim()).filter(Boolean); + return lines.length > 0 && lines.every((line) => searchStatusMessage(line) !== undefined); +} +/** + * Cursor's find/glob executor shows one path per line. A directory ends with + * `/`; a file does not. Some hosts instead print a folded tree: + * + * # src/ + * a.ts + * ## components/ + * button.tsx + * # tests/ + * + * A `# dir/` line is a grouping header, not another match. Emitting it lists + * the directory beside its own children (and the shared search prefix shows up + * as a blank entry once that prefix is the directory being searched). Expand + * the tree to the files underneath only. A header with no child is still just + * an empty directory in the walk — Cursor find reports files, so leave it out + * (same as targeting that empty directory and getting a miss). + */ +function parseGroupedPathListing(output) { + const lines = output.replace(/\n+$/, "").split("\n"); + const events = []; + const stack = []; + let sawHeader = false; + let inNotes = false; + for (const line of lines) { + if (!line.trim()) { + if (sawHeader) + inNotes = true; + continue; + } + if (inNotes) { + events.push({ kind: "note", text: line }); + continue; + } + const status = searchStatusMessage(line); + if (status) { + events.push({ kind: "note", text: status }); + continue; + } + const header = /^(#+)\s+(\S.*?)\s*$/.exec(line); + if (header) { + const rawName = header[2] ?? ""; + if (!rawName.endsWith("/") && !rawName.endsWith("\\")) + return undefined; + const depth = header[1].length - 1; + if (depth > stack.length) + return undefined; + const name = rawName.slice(0, -1); + if (!name || name === "." || name === "..") + return undefined; + stack.length = depth; + const parent = depth === 0 ? "" : (stack[depth - 1] ?? ""); + if (depth > 0 && !parent) + return undefined; + const full = parent ? joinDisplayPath(parent, name) : name; + stack.push(full); + events.push({ kind: "dir", depth, path: full }); + sawHeader = true; + continue; + } + if (line.startsWith(" ") || line.startsWith("\t") || /\s/.test(line) || line.includes("://")) { + return undefined; + } + if (line.includes("/") || line.includes("\\")) + return undefined; + if (!sawHeader) { + events.push({ kind: "file", path: line }); + continue; + } + const parent = stack[stack.length - 1]; + if (!parent) + return undefined; + events.push({ kind: "file", path: joinDisplayPath(parent, line) }); + } + if (!sawHeader) + return undefined; + const paths = []; + const notes = []; + let sawFile = false; + for (const event of events) { + if (event.kind === "note") + notes.push(event.text); + else if (event.kind === "file") { + sawFile = true; + paths.push(event.path); + } + // Directory headers are structure only — never emit them as matches. + } + // The header existed only to hold the miss sentence. Emitting it lists a + // directory that was never a match, usually the walk back to the workspace. + if (!sawFile && notes.some((note) => searchStatusMessage(note))) + return { paths: [], notes }; + return { paths, notes }; +} +function joinDisplayPath(parent, child) { + if (!parent || parent === ".") + return child; + if (parent === "/") + return `/${child}`; + const sep = parent.includes("\\") && !parent.includes("/") ? "\\" : "/"; + if (parent.endsWith("/") || parent.endsWith("\\")) + return `${parent}${child}`; + return `${parent}${sep}${child}`; +} +function renderGroupedPathListing(listing, workspaceRoot) { + const paths = listing.paths.map((entry) => resolveToolPath(entry, workspaceRoot)); + if (paths.length === 0) { + // Grouped output that only had directory headers (empty dirs) is a miss, + // same as targeting that empty directory directly. + if (listing.notes.length === 0) + return "No files found"; + return listing.notes.join("\n"); + } + if (listing.notes.length === 0) + return paths.join("\n"); + return [...paths, "", ...listing.notes].join("\n"); +} +function groundSearchOutput(output, workspaceRoot) { + const normalized = normalizeToolText(output); + const grouped = parseGroupedPathListing(normalized); + if (grouped) + return renderGroupedPathListing(grouped, workspaceRoot); + const collapsed = collapseSearchStatus(normalized); + if (isSearchStatusReport(collapsed)) + return collapsed; + if (!workspaceRoot) + return collapsed; + const first = collapsed.split("\n", 1)[0] ?? ""; + const searchShaped = /^Found \d+ matches/.test(first) || searchStatusMessage(first) !== undefined; + if (!searchShaped && !isBarePathList(collapsed)) + return collapsed; + return collapsed.split("\n").map((line) => rewriteSearchPathLine(line, workspaceRoot)).join("\n"); +} +function isBarePathList(output) { + const lines = output.split("\n").map((line) => line.trim()).filter(Boolean); + if (lines.length === 0 || lines.length > 2000) + return false; + return lines.every((line) => { + if (line.startsWith("(")) + return true; + if (/\s/.test(line) || line.includes("://")) + return false; + return true; + }); +} +function rewriteSearchPathLine(line, workspaceRoot) { + if (!line || line.startsWith(" ") || line.startsWith("\t") || line.startsWith("(")) + return line; + const status = searchStatusMessage(line); + if (status || line.startsWith("Found ")) + return status ?? line; + const header = /^(.*):$/.exec(line); + if (header && !header[1]?.includes("://")) { + return `${resolveToolPath(header[1] ?? "", workspaceRoot)}:`; + } + if (line.includes("://")) + return line; + return resolveToolPath(line, workspaceRoot); +} +/** + * Shell stdout is mixed prose. Rewrite only tokens that are clearly relative + * paths, including `file:line` and `file:line:col`. Leave sentences, URLs, and + * status words alone. + */ +function groundShellPathText(output, root) { + if (!root || !output) + return output; + return normalizeToolText(output).split("\n").map((line) => rewriteShellPathLine(line, root)).join("\n"); +} +function shellPathRoot(resultMetadata, workspaceRoot) { + const workingDirectory = str(resultMetadata?.working_directory)?.trim(); + if (!workingDirectory) + return workspaceRoot; + if (isAbsoluteToolPath(workingDirectory)) + return workingDirectory; + return workspaceRoot ? joinToolPath(workspaceRoot, workingDirectory) : undefined; +} +function rewriteShellPathLine(line, root) { + if (!line || line.startsWith(" ") || line.startsWith("\t")) + return line; + const trimmed = line.trimEnd(); + if (trimmed.includes("://") || trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\")) { + return line; + } + const located = /^(.+?):(\d+)(?::(\d+))?$/.exec(trimmed); + if (located && located[1] && isRelativePathToken(located[1])) { + const suffix = located[3] !== undefined ? `:${located[2]}:${located[3]}` : `:${located[2]}`; + return `${resolveToolPath(located[1], root)}${suffix}`; + } + if (/\s/.test(trimmed)) + return line; + const header = /^(.*):$/.exec(trimmed); + if (header && header[1] && isRelativePathToken(header[1])) { + return `${resolveToolPath(header[1], root)}:`; + } + if (isRelativePathToken(trimmed)) + return resolveToolPath(trimmed, root); + return line; +} +const OPENCODE_GREP_LINE = /^[ \t]+Line (\d+):[ \t]?(.*)$/; +/** + * OpenCode 1.x and 2.0 both render grep as `Found N matches`, a `path:` header, + * and indented `Line N: text` previews. A page with no previews is not content. + */ +function parseOpenCodeGrepContent(output, workspaceRoot) { + const lines = normalizeToolText(output).split("\n"); + const first = lines[0]?.trim() ?? ""; + if (!/^Found \d+ matches\b/.test(first)) + return undefined; + const matches = []; + let current; + let truncated = /\bmore matches available\b/.test(first); + let sawLine = false; + for (const raw of lines.slice(1)) { + if (!raw.trim()) + continue; + const preview = OPENCODE_GREP_LINE.exec(raw); + if (preview) { + sawLine = true; + if (!current) + continue; + const lineNumber = Number(preview[1]); + if (!Number.isSafeInteger(lineNumber) || lineNumber < 1) + continue; + current.matches.push({ line_number: lineNumber, content: preview[2] ?? "" }); + continue; + } + if (raw.trimStart().startsWith("(")) { + if (/truncat/i.test(raw)) + truncated = true; + continue; + } + if (raw.startsWith(" ") || raw.startsWith("\t")) + continue; + const header = /^(.*):$/.exec(raw.trim()); + if (!header || !header[1] || header[1].includes("://")) + continue; + current = { file: resolveToolPath(header[1], workspaceRoot), matches: [] }; + matches.push(current); + } + if (!sawLine) + return undefined; + const withHits = matches.filter((file) => file.matches.length > 0); + if (withHits.length === 0) + return undefined; + return { + matches: withHits, + truncated, + totalMatchedLines: withHits.reduce((count, file) => count + file.matches.length, 0), + }; +} +function grepRequestResultMetadata(raw) { + const pattern = str(raw.pattern); + const outputMode = str(raw.output_mode); + const searchPath = str(raw.path); + if (!pattern && !outputMode && !searchPath) + return undefined; + return { + ...(pattern ? { pattern } : {}), + ...(outputMode ? { output_mode: outputMode } : {}), + ...(searchPath ? { path: searchPath } : {}), + }; +} +function extractGroundedPaths(output, workspaceRoot) { + const normalized = normalizeToolText(output); + const grouped = parseGroupedPathListing(normalized); + if (grouped) { + return grouped.paths.map((entry) => resolveToolPath(entry, workspaceRoot)).slice(0, 2000); + } + const collapsed = collapseSearchStatus(normalized); + if (isSearchStatusReport(collapsed)) + return []; + const directory = parseOpenCode2DirectoryListing(collapsed, workspaceRoot); + if (directory) + return directory.entries.slice(0, 2000); + const first = collapsed.split("\n", 1)[0]?.trim() ?? ""; + if (/^Found \d+ matches/.test(first) || searchStatusMessage(first)) { + const paths = []; + for (const raw of collapsed.split("\n").slice(1)) { + const line = raw.trim(); + if (!line + || line.startsWith("(") + || line.startsWith("Line ") + || line.startsWith("#") + || searchStatusMessage(line) + || raw.startsWith(" ") + || raw.startsWith("\t")) { + continue; + } + const header = /^(.*):$/.exec(line); + const candidate = header && !header[1]?.includes("://") ? (header[1] ?? "") : line; + if (!candidate || candidate.includes("://") || searchStatusMessage(candidate)) + continue; + paths.push(resolveToolPath(candidate, workspaceRoot)); + } + return paths.slice(0, 2000); + } + if (isBarePathList(output)) { + return output + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("(")) + .map((line) => resolveToolPath(line.endsWith(":") && !line.includes("://") ? line.slice(0, -1) : line, workspaceRoot)) + .slice(0, 2000); + } + return extractPathLines(output).map((line) => { + const trimmed = line.trim(); + const candidate = trimmed.endsWith(":") && !trimmed.includes("://") ? trimmed.slice(0, -1) : trimmed; + return resolveToolPath(candidate, workspaceRoot); + }); +} +/** + * Strip opencode's `read` envelope, leaving raw file content. + * + * OpenCode 1.x (`tool/read.ts`) wraps content in an XML-ish envelope its own + * models are trained on, but Cursor's are not: + * {abs}\nfile\n\n{N}: {line}\n…\n\n{footer}\n + * OpenCode 2 replaced that with `Read file , lines -` plus + * the same `N: ` prefixes and `[Output truncated. Continue reading with offset: N]`. + * Forwarding either envelope verbatim made Cursor's model treat the wrapper as + * literal file content and write tags or line prefixes back into files. + * + * Returns the raw file body (line numbers + footer + `` dropped). + * + * Deliberately exception-safe: if the expected envelope is absent — non-read + * output, already-raw text, or a future opencode format change — it returns the + * input unchanged, so a result is never broken and we never throw mid-turn. + * Callers must still gate `mcp_result` on `toolName === "read"`; this helper + * alone is not a tool-identity check. + */ +export function unwrapReadOutput(output) { + if (typeof output !== "string" || output.length === 0) + return output; + const opencode2 = parseOpenCode2FileRead(output); + if (opencode2) + return opencode2.content; + if (normalizeToolText(output).startsWith("Read file ")) { + trace("unwrapReadOutput: OpenCode 2 read header did not match the page parser — leaving output unchanged"); + } + // Require the full opencode read-envelope skeleton *before* `` + // (read.ts opens with …, file, ) so a + // stray "" later in tool chatter can't trigger unwrapping just + // because path/type tags appear elsewhere in the payload. + const contentHeaderIdx = output.indexOf(""); + if (contentHeaderIdx === -1) + return output; + const header = output.slice(0, contentHeaderIdx); + const hasSkeleton = header.indexOf("") !== -1 && + header.indexOf("file") !== -1; + if (!hasSkeleton) { + // Saw "" but not the read skeleton ahead of it — almost certainly + // a non-read payload, or an opencode format drift. Surface it so drift + // can't silently resurrect the wrapper-corruption bug, but still fail + // safe (no mutate). + trace("unwrapReadOutput: present without leading /file> skeleton — leaving output unchanged (possible non-read payload or opencode read format drift)"); + return output; + } + // Body starts right after "\n". opencode emits one numbered line per + // file line ("N: "), then a blank, a "(…)" footer, and a standalone + // "". The body is a *contiguous run* of /^N: / lines — so we stop at + // the first non-numbered line. Critically we do NOT search for a closing + // "" substring: a file line that literally contains "" + // is rendered as "N: " (a body line), and a raw indexOf would + // truncate the read there. + let rest = output.slice(contentHeaderIdx + "".length); + if (rest.startsWith("\n")) + rest = rest.slice(1); + const raw = []; + for (const line of rest.split("\n")) { + const m = /^(\d+):[ \t]?(.*)$/.exec(line); + if (!m) + break; // blank / "(footer)" / "" → end of body run + // Strip only the leading "N: " prefix; a line that itself begins with + // digits+colon keeps its content (we remove just the first match). Blank + // file lines render as "N: " and are preserved (capture group is ""). + raw.push(m[2]); + } + // Envelope confirmed but no numbered body → empty file. Return "" rather + // than the envelope (the envelope is exactly what Cursor echoes into writes). + return raw.join("\n"); +} +/** + * Map OpenCode tool text into the agent.v1 result oneof for each exec variant. + * OpenCode returns free-form text; we wrap it in the minimal success shape the + * server accepts (verified against agent.v1 wire captures). + */ +export function buildTypedExecResult(resultField, output, error, toolName, resultMetadata, shellOutcome, workspaceRoot) { + // Prefer the session workspace; never advertise the host process cwd (daemon + // often starts in $HOME) as the path Cursor shows the model for glob/ls. + const trimmedRoot = typeof workspaceRoot === "string" ? workspaceRoot.trim() : ""; + // A posix process must not turn `C:/…` or `\\server\…` into `/C:/…`. + const resultRoot = trimmedRoot + ? (isForeignAbsoluteToolPath(trimmedRoot) ? trimmedRoot : path.resolve(trimmedRoot)) + : undefined; + switch (resultField) { + case "read_result": { + const parsedFile = parseOpenCode2FileRead(output, resultMetadata); + const listing = error ? undefined : parseOpenCode2DirectoryListing(output, resultRoot); + const rawPath = str(resultMetadata?.path) + ?? parsedFile?.path + ?? listing?.directory + ?? extractPathTag(output) + ?? ""; + const readPath = resolveToolPath(rawPath, resultRoot); + if (error) + return { error: { path: readPath, error } }; + // The host may have resolved an alternate spelling after the request + // (for example a Unicode-space filename). Use the result's own path for + // local stat/newline recovery while preserving the requested path in the + // Cursor result, matching the existing OpenCode 1 behavior. + const outputPath = parsedFile?.path ?? extractPathTag(output); + const statPath = resolveToolPath(outputPath ?? readPath, resultRoot); + // Strip OpenCode's read envelope so Cursor's model receives raw file + // content and can't echo the wrapper into subsequent writes. + // Cursor's native read_args lands here; `mcp_args` reads land in + // mcp_result. Live captures show gpt-5.4-mini and grok-4.5 both use the + // native channel, so this is the path that matters in practice. + const outputMetadata = parseOpenCodeReadMetadata(output, resultMetadata); + const content = listing + ? listing.text + : restoreCompleteReadTerminator(unwrapReadOutput(output), statPath, outputMetadata, resultMetadata, resultRoot); + const totalLines = outputMetadata.totalLines ?? readFileLineCount(statPath) ?? countLines(content); + const rangeApplied = readRangeApplied(resultMetadata, totalLines); + // `truncated` alone is not enough: it is set here, and models still assert + // the partial content is the whole file. Cursor's own executor puts the + // limit marker in the output text for the same reason, so append one. + const notices = [ + readTruncationNotice(output, resultMetadata), + readLongLineTruncationNotice(output), + ].filter((notice) => !!notice); + return { + success: { + path: listing?.directory || readPath, + content: notices.length > 0 ? `${content}\n\n${notices.join("\n\n")}` : content, + total_lines: totalLines, + file_size: listing ? 0 : readFileSize(statPath), + truncated: readOutputTruncated(resultMetadata, outputMetadata, totalLines), + range_applied: rangeApplied, + }, + }; + } + case "grep_result": { + if (error) + return { error: { error } }; + const cwd = resultRoot ?? ""; + const pattern = str(resultMetadata?.pattern) ?? ""; + const requestedPath = str(resultMetadata?.path) ?? cwd; + const requestedMode = str(resultMetadata?.output_mode); + // OpenCode always returns match previews. Cursor's native Grep defaults + // to content; encoding that as files_with_matches drops the previews and + // the model only sees paths. Keep files_with_matches for an explicit + // files-only request, and for path lists (glob remaps, no matches). + const parsedContent = parseOpenCodeGrepContent(output, resultRoot); + const content = requestedMode === "files_with_matches" + ? undefined + : parsedContent; + if (content) { + return { + success: { + pattern, + path: requestedPath, + output_mode: "content", + workspace_results: { + [cwd]: { + content: { + matches: content.matches, + total_lines: content.totalMatchedLines, + total_matched_lines: content.totalMatchedLines, + client_truncated: content.truncated, + ripgrep_truncated: false, + }, + }, + }, + }, + }; + } + const files = extractGroundedPaths(output, resultRoot); + return { + success: { + pattern, + path: requestedPath, + output_mode: "files_with_matches", + workspace_results: { + [cwd]: { + files: { + files, + total_files: files.length, + client_truncated: parsedContent?.truncated ?? false, + }, + }, + }, + }, + }; + } + case "write_result": { + // Prefer the path recorded from WriteArgs / a remapped apply_patch write. + // Host write output is not a `` envelope, and Cursor's GetMcpTools + // spill copies WriteSuccess.path into the model-visible filePath. + const remappedPath = str(resultMetadata?.path); + if (error) + return { error: { path: remappedPath ?? "", error } }; + return { + success: { + path: remappedPath ?? extractPathTag(output) ?? "", + lines_created: countLines(output), + file_size: output.length, + }, + }; + } + case "pi_write_result": + // PiWriteExecSuccess is just { output }; error is { error }. + if (error) + return { error: { error } }; + return { success: { output: output || "Wrote file successfully." } }; + case "pi_read_result": { + if (error) + return { error: { error } }; + // Pi results carry truncation structurally (PiReadExecSuccess field 2), + // which is how Cursor's own executors report a capped payload. + const content = unwrapReadOutput(output); + const truncation = readTruncationMessage(output, content, resultMetadata); + return { success: { output: content, ...(truncation ? { truncation } : {}) } }; + } + case "shell_result": { + const command = str(resultMetadata?.command) ?? ""; + const workingDirectory = str(resultMetadata?.working_directory) ?? ""; + const stdout = groundShellPathText(output, shellPathRoot(resultMetadata, resultRoot)); + if (error) { + return { + failure: { + command, + working_directory: workingDirectory, + exit_code: 1, + stdout: stdout || "", + stderr: error, + aborted: false, + }, + }; + } + if (shellOutcome?.kind === "timeout") { + return { + timeout: { + command, + working_directory: workingDirectory, + timeout_ms: shellOutcome.timeoutMs, + }, + }; + } + if (shellOutcome?.kind === "backgrounded") { + return { + success: { + command: shellOutcome.command || command, + working_directory: shellOutcome.workingDirectory || workingDirectory, + exit_code: 0, + stdout: stdout, + shell_id: shellOutcome.shellId, + pid: shellOutcome.pid, + ms_to_wait: shellOutcome.msToWait, + background_reason: shellOutcome.reason, + }, + is_background: true, + pid: shellOutcome.pid, + }; + } + const exitCode = shellOutcome?.kind === "exit" + ? Math.max(0, Math.min(0xffff_ffff, shellOutcome.code)) + : 0; + return { + success: { + command, + working_directory: workingDirectory, + exit_code: exitCode, + stdout: stdout, + }, + }; + } + case "pi_bash_result": + if (error) + return { error: { error } }; + return { success: { output: groundShellPathText(output, shellPathRoot(resultMetadata, resultRoot)) } }; + case "pi_edit_result": + if (error) + return { error: { error } }; + return { success: { output } }; + case "pi_grep_result": + case "pi_find_result": + if (error) + return { error: { error } }; + return { success: { output: groundSearchOutput(output, resultRoot) } }; + case "pi_ls_result": { + if (error) + return { error: { error } }; + const listing = parseOpenCode2DirectoryListing(output, resultRoot); + return { success: { output: listing?.text ?? groundSearchOutput(output, resultRoot) } }; + } + case "delete_result": + if (error) + return { error: { path: "", error } }; + return { success: { path: "", deleted_file: "" } }; + case "background_shell_spawn_result": { + const command = str(resultMetadata?.command) ?? ""; + const workingDirectory = str(resultMetadata?.working_directory) ?? ""; + if (error) + return { error: { command, working_directory: workingDirectory, error } }; + // Prefer the structured outcome captured by the plugin after-hook; markers + // are already stripped from the stored/rendered OpenCode output by then. + if (shellOutcome?.kind === "backgrounded") { + return { + success: { + shell_id: shellOutcome.shellId, + command: shellOutcome.command || command, + working_directory: shellOutcome.workingDirectory || workingDirectory, + pid: shellOutcome.pid, + }, + }; + } + // Fallback when no plugin hook ran: parse the private spawn marker inline. + const match = new RegExp(`${BACKGROUND_SHELL_MARKER}(\\d+):([^\\r\\n]+)`).exec(output); + const pid = match ? Number(match[1]) : 0; + if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff) { + return { + error: { + command, + working_directory: workingDirectory, + error: "OpenCode did not return a valid background shell process id.", + }, + }; + } + return { + success: { + shell_id: pid, + command, + working_directory: workingDirectory, + pid, + }, + }; + } + case "ls_result": { + if (error) + return { error: { path: "", error } }; + const listing = parseOpenCode2DirectoryListing(output, resultRoot); + const rootPath = listing?.directory || resultRoot || ""; + const entries = listing ? listing.entries : extractGroundedPaths(output, resultRoot); + const directories = listing?.directories ?? []; + const files = listing?.files ?? entries; + return { + success: { + directory_tree_root: { + abs_path: rootPath, + children_dirs: directories.map((entry) => ({ + abs_path: stripTrailingPathSeparator(entry), + children_dirs: [], + children_files: [], + num_files: 0, + })), + children_files: files.map((entry) => ({ + name: listedEntryName(entry), + })), + num_files: files.length, + }, + }, + }; + } + case "mcp_result": { + if (error) + return { error: { error } }; + // opencode built-ins (read/write/grep/…) are advertised as MCP tools, so + // a read call returns through mcp_result. Scope the unwrap to toolName + // "read" so a non-read MCP tool whose output merely contains a + // ""-like block is never rewritten. + if (toolName === "grep" || toolName === "glob") { + return { + success: { + content: [{ text: { text: groundSearchOutput(output, resultRoot) } }], + is_error: false, + }, + }; + } + if (toolName !== "read") { + return { success: { content: [{ text: { text: output } }], is_error: false } }; + } + const listing = parseOpenCode2DirectoryListing(output, resultRoot); + if (listing) { + return { + success: { content: [{ text: { text: listing.text } }], is_error: false }, + }; + } + // Carry the truncation notice as its own content item: the file content + // item stays byte-exact, so it can still never be echoed into a write. + const notices = [ + readTruncationNotice(output, resultMetadata), + readLongLineTruncationNotice(output), + ].filter((notice) => !!notice); + return { + success: { + content: [ + { text: { text: unwrapReadOutput(output) } }, + ...notices.map((notice) => ({ text: { text: notice } })), + ], + is_error: false, + }, + }; + } + case "subagent_result": { + const task = parseOpenCodeTaskOutput(output); + if (error || task.state === "error") { + return { + error: { + ...(task.agentId ? { agent_id: task.agentId } : {}), + error: error ?? task.message ?? output, + }, + }; + } + return { + success: { + agent_id: task.agentId ?? "", + ...(task.message !== undefined ? { final_message: task.message } : {}), + tool_call_count: 0, + // OpenCode marks an asynchronous launch as state="running". Cursor's + // canonical USER_REQUEST enum value is 2; foreground/default is 0. + background_reason: task.state === "running" ? 2 : 0, + }, + }; + } + default: + // Unknown variant: best-effort success wrapper so the server sees a oneof. + if (error) + return { error: { error } }; + return { success: { content: output } }; + } +} +function parseOpenCodeTaskOutput(output) { + // Attribute order is not guaranteed; accept id/state in either order and + // ignore additional attributes OpenCode may emit on the open tag. + const open = /]*)>/i.exec(output); + if (!open) + return { message: output }; + const attrs = open[1]; + const agentId = /\bid="([^"]+)"/i.exec(attrs)?.[1]; + const state = /\bstate="(running|completed|error)"/i.exec(attrs)?.[1]; + if (!agentId || !state) + return { message: output }; + const tag = state === "error" ? "task_error" : "task_result"; + const body = new RegExp(`<${tag}>\\n?([\\s\\S]*?)\\n?`, "i").exec(output); + return { + agentId, + state, + message: body?.[1] ?? output, + }; +} +function extractPathTag(output) { + const m = output.match(/([^<]+)<\/path>/); + return m?.[1]; +} +/** + * Isolate opencode's read footer: the last blank-line-separated block before + * the closing ``. Scanning the whole envelope would let a file that + * merely *quotes* a footer (this repository's own docs and tests do) pass for a + * truncated read. The body can never contain a blank line — every source line + * is rendered as `N: …`, so even an empty line keeps its `N: ` prefix. + * + * Falls back to the full string when the envelope is absent, preserving + * behavior for non-standard read output. + */ +function readEnvelopeFooter(output) { + const close = output.lastIndexOf("\n"); + if (close === -1) + return output; + const start = output.lastIndexOf("\n\n", close); + if (start === -1) + return output; + return output.slice(start + 2, close); +} +/** Recover full-file metadata before unwrapReadOutput removes OpenCode's footer. */ +function parseOpenCodeReadMetadata(output, resultMetadata) { + const opencode2 = parseOpenCode2FileRead(output, resultMetadata); + if (opencode2) { + return { + ...(opencode2.startLine !== undefined ? { startLine: opencode2.startLine } : {}), + ...(opencode2.endLine !== undefined ? { endLine: opencode2.endLine } : {}), + ...(opencode2.totalLines !== undefined ? { totalLines: opencode2.totalLines } : {}), + ...(opencode2.nextOffset !== undefined ? { nextOffset: opencode2.nextOffset } : {}), + outputCapped: opencode2.outputCapped, + hostTruncated: opencode2.hostTruncated, + ...(opencode2.truncatedLines.length > 0 ? { truncatedLines: opencode2.truncatedLines } : {}), + }; + } + const footer = readEnvelopeFooter(output); + const showing = /Showing lines (\d+)-(\d+)(?: of (\d+))?\./.exec(footer); + if (showing) { + return { + startLine: Number(showing[1]), + endLine: Number(showing[2]), + ...(showing[3] ? { totalLines: Number(showing[3]) } : {}), + outputCapped: footer.includes("(Output capped at "), + }; + } + const complete = /\(End of file - total (\d+) lines?\)/.exec(footer); + if (complete) + return { totalLines: Number(complete[1]) }; + return {}; +} +/** + * OpenCode's numbered read envelope cannot represent the terminator after the + * final line. Cursor's legacy edit executor performs exact replacement against + * this content, so dropping a real LF/CRLF makes otherwise-valid edits fail and + * encourages a whole-file rewrite. Restore only that final terminator for an + * unbounded, confirmed-complete read; paged/capped reads stay untouched. + */ +function restoreCompleteReadTerminator(content, readPath, metadata, resultMetadata, workspaceRoot) { + if (!readPath + || metadata.totalLines === undefined + || metadata.startLine !== undefined + || metadata.endLine !== undefined + || num(resultMetadata?.offset) !== undefined + || num(resultMetadata?.limit) !== undefined + || content.endsWith("\n")) + return content; + const absolute = isAbsoluteToolPath(readPath) + ? readPath + : joinToolPath(workspaceRoot ?? process.cwd(), readPath); + let fd; + try { + fd = fs.openSync(absolute, "r"); + const size = fs.fstatSync(fd).size; + if (size === 0) + return content; + const tail = Buffer.alloc(Math.min(2, size)); + fs.readSync(fd, tail, 0, tail.length, size - tail.length); + if (tail.length >= 2 && tail[tail.length - 2] === 0x0d && tail[tail.length - 1] === 0x0a) { + return `${content}\r\n`; + } + if (tail[tail.length - 1] === 0x0a) + return `${content}\n`; + } + catch { + // The host read result remains authoritative when the local path cannot be + // inspected (deleted/raced/permission changed after the tool completed). + } + finally { + if (fd !== undefined) { + try { + fs.closeSync(fd); + } + catch { /* already closed */ } + } + } + return content; +} +/** + * OpenCode's `read` caps output at 50 KB (`tool/read.ts` MAX_BYTES = 50 * 1024) + * and cuts on a whole-line boundary, then appends + * "(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)". + * + * `unwrapReadOutput` strips that footer along with the envelope, deliberately: + * Cursor's model echoes whatever it is handed straight back into the next + * write, so anything left in the content stream can end up written into the + * file. But dropping the notice with no replacement is worse — the model then + * believes a capped read is the complete file, rewrites it from what it has, + * and everything past the cap is destroyed. + * + * Cursor's own CLI never truncates silently. Its local executor annotates every + * capped payload ("50KB limit reached", "[Showing last …KB of line N (50KB + * limit). Full output: …]") and returns structured truncation metadata. Mirror + * that, keeping the signal out of the file content itself so it still cannot be + * echoed into a write. + */ +function readTruncationSummary(output, resultMetadata) { + const meta = parseOpenCodeReadMetadata(output, resultMetadata); + if (meta.startLine === undefined || meta.endLine === undefined) + return undefined; + // A complete read reports "(End of file …)" and never reaches this shape. + if (meta.totalLines !== undefined && meta.endLine >= meta.totalLines && !meta.outputCapped) + return undefined; + return { + startLine: meta.startLine, + endLine: meta.endLine, + nextOffset: meta.nextOffset ?? meta.endLine + 1, + capped: meta.outputCapped === true, + ...(meta.totalLines !== undefined ? { totalLines: meta.totalLines } : {}), + }; +} +/** + * Model-visible replacement for the stripped footer. + * + * Only for reads the model did not ask to bound. A caller that passed an + * explicit offset/limit already knows it asked for a slice — warning there + * would cry wolf on every deliberate paged read. + */ +function readTruncationNotice(output, resultMetadata) { + const summary = readTruncationSummary(output, resultMetadata); + if (!summary) + return undefined; + const rangeRequested = num(resultMetadata?.offset) !== undefined || num(resultMetadata?.limit) !== undefined; + if (rangeRequested && !summary.capped) + return undefined; + const range = summary.totalLines !== undefined + ? `lines ${summary.startLine}-${summary.endLine} of ${summary.totalLines}` + : `lines ${summary.startLine}-${summary.endLine}`; + return (`[Partial read: the content above is ${range}` + + (summary.capped ? ", capped at the host's 50 KB output limit" : "") + + `. It is NOT the complete file. Continue with offset=${summary.nextOffset} before ` + + `acting on the whole file; writing the content above back would delete everything ` + + `after line ${summary.endLine}.]`); +} +function readLongLineTruncationNotice(output) { + const lines = parseOpenCodeReadMetadata(output).truncatedLines; + if (!lines || lines.length === 0) + return undefined; + const displayed = lines.slice(0, 8).join(", "); + const remainder = lines.length > 8 ? ` and ${lines.length - 8} more` : ""; + return (`[Partial read: OpenCode shortened ${lines.length === 1 ? "line" : "lines"} ` + + `${displayed}${remainder} to ${OPENCODE2_READ_MAX_LINE_CHARS} characters. ` + + "It is NOT the complete file. Use a byte-preserving read method to inspect the full " + + "line content before acting on the whole file; writing the content above back would lose data.]"); +} +/** agent.v1.PiTruncation for a capped OpenCode read. */ +function readTruncationMessage(output, content, resultMetadata) { + const summary = readTruncationSummary(output, resultMetadata); + const longLines = parseOpenCodeReadMetadata(output, resultMetadata).truncatedLines; + if (!summary && (!longLines || longLines.length === 0)) + return undefined; + const rangeRequested = num(resultMetadata?.offset) !== undefined || num(resultMetadata?.limit) !== undefined; + if (summary && rangeRequested && !summary.capped && (!longLines || longLines.length === 0)) + return undefined; + return { + truncated: true, + truncated_by: summary?.capped ? "bytes" : longLines?.length ? "characters" : "lines", + ...(summary?.totalLines !== undefined ? { total_lines: summary.totalLines } : {}), + output_lines: summary + ? Math.max(0, summary.endLine - summary.startLine + 1) + : countLines(content), + output_bytes: Buffer.byteLength(content, "utf8"), + ...(summary?.capped ? { max_bytes: OPENCODE_READ_MAX_BYTES } : {}), + }; +} +/** `tool/read.ts` MAX_BYTES — mirrored only to report the cap, never to apply it. */ +const OPENCODE_READ_MAX_BYTES = 50 * 1024; +function readRangeApplied(resultMetadata, totalLines) { + const offset = num(resultMetadata?.offset); + const limit = num(resultMetadata?.limit); + if (offset === undefined && limit === undefined) + return false; + if (totalLines === 0) + return false; + const startLine = offset ?? 1; + return startLine < 0 || startLine <= totalLines; +} +function readOutputTruncated(resultMetadata, outputMetadata, totalLines) { + if (outputMetadata.truncatedLines && outputMetadata.truncatedLines.length > 0) + return true; + if (outputMetadata.outputCapped) + return true; + if (outputMetadata.hostTruncated) { + const rangeRequested = num(resultMetadata?.offset) !== undefined || num(resultMetadata?.limit) !== undefined; + if (!rangeRequested) + return true; + } + const returnedEnd = outputMetadata.endLine; + if (returnedEnd === undefined || totalLines === 0) + return false; + const offset = num(resultMetadata?.offset); + const limit = num(resultMetadata?.limit); + const startLine = offset ?? 1; + if (startLine < 0) + return false; + const expectedEnd = limit === undefined + ? totalLines + : Math.min(totalLines, Math.max(1, startLine) + limit - 1); + return returnedEnd < expectedEnd; +} +function listedEntryName(entry) { + const trimmed = stripTrailingPathSeparator(entry); + const slash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\")); + return slash >= 0 ? trimmed.slice(slash + 1) : trimmed; +} +function stripTrailingPathSeparator(entry) { + return entry.endsWith("/") || entry.endsWith("\\") ? entry.slice(0, -1) : entry; +} +function readFileSize(filePath) { + if (!filePath) + return 0; + try { + return fs.statSync(filePath).size; + } + catch { + return 0; + } +} +function readFileLineCount(filePath) { + if (!filePath) + return undefined; + let fd; + try { + fd = fs.openSync(filePath, "r"); + const buffer = Buffer.allocUnsafe(64 * 1024); + let totalBytes = 0; + let lines = 1; + while (true) { + const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null); + if (bytesRead === 0) + break; + totalBytes += bytesRead; + for (let index = 0; index < bytesRead; index++) { + if (buffer[index] === 0x0a) + lines++; + } + } + return totalBytes === 0 ? 0 : lines; + } + catch { + return undefined; + } + finally { + if (fd !== undefined) { + try { + fs.closeSync(fd); + } + catch { + /* best effort */ + } + } + } +} +function extractPathLines(output) { + const lines = collapseSearchStatus(output).split("\n").map((l) => l.trim()).filter(Boolean); + // A folded `# dir/` header is not itself a path. Prefer absolute / relative + // path-looking lines; fall back to remaining non-status lines. + const usable = lines.filter((line) => !searchStatusMessage(line) && !/^#+\s+\S/.test(line)); + const paths = usable.filter((l) => l.startsWith("/") || l.startsWith("./") || l.includes("/")); + return (paths.length > 0 ? paths : usable).slice(0, 2000); +} +function countLines(s) { + if (!s) + return 0; + let n = 1; + for (let i = 0; i < s.length; i++) + if (s.charCodeAt(i) === 10) + n++; + return n; +} +function encodeShellStream(execId, executionTimeMs, shellStream) { + const clientMsg = { + id: execId, + shell_stream: shellStream, + }; + if (executionTimeMs !== undefined) { + clientMsg.local_execution_time_ms = executionTimeMs; + } + return encodeMessage("AgentClientMessage", { + exec_client_message: clientMsg, + }); +} +// ── Map a Cursor tool call to the opencode tool-call struct ── +export function buildToolCallPart(execMsg, sessionId) { + // Tag the toolCallId with the originating session's id so the result-bearing + // doStream call can disambiguate the Run stream — Cursor resets exec ids per + // stream, so two concurrent conversations would otherwise collide on `id`. + return { + toolCallId: `cursor_${sessionId}_${execMsg.id}`, + toolName: execMsg.toolName, + // LanguageModelV3ToolCall.input is a *stringified* JSON object. The AI SDK + // does `input.trim()` before JSON.parse; emitting a plain object crashes + // with "input.trim is not a function" and the model retries forever. + // OpenCode's processor then receives the parsed object from the SDK. + input: JSON.stringify(execMsg.args ?? {}), + }; +} +// ── Extract exec id from tool call id ── +export function parseExecIdFromToolCallId(toolCallId) { + // Format: cursor__. sessionId may itself contain + // underscores (e.g. UUIDs), so anchor on the trailing _. + const match = toolCallId.match(/^cursor_(.+)_(\d+)$/); + if (!match) + return undefined; + const execId = parseInt(match[2], 10); + if (!Number.isFinite(execId)) + return undefined; + return { sessionId: match[1], execId }; +} +// ── Unknown exec diagnostics ── +// +// Request/result field numbers are not universally identical (the Pi range is +// offset by one), so unknown variants must never receive a guessed empty reply. +// The pump uses this raw detector to report schema drift and fail the Run. +/** + * Find the exec variant field number from the raw (gunzipped) AgentServerMessage + * payload: peel field #2 (exec_server_message), then return the first + * message-typed field that isn't id(#1)/exec_id(#15)/span_context(#19). + * Returns undefined if there is no exec_server_message or no variant set. + */ +export function detectExecVariantField(agentServerPayload) { + const execBytes = readAllFields(agentServerPayload).find((f) => f.fn === 2 && f.wt === 2)?.bytes; + if (!execBytes) + return undefined; + for (const f of readAllFields(execBytes)) { + if (f.wt !== 2) + continue; // message-typed only (wire type 2) + if (f.fn === 1 || f.fn === 15 || f.fn === 19) + continue; + return f.fn; + } + return undefined; +} +/** + * Encode exec #10 request_context_result from a prebuilt RequestContext payload. + */ +export function buildRequestContextResult(execId, requestContext) { + traceRequestContextPaths(`buildRequestContextResult id=${execId}`, requestContext); + return encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + request_context_result: { + success: { + request_context: requestContext, + }, + }, + }, + }); +} +/** + * Answer Cursor's exec #36 MCP-state probe from the same descriptors advertised + * in RequestContext. OpenCode remains the executor; this only confirms that the + * provider's virtual MCP servers and their tools are available. + */ +export function buildMcpStateResult(execId, args, requestContext) { + const requested = new Set(Array.isArray(args.server_identifiers) + ? args.server_identifiers.filter((id) => typeof id === "string" && id.length > 0) + : []); + const fsOptions = recordValue(requestContext.mcp_file_system_options); + const nested = Array.isArray(fsOptions?.mcp_descriptors) + ? fsOptions.mcp_descriptors.map(recordValue).filter((d) => !!d) + : []; + const descriptors = nested.length > 0 ? nested : descriptorsFromFlatTools(requestContext.tools); + const flatTools = Array.isArray(requestContext.tools) + ? requestContext.tools.map(recordValue).filter((tool) => !!tool) + : []; + const servers = descriptors + .filter((descriptor) => { + const id = stringValue(descriptor.server_identifier); + return requested.size === 0 || (id !== undefined && requested.has(id)); + }) + .map((descriptor) => { + const serverIdentifier = stringValue(descriptor.server_identifier) ?? stringValue(descriptor.server_name) ?? ""; + const tools = Array.isArray(descriptor.tools) + ? descriptor.tools + .map(recordValue) + .filter((tool) => !!tool) + .map((tool) => mcpStateToolDefinition(serverIdentifier, tool, flatTools)) + : []; + return { + server_name: stringValue(descriptor.server_name) ?? serverIdentifier, + server_identifier: serverIdentifier, + tools, + }; + }); + return encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + mcp_state_exec_result: { success: { servers } }, + }, + }); +} +/** + * Exec #36 uses McpToolDefinition, not the narrower McpToolDescriptor used by + * RequestContext's filesystem/meta-tool catalogs. Rehydrate the full identity + * from RequestContext.tools so Cursor's native get_mcp_tools can correlate the + * discovered definition with the later provider_identifier/tool_name request. + */ +function mcpStateToolDefinition(serverIdentifier, descriptor, flatTools) { + const toolName = stringValue(descriptor.tool_name) ?? ""; + const advertised = flatTools.find((tool) => stringValue(tool.provider_identifier) === serverIdentifier + && stringValue(tool.tool_name) === toolName); + return { + name: stringValue(advertised?.name) ?? `${serverIdentifier}-${toolName}`, + description: stringValue(advertised?.description) ?? stringValue(descriptor.description) ?? "", + input_schema: advertised?.input_schema ?? descriptor.input_schema, + provider_identifier: serverIdentifier, + tool_name: toolName, + }; +} +/** + * Total fallback for Cursor's native MCP-resource exec channel (agent.v1 + * fields #17/#18, tasks/plans/fix-cursor-mcp-resource-exec.md). Under Option B + * `list_mcp_resources`/`read_mcp_resource` execute through the ordinary + * field-11 MCP path via the alias rules above, so any 17/18 that still reaches + * this provider is Cursor emitting the native variant unsolicited (its own + * client owns both executors unconditionally — there is no descriptor to + * un-advertise). Cursor's executors are total; mirror that here rather than + * guessing a generic success, which risks an endless heartbeat loop. + */ +export function buildListMcpResourcesFallback(execId) { + return encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + list_mcp_resources_exec_result: { success: { resources: [] } }, + }, + }); +} +/** See buildListMcpResourcesFallback. Always an `error`, even for `download_path`. */ +export function buildReadMcpResourceFallback(execId, server, uri) { + return encodeMessage("AgentClientMessage", { + exec_client_message: { + id: execId, + read_mcp_resource_exec_result: { + error: { uri, error: `Server "${server}" not found` }, + }, + }, + }); +} +function recordValue(value) { + return value && typeof value === "object" && !Array.isArray(value) + ? value + : undefined; +} +function descriptorsFromFlatTools(value) { + if (!Array.isArray(value)) + return []; + const byServer = new Map(); + for (const raw of value) { + const tool = recordValue(raw); + if (!tool) + continue; + const server = stringValue(tool.provider_identifier) ?? "opencode"; + const tools = byServer.get(server) ?? []; + tools.push({ + tool_name: stringValue(tool.tool_name) ?? stringValue(tool.name) ?? "", + description: stringValue(tool.description) ?? "", + input_schema: tool.input_schema, + }); + byServer.set(server, tools); + } + return [...byServer].map(([server, tools]) => ({ + server_name: server, + server_identifier: server, + tools, + })); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/workspace-grounding.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/workspace-grounding.d.ts new file mode 100644 index 00000000..18516534 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/workspace-grounding.d.ts @@ -0,0 +1,17 @@ +export type WorkspaceGroundingOptions = { + /** OpenCode 2 file tools take `path` and do not say it must be absolute. */ + requireAbsolutePathArg?: boolean; +}; +/** + * Attach the exact workspace root to a model-visible string. + * + * Idempotent: a second call does not stack another root line. The absolute-path + * sentence is added only when requested, and only once. + */ +export declare function appendWorkspaceRootGrounding(reason: string, workspaceRoot: string | undefined, options?: WorkspaceGroundingOptions): string; +/** + * Checkpointed Runs omit the system prompt, which is where the root normally + * lives. Put the same reminder on the live user message so a later turn cannot + * invent an absolute prefix before any tool result arrives. + */ +export declare function appendCheckpointUserGrounding(userText: string, workspaceRoot: string | undefined, options?: WorkspaceGroundingOptions): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/protocol/workspace-grounding.js b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/workspace-grounding.js new file mode 100644 index 00000000..d099df52 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/protocol/workspace-grounding.js @@ -0,0 +1,37 @@ +const ABSOLUTE_PATH_ARG = "OpenCode file tools take `path` as an absolute path under this root. Do not pass a project-relative path, and do not invent a different absolute prefix."; +/** + * Attach the exact workspace root to a model-visible string. + * + * Idempotent: a second call does not stack another root line. The absolute-path + * sentence is added only when requested, and only once. + */ +export function appendWorkspaceRootGrounding(reason, workspaceRoot, options) { + if (!workspaceRoot || !workspaceRoot.trim()) + return reason; + const pathSentence = options?.requireAbsolutePathArg ? ` ${ABSOLUTE_PATH_ARG}` : ""; + if (reason.includes("Workspace root:")) { + if (!pathSentence || reason.includes("take `path` as an absolute path")) + return reason; + return `${reason}${pathSentence}`; + } + const note = `Workspace root: ${JSON.stringify(workspaceRoot)}. ` + + "Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them." + + pathSentence; + if (!reason) + return note; + return `${reason}\n${note}`; +} +/** + * Checkpointed Runs omit the system prompt, which is where the root normally + * lives. Put the same reminder on the live user message so a later turn cannot + * invent an absolute prefix before any tool result arrives. + */ +export function appendCheckpointUserGrounding(userText, workspaceRoot, options) { + const note = appendWorkspaceRootGrounding("", workspaceRoot, options); + if (!note) + return userText; + if (userText.includes("Workspace root:")) { + return appendWorkspaceRootGrounding(userText, workspaceRoot, options); + } + return userText ? `${userText}\n\n${note}` : note; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/replay-safety.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/replay-safety.d.ts new file mode 100644 index 00000000..b876a601 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/replay-safety.d.ts @@ -0,0 +1,23 @@ +import type { CursorProviderError } from "./errors.js"; +export type ReplayBarrierReason = "visible-text" | "visible-reasoning" | "display-tool-lifecycle" | "non-control-exec" | "stateful-interaction" | "unknown-or-malformed-frame"; +export declare class AttemptReplaySafety { + private readonly sessionId; + private barrierReason; + constructor(sessionId: string); + markBarrier(reason: ReplayBarrierReason): void; + applyTo(failure: CursorProviderError): CursorProviderError; +} +export type DecodedReplayFrame = { + interactionUpdate?: Record; + exec?: Record; + kv?: Record; + execControl?: Record; + interactionQuery?: Record; + checkpointBytes?: Uint8Array; +}; +export type ReplayFrameAnalysis = { + semanticProgress: boolean; + barrier?: ReplayBarrierReason; +}; +/** Classify one decoded server frame without performing any protocol side effects. */ +export declare function analyzeReplayFrame(payload: Uint8Array, decoded: DecodedReplayFrame): ReplayFrameAnalysis; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/replay-safety.js b/references/upstream-plugins/cursor-opencode-provider/dist/replay-safety.js new file mode 100644 index 00000000..8b6b7790 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/replay-safety.js @@ -0,0 +1,126 @@ +import { trace } from "./debug.js"; +import { readAllFieldsStrict } from "./protocol/struct.js"; +export class AttemptReplaySafety { + sessionId; + barrierReason; + constructor(sessionId) { + this.sessionId = sessionId; + } + markBarrier(reason) { + if (this.barrierReason) + return; + this.barrierReason = reason; + trace(`replay barrier: reason=${reason} sessionId=${this.sessionId}`); + } + applyTo(failure) { + failure.replaySafe = this.barrierReason === undefined && failure.replaySafe; + if (this.barrierReason) { + trace(`replay suppressed: reason=${this.barrierReason} sessionId=${this.sessionId}`); + } + return failure; + } +} +const INTERACTION_UPDATE_FIELDS = new Set([1, 2, 3, 4, 7, 13, 14, 16, 17]); +const INTERACTION_QUERY_FIELDS = new Set([2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14]); +const TOP_LEVEL_FIELDS = new Set([1, 2, 3, 4, 5, 7]); +function nestedFields(topLevel, field) { + if (topLevel?.fn !== field || topLevel.wt !== 2 || !topLevel.bytes) + return []; + return readAllFieldsStrict(topLevel.bytes) ?? []; +} +function analyzeExecWire(topLevel) { + const variants = nestedFields(topLevel, 2) + .filter((field) => ![1, 15, 19].includes(field.fn)); + const exactVariant = (field) => variants.length === 1 && variants[0].fn === field && variants[0].wt === 2; + return { + exactRequestContext: exactVariant(10), + exactMcpState: exactVariant(36), + }; +} +function validKvWire(topLevel) { + const variants = nestedFields(topLevel, 4).filter((field) => field.fn !== 1); + const variant = variants.length === 1 ? variants[0] : undefined; + const args = variant?.wt === 2 && variant.bytes + ? (readAllFieldsStrict(variant.bytes) ?? []) + : []; + const blobIds = args.filter((field) => field.fn === 1 && field.wt === 2 && (field.bytes?.length ?? 0) > 0); + return !!variant + && [2, 3].includes(variant.fn) + && variant.wt === 2 + && blobIds.length === 1 + && args.every((field) => field.wt === 2 && (field.fn === 1 || (variant.fn === 3 && field.fn === 2))); +} +function validInteractionUpdateWire(topLevel, decoded) { + if (!decoded) + return true; + const fields = nestedFields(topLevel, 1); + const update = fields.length === 1 ? fields[0] : undefined; + if (!update || update.wt !== 2 || !INTERACTION_UPDATE_FIELDS.has(update.fn)) + return false; + if (![1, 4].includes(update.fn)) + return true; + const delta = update.bytes ? (readAllFieldsStrict(update.bytes) ?? []) : []; + return delta.length === 1 && delta[0].fn === 1 && delta[0].wt === 2; +} +function validInteractionQueryWire(topLevel, decoded) { + if (!decoded) + return true; + const fields = nestedFields(topLevel, 7); + const ids = fields.filter((field) => field.fn === 1); + const variants = fields.filter((field) => field.fn !== 1); + return ids.length <= 1 + && ids.every((field) => field.wt === 0) + && variants.length === 1 + && variants[0].wt === 2 + && INTERACTION_QUERY_FIELDS.has(variants[0].fn); +} +function decodedMatchesWire(topLevel, decoded) { + return !((topLevel?.fn === 1 && !decoded.interactionUpdate) + || (topLevel?.fn === 2 && !decoded.exec) + || (topLevel?.fn === 4 && !decoded.kv) + || (topLevel?.fn === 5 && !decoded.execControl) + || (topLevel?.fn === 7 && !decoded.interactionQuery)); +} +function hasSemanticProgress(decoded) { + const update = decoded.interactionUpdate; + const text = update?.text_delta?.text; + const thinking = update?.thinking_delta?.text; + return (typeof text === "string" && text.length > 0) + || (typeof thinking === "string" && thinking.length > 0) + || !!update?.turn_ended + || !!update?.tool_call_started + || !!update?.tool_call_completed + || !!decoded.exec + || !!decoded.kv + || !!decoded.execControl + || !!decoded.interactionQuery + || !!decoded.checkpointBytes?.length; +} +/** Classify one decoded server frame without performing any protocol side effects. */ +export function analyzeReplayFrame(payload, decoded) { + const topLevelFields = readAllFieldsStrict(payload) ?? []; + const topLevel = topLevelFields.length === 1 ? topLevelFields[0] : undefined; + const exec = analyzeExecWire(topLevel); + const validKv = validKvWire(topLevel); + const malformed = !topLevel + || topLevel.wt !== 2 + || !TOP_LEVEL_FIELDS.has(topLevel.fn) + || !validInteractionUpdateWire(topLevel, decoded.interactionUpdate) + || !validInteractionQueryWire(topLevel, decoded.interactionQuery) + || !decodedMatchesWire(topLevel, decoded) + || !!decoded.execControl; + let barrier; + if (decoded.interactionUpdate?.tool_call_started || decoded.interactionUpdate?.tool_call_completed) { + barrier = "display-tool-lifecycle"; + } + else if (malformed || (topLevel.fn === 4 && !validKv)) { + barrier = "unknown-or-malformed-frame"; + } + else if (topLevel.fn === 2 && !exec.exactRequestContext && !exec.exactMcpState) { + barrier = "non-control-exec"; + } + return { + semanticProgress: hasSemanticProgress(decoded), + barrier, + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/session-directory.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/session-directory.d.ts new file mode 100644 index 00000000..41463fe8 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/session-directory.d.ts @@ -0,0 +1,36 @@ +/** + * Session-scoped workspace directory for the OpenCode 2.0 plugin. + * + * The classic plugin learns the project directory from `input.directory`, + * supplied once per invocation by the 1.x host. OpenCode 2.0 runs a single + * daemon across many projects, so `process.cwd()` captured at sdk-creation + * time is wrong for any session but the one open when the daemon started. + * + * The 2.0 runtime names a session directory in two places: + * - request header `x-opencode-directory` (per-request; preferred in the LM) + * - `ctx.session.get()` → flat `info.directory`, or legacy + * `info.location.directory`, reachable from `session.hook("context")` + * + * This module records the session-get value by id so the language model can + * fall back when the header is absent, same mechanism as `compaction-marker.ts`. + * + * Bounded so a long-lived server cannot accumulate ids for dead sessions. + */ +export declare function markSessionDirectory(sessionID: string, directory: string | undefined): void; +export declare function getSessionDirectory(sessionID: string | undefined): string | undefined; +export declare function clearSessionDirectories(): void; +/** + * Active session workspace directory from OpenCode 2.0 request headers. + * Values may be URI-encoded. + */ +export declare function opencodeDirectoryHeader(headers: Record | undefined): string | undefined; +/** + * Resolve the workspace root for a model turn. + * Prefer the per-request header, then the session mark, then static options/cwd. + */ +export declare function resolveSessionWorkspaceRoot(input: { + sessionKey?: string; + headers?: Record; + workspaceRoot?: string; + cwd?: string; +}): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/session-directory.js b/references/upstream-plugins/cursor-opencode-provider/dist/session-directory.js new file mode 100644 index 00000000..d5261143 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/session-directory.js @@ -0,0 +1,67 @@ +/** + * Session-scoped workspace directory for the OpenCode 2.0 plugin. + * + * The classic plugin learns the project directory from `input.directory`, + * supplied once per invocation by the 1.x host. OpenCode 2.0 runs a single + * daemon across many projects, so `process.cwd()` captured at sdk-creation + * time is wrong for any session but the one open when the daemon started. + * + * The 2.0 runtime names a session directory in two places: + * - request header `x-opencode-directory` (per-request; preferred in the LM) + * - `ctx.session.get()` → flat `info.directory`, or legacy + * `info.location.directory`, reachable from `session.hook("context")` + * + * This module records the session-get value by id so the language model can + * fall back when the header is absent, same mechanism as `compaction-marker.ts`. + * + * Bounded so a long-lived server cannot accumulate ids for dead sessions. + */ +import path from "node:path"; +const MAX_TRACKED_SESSIONS = 256; +const sessionDirectories = new Map(); +export function markSessionDirectory(sessionID, directory) { + if (!sessionID || !directory) + return; + // Re-insert to keep insertion order meaningful for the eviction below. + sessionDirectories.delete(sessionID); + sessionDirectories.set(sessionID, directory); + while (sessionDirectories.size > MAX_TRACKED_SESSIONS) { + const oldest = sessionDirectories.keys().next().value; + if (oldest === undefined) + break; + sessionDirectories.delete(oldest); + } +} +export function getSessionDirectory(sessionID) { + return typeof sessionID === "string" ? sessionDirectories.get(sessionID) : undefined; +} +export function clearSessionDirectories() { + sessionDirectories.clear(); +} +/** + * Active session workspace directory from OpenCode 2.0 request headers. + * Values may be URI-encoded. + */ +export function opencodeDirectoryHeader(headers) { + if (!headers) + return undefined; + const raw = headers["x-opencode-directory"] ?? headers["X-Opencode-Directory"]; + if (typeof raw !== "string" || raw.trim().length === 0) + return undefined; + const trimmed = raw.trim(); + try { + return decodeURIComponent(trimmed); + } + catch { + return trimmed; + } +} +/** + * Resolve the workspace root for a model turn. + * Prefer the per-request header, then the session mark, then static options/cwd. + */ +export function resolveSessionWorkspaceRoot(input) { + return path.resolve(opencodeDirectoryHeader(input.headers) ?? + getSessionDirectory(input.sessionKey) ?? + (input.workspaceRoot || input.cwd || process.cwd())); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/session.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/session.d.ts new file mode 100644 index 00000000..720713c1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/session.d.ts @@ -0,0 +1,321 @@ +import type { BidiStream } from "./transport/connect.js"; +import { type CursorProviderError } from "./errors.js"; +import { type SessionActivitySource } from "./activity.js"; +import type { HostSubagentCatalog, HostToolDialect, OpencodeToolDef, ToolAliasRegistry } from "./protocol/tools.js"; +import type { CursorConversationTokenDetails } from "./protocol/token-details.js"; +export type Frame = { + flags: number; + payload: Uint8Array; +}; +export type CursorContinuationOptions = { + semanticIdleMs?: number; + /** @deprecated Use semanticIdleMs. Kept as a strict alias for compatibility. */ + softHealthMs?: number; + /** Pending-tool inactivity window, renewed by OpenCode session progress. */ + hardCapMs?: number; + heartbeatMs?: number; +}; +export type CursorContinuationPolicy = { + semanticIdleMs: number; + hardCapMs: number; + heartbeatMs: number; +}; +export declare const DEFAULT_CONTINUATION_POLICY: Readonly; +export declare function resolveContinuationPolicy(options: CursorContinuationOptions | undefined): CursorContinuationPolicy; +export type PendingExecState = "pending" | "claimed" | "delivered"; +/** + * A held-open Run stream. Cursor drives the agentic loop server-side and + * expects tool results on the SAME bidi stream. opencode, by contrast, owns + * its tool loop: it calls `doStream`, gets a tool-call, executes it, then calls + * `doStream` again with the result. We bridge the two by keeping the Run stream + * (and its single frames iterator) alive in a session across `doStream` calls, + * keyed by the exec ids we are waiting results for. + */ +export type PendingExec = { + /** ExecClientMessage result field to reply with (matches the request variant). */ + resultField: string; + state: PendingExecState; + registeredAt: number; + hardDeadlineAt: number; + /** + * Resolved opencode tool name (read/write/grep/…). Used on continuation so + * mcp_result can unwrap read envelopes even if the prompt omits toolName. + */ + toolName?: string; + /** Original request fields required by a typed result message. */ + resultMetadata?: Record; + /** + * True when this pending entry was synthesized from a Cursor display-only + * tool_call_* frame (no ExecServerMessage). Continuation must not write an + * exec result back to Cursor — just clear pending and keep pumping. + */ + bridged?: boolean; +}; +export type ContinuationTerminalReason = "hard-cap-expired" | "remote-clean-close" | "remote-error" | "result-write-failed" | "ambiguous-partial-write" | "heartbeat-write-failed" | "reply-write-failed" | "process-disposed" | "superseded-by-new-run" | "open-session-cap-exceeded"; +export type SessionCloseReason = ContinuationTerminalReason | "ordinary-cleanup" | "turn-ended" | "initial-write-failed"; +export type ContinuationClaim = { + session: CursorSession; + execId: number; + pending: PendingExec; +}; +export type ContinuationClassification = { + kind: "deliverable"; + session: CursorSession; + pending: PendingExec; +} | { + kind: "duplicate"; + reason: "in-flight" | "delivered"; +} | { + kind: "terminal"; + reason: ContinuationTerminalReason; +} | { + kind: "missing"; + reason: "missing-process-local-state"; +}; +export type DeliveryOutcome = { + kind: "delivered"; + framesWritten: number; +} | { + kind: "duplicate"; + reason: "in-flight" | "delivered"; + framesWritten: 0; +} | { + kind: "terminal"; + reason: ContinuationTerminalReason; + framesWritten: number; +} | { + kind: "missing"; + reason: "missing-process-local-state"; + framesWritten: 0; +}; +export type CursorSession = { + /** + * Stable per-Run-stream id (distinct from Cursor's own conversation_id). + * Tags toolCallIds so two concurrent Run streams with overlapping exec ids + * (Cursor resets them per stream) can't cross-deliver results. + */ + sessionId: string; + /** + * Cursor conversation_id for this Run — used to store/echo + * conversation_checkpoint_update (CLI parity). + */ + conversationId: string; + /** Cache root owning this conversation's durable restart state. */ + cacheDir?: string; + /** Latest eligible checkpoint emitted by this Run attempt. */ + resumeCheckpoint?: Uint8Array; + /** Last checkpoint-derived context snapshot, independent of retry eligibility. */ + tokenDetails?: CursorConversationTokenDetails; + /** True only after this Run receives a checkpoint containing token details. */ + tokenDetailsFresh?: boolean; + /** + * Run-lifetime cache evidence. Cursor exposes only aggregate cache counters at + * TurnEnded, so retain the inputs and protocol activity needed to explain + * whether a low ratio came from a cold/rebased prefix, context growth, or a + * multi-step agent Run. + */ + cacheDiagnostics?: { + sessionKey?: string; + conversationId: string; + conversationGroupId?: string; + modelId?: string; + priorTokenDetails?: CursorConversationTokenDetails; + startedWithCheckpoint: boolean; + requestContextReused: boolean; + requestContextHash: string; + systemPromptHash?: string; + checkpointUpdates: number; + tokenDetailUpdates: number; + pumpPasses: number; + stepStarts: number; + stepCompletes: number; + displayToolCalls: number; + execRequests: number; + createPlanInTurn?: boolean; + switchModeInTurn?: boolean; + }; + /** OpenCode session whose own or descendant activity renews tool leases. */ + openCodeSessionId?: string; + /** Host primary agent whose prompt/permissions this Run was seeded with. */ + hostAgent?: string; + /** Stable host-system + provider-guidance identity for restart validation. */ + stableSystemPromptHash?: string; + /** Completed compaction must rebase once before resuming a normal agent. */ + postCompactionRebase?: boolean; + /** Last real host catalog, retained only as a lifecycle-turn fallback. */ + toolCatalog?: OpencodeToolDef[]; + stream: BidiStream; + frames: AsyncIterator; + pending: Map; + /** + * Cursor display tool calls (tool_call_started) awaiting either an exec or a + * tool_call_completed. Keyed by call_id. Cleared when exec handles the call + * or when we bridge the completed display call into an OpenCode tool-call. + */ + displayToolCalls: Map>; + /** + * Last full todo snapshot successfully mirrored into OpenCode `todowrite`. + * Used to apply Cursor `merge: true` display updates that omit the final + * completed list so host todos still receive a replace-all snapshot. + */ + mirroredTodos?: Array>; + /** + * Legacy edit calls whose authoritative exec path is still in progress. + * Cursor implements these as read -> whole-file write; retaining the path + * lets the pump expose the final mutation to OpenCode as a targeted edit. + */ + editToolCalls?: Map; + /** Monotonic synthetic exec ids for bridged (display-only) OpenCode tool calls. */ + nextBridgedExecId: number; + /** KV blob store: blob_id (hex) → data, for Cursor's out-of-band payload channel. */ + blobs: Map; + /** McpToolDefinition list advertised this turn — echoed into the request_context reply. */ + toolDescriptors: Array>; + /** Cursor-facing web alias → exact executable host tool for this Run. */ + toolAliases?: ToolAliasRegistry; + /** OpenCode 1.x `filePath`/`bash` vs 2.x `path`/`shell`, from advertised schemas. */ + hostToolDialect?: HostToolDialect; + /** Spawnable host agents extracted from this turn's Task/Actor definition. */ + subagentCatalog?: HostSubagentCatalog; + /** Full RequestContext for exec #10 replies. */ + requestContext: Record; + /** + * False when OpenCode passed no tools (compaction/summary) or toolChoice "none". + * Cursor may still fire native Grep/etc.; we must refuse those on the Run + * stream instead of emitting tool-call parts OpenCode will reject. + */ + allowTools: boolean; + /** + * Host-enabled tool names for *this* Run (what actually arrived). May be a + * subset of the epoch advertisement when plan denies edit — advertise fully + * for cache stability, but refuse exec outside this set. + */ + permittedToolNames?: ReadonlySet; + /** + * Best-effort held-Run activity counters used only for diagnostics. Displayed + * AI SDK usage comes from checkpoint occupancy (tool steps) and TurnEnded + * (stop); this estimate is never sent to OpenCode. + */ + usageEstimate: { + inputTokens: number; + outputTokens: number; + cacheRead: number; + cacheWrite: number; + reasoningTokens: number; + }; + /** + * True while a doStream pull() is actively reading this session's frames. + * Prevents a late cancel/abort from a prior ReadableStream from destroying + * the Run connection after tool results were delivered (pending cleared) but + * Cursor is still generating. + */ + pumpActive: boolean; + /** The active pull owner; stale cancel callbacks cannot affect a newer pump. */ + pumpOwner: symbol | null; + heartbeat: ReturnType | null; + heartbeatCancel: (() => void) | null; + hardDeadlineTimer: ReturnType | null; + semanticDeadlineCancel: (() => void) | null; + terminalUnsubscribe: (() => void) | null; + deferredTerminalReason: "remote-clean-close" | "remote-error" | null; + policy: CursorContinuationPolicy; + createdAt: number; + lastInboundAt: number; + lastHeartbeatWriteAt: number; + semanticDeadlineAt: number; + closeError: CursorProviderError | null; + closed: boolean; + reopenWithUserMessage?: (text: string) => Promise; +}; +type SessionManagerOptions = { + now?: () => number; + setTimer?: (callback: () => void, delayMs: number) => ReturnType; + clearTimer?: (timer: ReturnType) => void; + activitySource?: SessionActivitySource; + tombstoneTtlMs?: number; + tombstoneLimit?: number; + maxOpenSessions?: number; +}; +export declare class SessionManager { + private byExecId; + private sessions; + private byOpenCodeSessionId; + private tombstones; + private readonly now; + private readonly setTimer; + private readonly clearTimer; + private readonly activitySource; + private readonly tombstoneTtlMs; + private readonly tombstoneLimit; + private readonly maxOpenSessions; + constructor(options?: SessionManagerOptions); + registerSession(session: CursorSession): void; + /** Live open Run for this OpenCode session id, if any. */ + findOpenByOpenCodeSessionId(openCodeSessionId: string | undefined): CursorSession | undefined; + isActivelyPumping(session: CursorSession): boolean; + /** + * Clear display-only bridged pendings that do not require a Cursor exec write. + * Used when the host starts a fresh user turn instead of returning the bridged + * tool result (e.g. human `continue` while a todowrite mirror is outstanding). + * + * @returns number of bridged pendings settled + */ + settleBridgedPending(session: CursorSession): number; + private isPumping; + /** + * Force-close the oldest idle open session(s) once registration exceeds the + * cap. Never closes a session with a live pull() mid-await, or one still + * holding pending execs (continuation / fresh-turn cancel+drain owns those). + * An unclosable backlog of genuinely active sessions just means the cap + * can't be enforced until one finishes. + */ + private enforceOpenSessionCap; + recordSemanticProgress(session: CursorSession, at?: number): void; + recordHeartbeatWrite(session: CursorSession): void; + /** Register that `session` is awaiting a result for `execId`. */ + registerPending(execId: number, session: CursorSession, resultField: string, toolName?: string, bridged?: boolean, resultMetadata?: Record): void; + /** The pending exec info for an id on a specific session, if still awaiting it. */ + pendingFor(sessionId: string, execId: number): PendingExec | undefined; + classify(sessionId: string, execId: number): ContinuationClassification; + claim(sessionId: string, execId: number): ContinuationClaim | ContinuationClassification; + deliverClaim(claim: ContinuationClaim, frames: readonly Uint8Array[]): DeliveryOutcome; + /** Find the live session awaiting one of the given exec ids. */ + findByExecIds(sessionId: string, execIds: number[]): CursorSession | undefined; + /** Mark an exec id as resolved (its result has been delivered). */ + resolve(sessionId: string, execId: number): void; + beginPump(session: CursorSession, owner: symbol): void; + isPumpOwner(session: CursorSession, owner: symbol): boolean; + endPump(session: CursorSession, owner: symbol): boolean; + /** + * Swap the live bidi Run while a pump is still pulling frames. + * Callers must cancel the session heartbeat and wait for the old stream's + * write chain before this, so an in-flight heartbeat cannot close the session. + */ + replaceStream(session: CursorSession, next: BidiStream): void; + private subscribeTerminal; + private key; + close(session: CursorSession, reason?: SessionCloseReason, error?: CursorProviderError): void; + /** + * Close only if nothing is awaiting a tool result AND no pull() is actively + * pumping this session. OpenCode aborts each doStream after finishReason + * "tool-calls"; that abort must NOT tear down the Cursor Run stream. + * Equally, a late cancel from the previous ReadableStream must not destroy + * the session once the continuation has cleared pending and resumed pumping. + * Returns true if the session was closed. + */ + closeUnlessPending(session: CursorSession): boolean; + dispose(): void; + sweepHardDeadlines(): void; + private onStreamTerminal; + private refreshHardDeadline; + private hardDeadlineExpired; + private scheduleHardDeadline; + private getTombstone; + private putTombstone; + private isTerminalReason; +} +export declare const sessionManager: SessionManager; +export {}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/session.js b/references/upstream-plugins/cursor-opencode-provider/dist/session.js new file mode 100644 index 00000000..98e0cef1 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/session.js @@ -0,0 +1,586 @@ +import { trace } from "./debug.js"; +import { CursorProtocolError } from "./errors.js"; +import { sessionActivity } from "./activity.js"; +export const DEFAULT_CONTINUATION_POLICY = { + semanticIdleMs: 120_000, + hardCapMs: 600_000, + heartbeatMs: 5_000, +}; +const MAX_TIMER_MS = 2_147_483_647; +const DEFAULT_TOMBSTONE_TTL_MS = 15 * 60_000; +const DEFAULT_TOMBSTONE_LIMIT = 1_024; +// Well below Cursor's server-side concurrent-Run ceiling per HTTP/2 connection, +// so a caller that keeps abandoning Runs instead of delivering continuations +// gets corrected here instead of piling up open streams until Cursor's server +// force-closes the whole shared connection. +const DEFAULT_MAX_OPEN_SESSIONS = 24; +function positiveInteger(name, value, fallback) { + const resolved = value === undefined ? fallback : value; + if (typeof resolved !== "number" || + !Number.isSafeInteger(resolved) || + resolved <= 0 || + resolved > MAX_TIMER_MS) { + throw new CursorProtocolError(`Cursor continuation ${name} must be a positive integer no greater than ${MAX_TIMER_MS}`); + } + return resolved; +} +export function resolveContinuationPolicy(options) { + if (options !== undefined && (options === null || typeof options !== "object" || Array.isArray(options))) { + throw new CursorProtocolError("Cursor continuation options must be an object"); + } + for (const key of Object.keys(options ?? {})) { + if (!["heartbeatMs", "semanticIdleMs", "softHealthMs", "hardCapMs"].includes(key)) { + throw new CursorProtocolError(`Unknown Cursor continuation option: ${key}`); + } + } + if (options?.semanticIdleMs !== undefined && + options.softHealthMs !== undefined && + options.semanticIdleMs !== options.softHealthMs) { + throw new CursorProtocolError("Cursor continuation semanticIdleMs and deprecated softHealthMs must match when both are set"); + } + const heartbeatMs = positiveInteger("heartbeatMs", options?.heartbeatMs, DEFAULT_CONTINUATION_POLICY.heartbeatMs); + const semanticIdleMs = positiveInteger("semanticIdleMs", options?.semanticIdleMs ?? options?.softHealthMs, DEFAULT_CONTINUATION_POLICY.semanticIdleMs); + const hardCapMs = positiveInteger("hardCapMs", options?.hardCapMs, DEFAULT_CONTINUATION_POLICY.hardCapMs); + if (heartbeatMs >= semanticIdleMs) { + throw new CursorProtocolError("Cursor continuation heartbeatMs must be less than semanticIdleMs"); + } + if (semanticIdleMs > hardCapMs) { + throw new CursorProtocolError("Cursor continuation semanticIdleMs must be no greater than hardCapMs"); + } + return { heartbeatMs, semanticIdleMs, hardCapMs }; +} +export class SessionManager { + // Composite key `${sessionId}:${execId}` → owning session. Composite keying + // means two Run streams that both register an execId of 1 (Cursor resets + // counters per stream) coexist instead of overwriting each other. + byExecId = new Map(); + sessions = new Set(); + // Most recently registered open session per OpenCode session id. A second + // registration for the same id means the caller started a fresh Run instead + // of continuing the held-open one — the prior entry is stale and superseded. + byOpenCodeSessionId = new Map(); + tombstones = new Map(); + now; + setTimer; + clearTimer; + activitySource; + tombstoneTtlMs; + tombstoneLimit; + maxOpenSessions; + constructor(options = {}) { + this.now = options.now ?? Date.now; + this.setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs)); + this.clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer)); + this.activitySource = options.activitySource ?? sessionActivity; + this.tombstoneTtlMs = positiveInteger("tombstoneTtlMs", options.tombstoneTtlMs, DEFAULT_TOMBSTONE_TTL_MS); + this.tombstoneLimit = positiveInteger("tombstoneLimit", options.tombstoneLimit, DEFAULT_TOMBSTONE_LIMIT); + this.maxOpenSessions = positiveInteger("maxOpenSessions", options.maxOpenSessions, DEFAULT_MAX_OPEN_SESSIONS); + } + registerSession(session) { + if (session.closed) + throw new CursorProtocolError("Cannot register a closed Cursor session"); + if (this.sessions.has(session)) + return; + session.closed ??= false; + session.closeError ??= null; + session.pumpOwner ??= null; + session.heartbeatCancel ??= null; + session.hardDeadlineTimer ??= null; + session.semanticDeadlineCancel ??= null; + session.terminalUnsubscribe ??= null; + session.deferredTerminalReason ??= null; + session.policy ??= { ...DEFAULT_CONTINUATION_POLICY }; + session.createdAt ??= this.now(); + session.lastInboundAt ??= this.now(); + session.lastHeartbeatWriteAt ??= this.now(); + session.semanticDeadlineAt ??= this.now() + session.policy.semanticIdleMs; + this.sessions.add(session); + if (session.openCodeSessionId) { + const prior = this.byOpenCodeSessionId.get(session.openCodeSessionId); + if (prior && prior !== session && !prior.closed) { + if (this.isPumping(prior)) { + // A live pull() is mid-await on this stream (nextFrameWithSemanticDeadline + // or the raw frame iterator). Forcing close() here would reject that await + // and propagate a real error to whatever still holds the ReadableStream — + // exactly the kind of self-inflicted failure this cap is meant to prevent. + // Let it finish or expire on its own (hard deadline / heartbeat failure). + trace(`sessionManager.registerSession: openCodeSessionId=${session.openCodeSessionId} ` + + `has a still-pumping prior session ${prior.sessionId} (pending=${prior.pending.size}) ` + + `— leaving it to finish or expire naturally instead of interrupting it`); + } + else { + // Display-only bridged pendings do not need a Cursor exec write — settle + // them first so a todowrite-only prior can still be superseded cleanly. + const settled = this.settleBridgedPending(prior); + if (settled > 0) { + trace(`sessionManager.registerSession: settled ${settled} bridged pending on prior ` + + `session ${prior.sessionId} before supersede`); + } + if (prior.pending.size > 0) { + // Real execs still outstanding — preparePriorSessionForFreshTurn should + // have cancelled + drained. Do not mid-exec supersede. + trace(`sessionManager.registerSession: openCodeSessionId=${session.openCodeSessionId} ` + + `has prior session ${prior.sessionId} with ${prior.pending.size} still-pending ` + + `exec(s) — refusing supersede; leaving held Run for cancel/drain/continuation`); + } + else { + trace(`sessionManager.registerSession: superseding stale session ${prior.sessionId} ` + + `(openCodeSessionId=${session.openCodeSessionId}, pending=${prior.pending.size}) ` + + `with new session ${session.sessionId}`); + this.close(prior, "superseded-by-new-run"); + } + } + } + this.byOpenCodeSessionId.set(session.openCodeSessionId, session); + } + this.enforceOpenSessionCap(session); + this.subscribeTerminal(session); + } + /** Live open Run for this OpenCode session id, if any. */ + findOpenByOpenCodeSessionId(openCodeSessionId) { + if (!openCodeSessionId) + return undefined; + const session = this.byOpenCodeSessionId.get(openCodeSessionId); + return session && !session.closed ? session : undefined; + } + isActivelyPumping(session) { + return this.isPumping(session); + } + /** + * Clear display-only bridged pendings that do not require a Cursor exec write. + * Used when the host starts a fresh user turn instead of returning the bridged + * tool result (e.g. human `continue` while a todowrite mirror is outstanding). + * + * @returns number of bridged pendings settled + */ + settleBridgedPending(session) { + if (session.closed) + return 0; + let settled = 0; + for (const [execId, pending] of [...session.pending.entries()]) { + if (!pending.bridged) + continue; + if (pending.state === "claimed") + continue; + const key = this.key(session.sessionId, execId); + this.putTombstone(key, "delivered"); + session.pending.delete(execId); + this.byExecId.delete(key); + settled++; + } + if (settled > 0) { + if (session.pending.size === 0) + this.recordSemanticProgress(session); + this.scheduleHardDeadline(session); + } + return settled; + } + isPumping(session) { + return session.pumpOwner != null || session.pumpActive; + } + /** + * Force-close the oldest idle open session(s) once registration exceeds the + * cap. Never closes a session with a live pull() mid-await, or one still + * holding pending execs (continuation / fresh-turn cancel+drain owns those). + * An unclosable backlog of genuinely active sessions just means the cap + * can't be enforced until one finishes. + */ + enforceOpenSessionCap(justRegistered) { + while (this.sessions.size > this.maxOpenSessions) { + let oldest; + for (const candidate of this.sessions) { + if (candidate === justRegistered || this.isPumping(candidate)) + continue; + // Held-open Runs with pending tools must not be cap-evicted — that is + // mid-exec supersede by another name and throws away the prefix. + if (candidate.pending.size > 0) + continue; + if (!oldest || candidate.createdAt < oldest.createdAt) + oldest = candidate; + } + if (!oldest) { + trace(`sessionManager.registerSession: open session cap exceeded ` + + `(${this.sessions.size} > ${this.maxOpenSessions}) but no idle session is ` + + `available to close — all remaining sessions are pumping or held-pending`); + return; + } + trace(`sessionManager.registerSession: open session cap exceeded ` + + `(${this.sessions.size} > ${this.maxOpenSessions}), force-closing oldest idle session ` + + `${oldest.sessionId} (openCodeSessionId=${oldest.openCodeSessionId ?? "-"}, ` + + `ageMs=${this.now() - oldest.createdAt}, pending=${oldest.pending.size})`); + this.close(oldest, "open-session-cap-exceeded"); + } + } + recordSemanticProgress(session, at) { + if (session.closed) + return; + const now = at ?? this.now(); + session.lastInboundAt = now; + session.semanticDeadlineAt = now + session.policy.semanticIdleMs; + } + recordHeartbeatWrite(session) { + if (!session.closed) + session.lastHeartbeatWriteAt = this.now(); + } + /** Register that `session` is awaiting a result for `execId`. */ + registerPending(execId, session, resultField, toolName, bridged = false, resultMetadata) { + this.registerSession(session); + if (session.closed) + throw new CursorProtocolError("Cannot register a pending exec on a closed Cursor session"); + const now = this.now(); + const key = this.key(session.sessionId, execId); + this.tombstones.delete(key); + session.pending.set(execId, { + resultField, + toolName, + bridged, + resultMetadata, + state: "pending", + registeredAt: now, + hardDeadlineAt: now + session.policy.hardCapMs, + }); + this.byExecId.set(key, session); + this.scheduleHardDeadline(session); + } + /** The pending exec info for an id on a specific session, if still awaiting it. */ + pendingFor(sessionId, execId) { + return this.byExecId.get(this.key(sessionId, execId))?.pending.get(execId); + } + classify(sessionId, execId) { + const key = this.key(sessionId, execId); + const session = this.byExecId.get(key); + if (session) { + const pending = session.pending.get(execId); + const legacyExpiresAt = session.expiresAt; + if (!pending || + session.closed || + session.stream.isClosed() || + (typeof legacyExpiresAt === "number" && this.now() >= legacyExpiresAt)) { + this.byExecId.delete(key); + if (!session.closed) + this.close(session, "remote-clean-close"); + } + else if (this.hardDeadlineExpired(session, pending)) { + this.close(session, "hard-cap-expired"); + } + else if (pending.state === "pending") { + return { kind: "deliverable", session, pending }; + } + else if (pending.state === "claimed") { + return { kind: "duplicate", reason: "in-flight" }; + } + else { + return { kind: "duplicate", reason: "delivered" }; + } + } + const tombstone = this.getTombstone(key); + if (!tombstone) + return { kind: "missing", reason: "missing-process-local-state" }; + if (tombstone.reason === "delivered") + return { kind: "duplicate", reason: "delivered" }; + return { kind: "terminal", reason: tombstone.reason }; + } + claim(sessionId, execId) { + const classification = this.classify(sessionId, execId); + if (classification.kind !== "deliverable") + return classification; + classification.pending.state = "claimed"; + return { + session: classification.session, + execId, + pending: classification.pending, + }; + } + deliverClaim(claim, frames) { + const { session, execId, pending } = claim; + const key = this.key(session.sessionId, execId); + if (session.closed || + this.byExecId.get(key) !== session || + session.pending.get(execId) !== pending) { + const current = this.classify(session.sessionId, execId); + if (current.kind === "duplicate") + return { ...current, framesWritten: 0 }; + if (current.kind === "terminal") + return { ...current, framesWritten: 0 }; + return { kind: "missing", reason: "missing-process-local-state", framesWritten: 0 }; + } + if (pending.state !== "claimed") { + return { + kind: "duplicate", + reason: pending.state === "delivered" ? "delivered" : "in-flight", + framesWritten: 0, + }; + } + if (this.hardDeadlineExpired(session, pending)) { + this.close(session, "hard-cap-expired"); + return { kind: "terminal", reason: "hard-cap-expired", framesWritten: 0 }; + } + let framesWritten = 0; + try { + if (!pending.bridged && frames.length === 0) { + throw new CursorProtocolError("No result frames were produced"); + } + for (const frame of frames) { + session.stream.write(frame); + framesWritten++; + } + } + catch { + const reason = framesWritten === 0 ? "result-write-failed" : "ambiguous-partial-write"; + this.close(session, reason); + return { kind: "terminal", reason, framesWritten }; + } + pending.state = "delivered"; + this.putTombstone(key, "delivered"); + session.pending.delete(execId); + this.byExecId.delete(key); + if (session.pending.size === 0) + this.recordSemanticProgress(session); + this.scheduleHardDeadline(session); + return { kind: "delivered", framesWritten }; + } + /** Find the live session awaiting one of the given exec ids. */ + findByExecIds(sessionId, execIds) { + for (const id of execIds) { + const classification = this.classify(sessionId, id); + if (classification.kind === "deliverable") + return classification.session; + } + return undefined; + } + /** Mark an exec id as resolved (its result has been delivered). */ + resolve(sessionId, execId) { + const k = this.key(sessionId, execId); + const s = this.byExecId.get(k); + if (s) { + s.pending.delete(execId); + this.putTombstone(k, "delivered"); + this.scheduleHardDeadline(s); + } + this.byExecId.delete(k); + } + beginPump(session, owner) { + this.registerSession(session); + if (session.pumpOwner && session.pumpOwner !== owner) { + throw new CursorProtocolError("Cursor session already has an active pump"); + } + session.pumpOwner = owner; + session.pumpActive = true; + } + isPumpOwner(session, owner) { + return !session.closed && session.pumpOwner === owner; + } + endPump(session, owner) { + if (session.pumpOwner !== owner) + return false; + session.pumpOwner = null; + session.pumpActive = false; + const deferred = session.deferredTerminalReason; + session.deferredTerminalReason = null; + if (deferred) + this.close(session, deferred); + return true; + } + /** + * Swap the live bidi Run while a pump is still pulling frames. + * Callers must cancel the session heartbeat and wait for the old stream's + * write chain before this, so an in-flight heartbeat cannot close the session. + */ + replaceStream(session, next) { + if (session.closed) + throw new CursorProtocolError("Cannot replace stream on a closed Cursor session"); + session.heartbeatCancel?.(); + session.terminalUnsubscribe?.(); + session.terminalUnsubscribe = null; + session.deferredTerminalReason = null; + const old = session.stream; + session.stream = next; + session.frames = next.frames()[Symbol.asyncIterator](); + this.subscribeTerminal(session); + try { + old.destroy(); + } + catch { /* already closed */ } + } + subscribeTerminal(session) { + const boundStream = session.stream; + const unsubscribe = boundStream.onTerminal?.((event) => { + if (session.stream !== boundStream) + return; + this.onStreamTerminal(session, event); + }) ?? (() => { }); + if (session.closed) + unsubscribe(); + else + session.terminalUnsubscribe = unsubscribe; + } + key(sessionId, execId) { + return `${sessionId}:${execId}`; + } + close(session, reason = "ordinary-cleanup", error) { + if (session.closed) + return; + session.closed = true; + session.closeError = error ?? session.closeError ?? null; + trace(`sessionManager.close: reason=${reason} pendingCount=${session.pending.size} blobs=${session.blobs.size}`); + if (session.heartbeatCancel) + session.heartbeatCancel(); + else if (session.heartbeat) + clearInterval(session.heartbeat); + session.heartbeat = null; + session.heartbeatCancel = null; + if (session.hardDeadlineTimer) + this.clearTimer(session.hardDeadlineTimer); + session.hardDeadlineTimer = null; + session.semanticDeadlineCancel?.(); + session.semanticDeadlineCancel = null; + session.terminalUnsubscribe?.(); + session.terminalUnsubscribe = null; + session.deferredTerminalReason = null; + for (const id of session.pending.keys()) { + const key = this.key(session.sessionId, id); + this.byExecId.delete(key); + if (this.isTerminalReason(reason)) + this.putTombstone(key, reason); + } + session.pending.clear(); + session.pumpOwner = null; + session.pumpActive = false; + session.displayToolCalls?.clear(); + session.mirroredTodos = undefined; + session.blobs?.clear(); + this.sessions.delete(session); + if (session.openCodeSessionId && + this.byOpenCodeSessionId.get(session.openCodeSessionId) === session) { + this.byOpenCodeSessionId.delete(session.openCodeSessionId); + } + try { + session.stream.destroy(); + } + catch { /* already closed */ } + } + /** + * Close only if nothing is awaiting a tool result AND no pull() is actively + * pumping this session. OpenCode aborts each doStream after finishReason + * "tool-calls"; that abort must NOT tear down the Cursor Run stream. + * Equally, a late cancel from the previous ReadableStream must not destroy + * the session once the continuation has cleared pending and resumed pumping. + * Returns true if the session was closed. + */ + closeUnlessPending(session) { + if (session.closed) + return true; + const pumpActive = this.isPumping(session); + if (session.pending.size > 0 || pumpActive) { + trace(`sessionManager.closeUnlessPending: KEEP open pendingCount=${session.pending.size} pumpActive=${pumpActive}`); + return false; + } + this.close(session, "ordinary-cleanup"); + return true; + } + dispose() { + for (const session of [...this.sessions]) + this.close(session, "process-disposed"); + this.byExecId.clear(); + } + sweepHardDeadlines() { + for (const session of [...this.sessions]) { + if (!session.closed && + [...session.pending.values()].some((pending) => this.hardDeadlineExpired(session, pending))) { + this.close(session, "hard-cap-expired"); + } + } + } + onStreamTerminal(session, event) { + if (event.kind === "local-close" || session.closed) + return; + const reason = event.kind === "remote-error" ? "remote-error" : "remote-clean-close"; + if (event.kind === "remote-error") + session.closeError = event.error; + if (session.pumpOwner !== null) { + session.deferredTerminalReason = reason; + return; + } + this.close(session, reason, event.kind === "remote-error" ? event.error : undefined); + } + refreshHardDeadline(session, pending) { + if (!session.openCodeSessionId) + return pending.hardDeadlineAt; + const activityAt = this.activitySource.lastActivityAt(session.openCodeSessionId); + if (activityAt === undefined || activityAt <= pending.registeredAt) + return pending.hardDeadlineAt; + const renewedDeadline = activityAt + session.policy.hardCapMs; + if (renewedDeadline > pending.hardDeadlineAt) { + pending.hardDeadlineAt = renewedDeadline; + trace("continuation lease renewed from OpenCode session activity"); + } + return pending.hardDeadlineAt; + } + hardDeadlineExpired(session, pending) { + return this.now() >= this.refreshHardDeadline(session, pending); + } + scheduleHardDeadline(session) { + if (session.hardDeadlineTimer) + this.clearTimer(session.hardDeadlineTimer); + session.hardDeadlineTimer = null; + if (session.closed || session.pending.size === 0) + return; + const earliest = Math.min(...[...session.pending.values()].map((pending) => this.refreshHardDeadline(session, pending))); + const delayMs = Math.max(0, earliest - this.now()); + const timer = this.setTimer(() => { + session.hardDeadlineTimer = null; + if (session.closed) + return; + if ([...session.pending.values()].some((pending) => this.hardDeadlineExpired(session, pending))) { + this.close(session, "hard-cap-expired"); + } + else { + this.scheduleHardDeadline(session); + } + }, delayMs); + session.hardDeadlineTimer = timer; + const unref = timer.unref; + if (typeof unref === "function") + unref.call(timer); + } + getTombstone(key) { + const tombstone = this.tombstones.get(key); + if (!tombstone) + return undefined; + if (this.now() >= tombstone.expiresAt) { + this.tombstones.delete(key); + return undefined; + } + return tombstone; + } + putTombstone(key, reason) { + this.tombstones.delete(key); + this.tombstones.set(key, { + reason, + expiresAt: this.now() + this.tombstoneTtlMs, + }); + while (this.tombstones.size > this.tombstoneLimit) { + const oldest = this.tombstones.keys().next().value; + if (oldest === undefined) + break; + this.tombstones.delete(oldest); + } + } + isTerminalReason(reason) { + return !["ordinary-cleanup", "turn-ended", "initial-write-failed"].includes(reason); + } +} +export const sessionManager = new SessionManager(); +function installProcessCleanup() { + const dispose = () => { + try { + sessionManager.dispose(); + } + catch { + /* ignore */ + } + }; + process.once("exit", dispose); + process.once("beforeExit", dispose); +} +installProcessCleanup(); diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/shared.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/shared.d.ts new file mode 100644 index 00000000..bfe9b30e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/shared.d.ts @@ -0,0 +1,26 @@ +export declare const CURSOR_API_HOST = "api2.cursor.sh"; +export declare const CURSOR_WEBSITE_HOST = "cursor.com"; +export declare const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0"; +export declare const CURSOR_PROVIDER_ID = "cursor"; +/** Private provider option injected by the OpenCode plugin for summary turns. */ +export declare const CURSOR_COMPACTION_OPTION = "opencodeCompaction"; +/** OpenCode host rebuilt its history without a provider compaction turn. */ +export declare const CURSOR_HISTORY_REWRITE_OPTION = "opencodeHistoryRewrite"; +/** Current OpenCode primary agent; used to invalidate incompatible checkpoints. */ +export declare const CURSOR_HOST_AGENT_OPTION = "opencodeHostAgent"; +export declare const TOKEN_EXPIRY_THRESHOLD_S = 300; +export declare const RUN_PATH = "/agent.v1.AgentService/Run"; +export declare const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels"; +export declare const SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig"; +export declare const MODEL_CACHE_FILE = "cursor-models.json"; +/** Bumped when the on-disk model cache shape/semantics change (forces refetch). */ +export declare const MODEL_CACHE_SCHEMA_VERSION = 3; +export declare const MODEL_CACHE_TTL_MS = 86400000; +export declare const CONVERSATION_CACHE_DIR = "cursor-conversations"; +export declare const CONVERSATION_CACHE_SCHEMA_VERSION = 4; +export declare const CONVERSATION_CACHE_TTL_MS = 86400000; +export declare const VERSION_CACHE_FILE = "cursor-client-version.json"; +export declare const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto"; +export declare const CONNECT_PROTOCOL_VERSION = "1"; +export type ReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max"; +export type ContextOption = "200k" | "272k" | "300k" | "1m"; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/shared.js b/references/upstream-plugins/cursor-opencode-provider/dist/shared.js new file mode 100644 index 00000000..44563b15 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/shared.js @@ -0,0 +1,24 @@ +export const CURSOR_API_HOST = "api2.cursor.sh"; +export const CURSOR_WEBSITE_HOST = "cursor.com"; +export const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0"; +export const CURSOR_PROVIDER_ID = "cursor"; +/** Private provider option injected by the OpenCode plugin for summary turns. */ +export const CURSOR_COMPACTION_OPTION = "opencodeCompaction"; +/** OpenCode host rebuilt its history without a provider compaction turn. */ +export const CURSOR_HISTORY_REWRITE_OPTION = "opencodeHistoryRewrite"; +/** Current OpenCode primary agent; used to invalidate incompatible checkpoints. */ +export const CURSOR_HOST_AGENT_OPTION = "opencodeHostAgent"; +export const TOKEN_EXPIRY_THRESHOLD_S = 300; +export const RUN_PATH = "/agent.v1.AgentService/Run"; +export const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels"; +export const SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig"; +export const MODEL_CACHE_FILE = "cursor-models.json"; +/** Bumped when the on-disk model cache shape/semantics change (forces refetch). */ +export const MODEL_CACHE_SCHEMA_VERSION = 3; +export const MODEL_CACHE_TTL_MS = 86_400_000; +export const CONVERSATION_CACHE_DIR = "cursor-conversations"; +export const CONVERSATION_CACHE_SCHEMA_VERSION = 4; +export const CONVERSATION_CACHE_TTL_MS = 86_400_000; +export const VERSION_CACHE_FILE = "cursor-client-version.json"; +export const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto"; +export const CONNECT_PROTOCOL_VERSION = "1"; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/shell-timeout.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/shell-timeout.d.ts new file mode 100644 index 00000000..ba70c10e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/shell-timeout.d.ts @@ -0,0 +1,119 @@ +/** Cursor agent.v1 TimeoutBehavior enum values. */ +export declare const CURSOR_TIMEOUT_CANCEL = 1; +export declare const CURSOR_TIMEOUT_BACKGROUND = 2; +/** Private marker for Cursor `background_shell_spawn_args` detach wrappers. */ +export declare const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__"; +export type CursorShellPolicy = { + command: string; + workingDirectory: string; + timeoutMs: number; + timeoutBehavior: number; + hardTimeoutMs?: number; + /** Immediate nohup detach for Cursor `background_shell_spawn_args`. */ + backgroundSpawn?: boolean; +}; +export type CursorShellOutcome = { + kind: "exit"; + code: number; +} | { + kind: "timeout"; + timeoutMs: number; +} | { + kind: "backgrounded"; + shellId: number; + pid: number; + command: string; + workingDirectory: string; + msToWait: number; + reason: 1; +}; +/** Track OpenCode's configured shell from the classic config hook. */ +export declare function setCursorShellPath(shell: string | undefined): void; +/** + * Mirror the relevant part of OpenCode Shell.acceptable(): fish/nu are denied, + * then POSIX falls back to bash when installed and `/bin/sh` otherwise. + */ +export declare function resolveCursorShellKind(shell?: string | undefined): "bash" | "zsh" | "sh" | "dash" | "other"; +export declare function shellPolicyFromMetadata(metadata: Record | undefined): CursorShellPolicy | undefined; +/** Register a Cursor shell request before OpenCode executes its emitted tool call. */ +export declare function registerCursorShellCall(toolCallId: string, metadata: Record | undefined): void; +/** + * F11 / soft-background helper. + * + * Run a Cursor soft-background command for its foreground window, then leave + * it detached (`nohup`) if still alive. The sentinel is removed by the after + * hook before OpenCode stores/renders the result. + * + * This approximates Cursor's TIMEOUT_BACKGROUND semantics through OpenCode's + * foreground-only bash tool. Residual: after OpenCode returns, the child (and + * optional hard-timeout watchdog) may still be running; this provider does not + * reap leftover processes — cleanup is left to the user / OS. + */ +export declare function buildSoftBackgroundCommand(policy: CursorShellPolicy): string; +/** + * F11 / background_shell_spawn_args helper. + * + * OpenCode's bash tool is foreground-only. Detach the requested command inside + * that one foreground call (`nohup … &`) and print a private marker containing + * the spawned PID and log path. With stdin and all output redirected, the host + * shell can return immediately instead of retaining OpenCode's tool pipe. + * + * Residual: the detached child is not reaped by this provider after OpenCode + * completes the tool call; cleanup is left to the user / OS. + */ +export declare function buildBackgroundShellCommand(command: string): string; +/** + * Prepare OpenCode Bash args before execution when Cursor requested wrapping. + * + * bash/zsh source the shell.env injector, so the original command remains in + * OpenCode's permission/UI state. sh/dash ignore those startup variables; for + * them, use a short `exec wrapper.sh` command that contains no user payload. + * + * background_shell_spawn may already contain the inline non-plugin fallback. + * The classic hook replaces it with the original command (bash/zsh) or the + * shorter wrapper-file command (sh/dash), avoiding duplicate execution. + */ +export declare function prepareCursorShellArgs(toolCallId: string, args: Record, options?: { + preferWrapperCommand?: boolean; +}): void; +/** Restore the model-facing command in OpenCode's completed tool title. */ +export declare function cursorShellOriginalCommand(toolCallId: string): string | undefined; +/** Drop injector temp files for a finished/abandoned Cursor shell call. */ +export declare function releaseCursorShellEnv(toolCallId: string): void; +/** + * Env vars for OpenCode's shell.env hook. bash/zsh execute the injector; the + * same materialized wrapper backs the direct-command sh/dash fallback. + */ +export declare function cursorShellEnvForCall(toolCallId: string | undefined): Record | undefined; +/** + * OpenCode 2.0 `shell.create.before` has no tool-call id. Correlate the pending + * wrap by command + working directory when possible, then fall back to the + * original command for hosts that omit the directory. + */ +export declare function cursorShellEnvForCommand(command: string | undefined, workingDirectory?: string): Record | undefined; +/** + * Strip private wrapper sentinels / OpenCode timeout envelopes for display. + * Does not record outcomes — use {@link captureCursorShellResult} for that. + * + * OpenCode 2.0 `Tool.Result.output` is structured (an object for shell), not + * a string. Non-string input is returned unchanged so the 2.0 after-hook can + * pass structured output through safely. + */ +export declare function sanitizeCursorShellDisplayOutput(output: string, policy?: CursorShellPolicy): string; +/** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */ +export declare function sanitizeRegisteredCursorShellOutput(toolCallId: string, output: string): string; +/** + * Capture Bash completion in the classic plugin's after hook. Returns the + * sanitized output that OpenCode should store and render. + * + * Guards non-string output (OpenCode 2.0 structured `Tool.Result.output`) + * by returning it unchanged. + */ +export declare function captureCursorShellResult(toolCallId: string, output: string, metadata?: Record): string; +/** Consume the structured result, with an inline fallback when no plugin hook ran. */ +export declare function consumeCursorShellResult(toolCallId: string, output: string): { + output: string; + outcome?: CursorShellOutcome; +}; +/** Test/process cleanup. */ +export declare function resetCursorShellCalls(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/shell-timeout.js b/references/upstream-plugins/cursor-opencode-provider/dist/shell-timeout.js new file mode 100644 index 00000000..e7ae50da --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/shell-timeout.js @@ -0,0 +1,516 @@ +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, delimiter, join } from "node:path"; +/** Cursor agent.v1 TimeoutBehavior enum values. */ +export const CURSOR_TIMEOUT_CANCEL = 1; +export const CURSOR_TIMEOUT_BACKGROUND = 2; +const MAX_TRACKED_SHELL_CALLS = 512; +const OPENCODE_TIMEOUT_GRACE_MS = 15_000; +const POLL_INTERVAL_MS = 100; +const BACKGROUND_MARKER = "__CURSOR_SHELL_BACKGROUND__"; +const EXIT_MARKER = "__CURSOR_SHELL_EXIT__"; +const TIMEOUT_MARKER = "__CURSOR_SHELL_TIMEOUT__"; +/** Private marker for Cursor `background_shell_spawn_args` detach wrappers. */ +export const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__"; +const policies = new Map(); +const outcomes = new Map(); +/** callIDs that need shell.env injectors or a direct-command fallback. */ +const pendingEnvWraps = new Set(); +const activeEnvWraps = new Map(); +let configuredShell; +/** Track OpenCode's configured shell from the classic config hook. */ +export function setCursorShellPath(shell) { + configuredShell = shell?.trim() || undefined; +} +function executableOnPath(name) { + for (const dir of (process.env.PATH ?? "").split(delimiter)) { + if (dir && existsSync(join(dir, name))) + return true; + } + return false; +} +/** + * Mirror the relevant part of OpenCode Shell.acceptable(): fish/nu are denied, + * then POSIX falls back to bash when installed and `/bin/sh` otherwise. + */ +export function resolveCursorShellKind(shell = configuredShell ?? process.env.SHELL) { + let name = shell ? basename(shell).toLowerCase().replace(/\.exe$/, "") : ""; + if (name === "fish" || name === "nu" || !name) { + name = executableOnPath("bash") ? "bash" : "sh"; + } + if (name === "bash" || name === "zsh" || name === "sh" || name === "dash") + return name; + return "other"; +} +function remember(map, key, value, onEvict) { + map.delete(key); + map.set(key, value); + while (map.size > MAX_TRACKED_SHELL_CALLS) { + const oldest = map.keys().next().value; + if (!oldest) + break; + const evicted = map.get(oldest); + map.delete(oldest); + if (evicted !== undefined && onEvict) + onEvict(evicted); + } +} +function finiteNonNegative(value) { + const n = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(n) || n < 0) + return undefined; + return Math.floor(n); +} +export function shellPolicyFromMetadata(metadata) { + if (!metadata) + return undefined; + if (metadata.background_shell_spawn === true) { + return { + command: typeof metadata.command === "string" ? metadata.command : "", + workingDirectory: typeof metadata.working_directory === "string" ? metadata.working_directory : "", + timeoutMs: 0, + timeoutBehavior: 0, + backgroundSpawn: true, + }; + } + if (metadata.shell_stream !== true) + return undefined; + const timeoutMs = finiteNonNegative(metadata.timeout_ms) ?? 30_000; + const timeoutBehavior = finiteNonNegative(metadata.timeout_behavior) ?? 0; + const hardTimeoutMs = finiteNonNegative(metadata.hard_timeout_ms); + return { + command: typeof metadata.command === "string" ? metadata.command : "", + workingDirectory: typeof metadata.working_directory === "string" ? metadata.working_directory : "", + timeoutMs, + timeoutBehavior, + ...(hardTimeoutMs !== undefined && hardTimeoutMs > 0 ? { hardTimeoutMs } : {}), + }; +} +/** Register a Cursor shell request before OpenCode executes its emitted tool call. */ +export function registerCursorShellCall(toolCallId, metadata) { + const policy = shellPolicyFromMetadata(metadata); + if (!policy || !toolCallId.startsWith("cursor_")) + return; + remember(policies, toolCallId, policy); +} +function shellQuote(value) { + return `'${value.replace(/'/g, `'\\''`)}'`; +} +/** + * F11 / soft-background helper. + * + * Run a Cursor soft-background command for its foreground window, then leave + * it detached (`nohup`) if still alive. The sentinel is removed by the after + * hook before OpenCode stores/renders the result. + * + * This approximates Cursor's TIMEOUT_BACKGROUND semantics through OpenCode's + * foreground-only bash tool. Residual: after OpenCode returns, the child (and + * optional hard-timeout watchdog) may still be running; this provider does not + * reap leftover processes — cleanup is left to the user / OS. + */ +export function buildSoftBackgroundCommand(policy) { + const polls = Math.ceil(policy.timeoutMs / POLL_INTERVAL_MS); + const hardPolls = policy.hardTimeoutMs !== undefined + ? Math.max(1, Math.ceil(policy.hardTimeoutMs / POLL_INTERVAL_MS)) + : undefined; + const lines = [ + 'cursor_shell_log="$(mktemp "${TMPDIR:-/tmp}/cursor-opencode-shell.XXXXXX")" || exit 1', + `nohup sh -c ${shellQuote(policy.command)} >"$cursor_shell_log" 2>&1 /dev/null; do sleep ${POLL_INTERVAL_MS / 1000}; cursor_hard_poll=$((cursor_hard_poll + 1)); done; if kill -0 "$2" 2>/dev/null; then printf timeout >"$3"; kill -TERM "$2" 2>/dev/null; sleep 3; kill -KILL "$2" 2>/dev/null; fi' cursor-shell-watchdog ${hardPolls} "$cursor_shell_pid" "$cursor_shell_status" >/dev/null 2>&1 /dev/null || true", 'if [ -n "$cursor_shell_watchdog_pid" ]; then disown "$cursor_shell_watchdog_pid" 2>/dev/null || true; fi', 'disown "$cursor_shell_pid" 2>/dev/null || true', "cursor_shell_poll=0", `while [ "$cursor_shell_poll" -lt ${polls} ] && kill -0 "$cursor_shell_pid" 2>/dev/null; do`, ` sleep ${POLL_INTERVAL_MS / 1000}`, " cursor_shell_poll=$((cursor_shell_poll + 1))", "done", 'if kill -0 "$cursor_shell_pid" 2>/dev/null; then', ' cat "$cursor_shell_log"', ` printf '\n${BACKGROUND_MARKER}%s:%s\n' "$cursor_shell_pid" "$cursor_shell_log"`, " exit 0", "fi", 'wait "$cursor_shell_pid" 2>/dev/null', "cursor_shell_code=$?", 'cat "$cursor_shell_log"', 'if [ -n "$cursor_shell_status" ] && [ "$(cat "$cursor_shell_status" 2>/dev/null)" = timeout ]; then', + // Reap the watchdog before printing the private marker so any residual + // shell diagnostics cannot trail the sentinel. + ' if [ -n "$cursor_shell_watchdog_pid" ]; then kill "$cursor_shell_watchdog_pid" 2>/dev/null || true; wait "$cursor_shell_watchdog_pid" 2>/dev/null || true; fi', ` printf '\n${TIMEOUT_MARKER}%s\n' ${policy.hardTimeoutMs ?? policy.timeoutMs}`, "else", ' if [ -n "$cursor_shell_watchdog_pid" ]; then kill "$cursor_shell_watchdog_pid" 2>/dev/null || true; wait "$cursor_shell_watchdog_pid" 2>/dev/null || true; fi', ` printf '\n${EXIT_MARKER}%s\n' "$cursor_shell_code"`, "fi", 'rm -f -- "$cursor_shell_log"', 'if [ -n "$cursor_shell_status" ]; then rm -f -- "$cursor_shell_status"; fi'); + return lines.join("\n"); +} +/** + * F11 / background_shell_spawn_args helper. + * + * OpenCode's bash tool is foreground-only. Detach the requested command inside + * that one foreground call (`nohup … &`) and print a private marker containing + * the spawned PID and log path. With stdin and all output redirected, the host + * shell can return immediately instead of retaining OpenCode's tool pipe. + * + * Residual: the detached child is not reaped by this provider after OpenCode + * completes the tool call; cleanup is left to the user / OS. + */ +export function buildBackgroundShellCommand(command) { + return [ + 'bg_log="$(mktemp "${TMPDIR:-/tmp}/cursor-opencode-bg.XXXXXX")" || exit 1', + `nohup sh -c ${shellQuote(command)} >"$bg_log" 2>&1 ` body runs. + const injector = [ + "unset BASH_ENV ZDOTDIR ENV CURSOR_OPENCODE_WRAP_ACTIVE", + `exec /bin/sh ${shellQuote(wrapperPath)}`, + "", + ].join("\n"); + writeFileSync(bashEnvPath, injector, { mode: 0o600 }); + writeFileSync(zshenvPath, injector, { mode: 0o600 }); + return { + wrapperPath, + env: { + BASH_ENV: bashEnvPath, + ZDOTDIR: dir, + }, + cleanup: () => { + try { + rmSync(dir, { recursive: true, force: true }); + } + catch { + // best-effort temp cleanup + } + }, + }; +} +function ensureShellEnvWrap(toolCallId, policy) { + const existing = activeEnvWraps.get(toolCallId); + if (existing) + return existing; + const wrapperBody = wrapperBodyForPolicy(policy); + if (!wrapperBody) + return undefined; + const wrap = writeShellEnvInjector(wrapperBody); + remember(activeEnvWraps, toolCallId, wrap, (evicted) => evicted.cleanup()); + return wrap; +} +/** + * Prepare OpenCode Bash args before execution when Cursor requested wrapping. + * + * bash/zsh source the shell.env injector, so the original command remains in + * OpenCode's permission/UI state. sh/dash ignore those startup variables; for + * them, use a short `exec wrapper.sh` command that contains no user payload. + * + * background_shell_spawn may already contain the inline non-plugin fallback. + * The classic hook replaces it with the original command (bash/zsh) or the + * shorter wrapper-file command (sh/dash), avoiding duplicate execution. + */ +export function prepareCursorShellArgs(toolCallId, args, options = {}) { + const policy = policies.get(toolCallId); + if (!policy) + return; + if (!policy.backgroundSpawn && policy.timeoutBehavior !== CURSOR_TIMEOUT_BACKGROUND) + return; + pendingEnvWraps.add(toolCallId); + if (!policy.backgroundSpawn) { + // The wrapper returns just after Cursor's foreground window. OpenCode's own + // timeout is only an outer safety net and must not win the race. + args.timeout = Math.max(OPENCODE_TIMEOUT_GRACE_MS, policy.timeoutMs + OPENCODE_TIMEOUT_GRACE_MS); + } + const shellKind = resolveCursorShellKind(); + // OpenCode 2.0 has no `shell.env` hook, so bash/zsh cannot be wrapped by + // sourcing an injector. Callers there opt into the wrapper-file command — + // the same mechanism sh/dash already use — which needs no env injection. + const envInjectable = !options.preferWrapperCommand && (shellKind === "bash" || shellKind === "zsh"); + if (process.platform === "win32" || envInjectable) { + // Native Windows PowerShell/cmd wrapping remains unsupported; do not emit + // a POSIX /bin/sh command there. Git Bash still uses the env path above. + args.command = policy.command; + return; + } + const wrap = ensureShellEnvWrap(toolCallId, policy); + if (wrap) + args.command = `exec /bin/sh ${shellQuote(wrap.wrapperPath)}`; +} +/** Restore the model-facing command in OpenCode's completed tool title. */ +export function cursorShellOriginalCommand(toolCallId) { + return policies.get(toolCallId)?.command || undefined; +} +/** Drop injector temp files for a finished/abandoned Cursor shell call. */ +export function releaseCursorShellEnv(toolCallId) { + pendingEnvWraps.delete(toolCallId); + const active = activeEnvWraps.get(toolCallId); + if (!active) + return; + activeEnvWraps.delete(toolCallId); + active.cleanup(); +} +/** + * Env vars for OpenCode's shell.env hook. bash/zsh execute the injector; the + * same materialized wrapper backs the direct-command sh/dash fallback. + */ +export function cursorShellEnvForCall(toolCallId) { + if (typeof toolCallId !== "string" || !toolCallId || !pendingEnvWraps.has(toolCallId)) + return undefined; + const policy = policies.get(toolCallId); + if (!policy) + return undefined; + const wrap = ensureShellEnvWrap(toolCallId, policy); + if (!wrap) + return undefined; + pendingEnvWraps.delete(toolCallId); + return wrap.env; +} +/** + * OpenCode 2.0 `shell.create.before` has no tool-call id. Correlate the pending + * wrap by command + working directory when possible, then fall back to the + * original command for hosts that omit the directory. + */ +export function cursorShellEnvForCommand(command, workingDirectory) { + if (typeof command !== "string" || !command) + return undefined; + if (workingDirectory) { + for (const [id, policy] of policies) { + if (!pendingEnvWraps.has(id)) + continue; + if (policy.command === command && policy.workingDirectory === workingDirectory) { + return cursorShellEnvForCall(id); + } + } + } + for (const [id, policy] of policies) { + if (!pendingEnvWraps.has(id)) + continue; + if (policy.command === command) + return cursorShellEnvForCall(id); + } + return undefined; +} +function withoutMarker(output, index) { + let clean = output.slice(0, index).replace(/[\t ]+$/gm, "").replace(/\n{2,}$/, "\n"); + if (clean.trim() === "" || clean.trim() === "(no output)") + clean = ""; + return clean; +} +/** + * OpenCode only stores/renders text (`output` / `metadata.output`). Private + * markers become typed Cursor outcomes, but stripping them alone can leave a + * blank or partial bash bubble that looks like success. Append a short + * user-facing status so the UI explains background handoff / timeout. + */ +function formatShellOutcomeDisplay(clean, outcome) { + let notice; + if (outcome.kind === "backgrounded") { + notice = outcome.msToWait > 0 + ? `Still running in the background (pid ${outcome.pid}) after ${outcome.msToWait}ms.` + : `Started in the background (pid ${outcome.pid}).`; + } + else if (outcome.kind === "timeout") { + notice = `Timed out after ${outcome.timeoutMs}ms.`; + } + if (!notice) + return clean; + if (!clean) + return `${notice}\n`; + return clean.endsWith("\n") ? `${clean}${notice}\n` : `${clean}\n${notice}\n`; +} +/** + * Find the last private wrapper sentinel. + * + * Soft-background wrappers print the marker as the final intentional line, but + * the host shell can still append job-control diagnostics afterwards (e.g. + * "Terminated: 15 … nohup sh -c '…cursor-shell-watchdog…'"). Match the sentinel + * on its own line and discard everything from that point to EOF. + */ +function lastPrivateMarker(output, marker, valuePattern) { + const re = new RegExp(`(?:^|\\r?\\n)(${marker}${valuePattern})`, "g"); + let match; + let last; + while ((match = re.exec(output)) !== null) { + if (match.index === undefined || match[1] === undefined) + continue; + const index = match[0].startsWith("\r\n") + ? match.index + 2 + : match[0].startsWith("\n") + ? match.index + 1 + : match.index; + last = { index, values: match.slice(2) }; + } + return last; +} +function parseOpenCodeTimeout(output) { + const closeTag = ""; + const closeAt = output.lastIndexOf(closeTag); + if (closeAt === -1 || output.slice(closeAt + closeTag.length).trim() !== "") + return undefined; + const header = /\r?\nshell tool terminated command after exceeding timeout (\d+) ms\./; + const match = header.exec(output.slice(0, closeAt)); + if (!match || match.index === undefined) + return undefined; + return { output: withoutMarker(output, match.index), timeoutMs: Number(match[1]) }; +} +function parseSoftBackgroundOutcome(output, policy) { + const background = lastPrivateMarker(output, BACKGROUND_MARKER, "(\\d+):([^\\r\\n]+)"); + if (background) { + const pid = Number(background.values[0]); + if (Number.isSafeInteger(pid) && pid > 0 && pid <= 0xffff_ffff) { + return { + output: withoutMarker(output, background.index), + outcome: { + kind: "backgrounded", + shellId: pid, + pid, + command: policy?.command ?? "", + workingDirectory: policy?.workingDirectory ?? "", + msToWait: policy?.timeoutMs ?? 0, + reason: 1, + }, + }; + } + } + const timeout = lastPrivateMarker(output, TIMEOUT_MARKER, "(\\d+)"); + if (timeout) { + return { + output: withoutMarker(output, timeout.index), + outcome: { kind: "timeout", timeoutMs: Number(timeout.values[0]) }, + }; + } + const exit = lastPrivateMarker(output, EXIT_MARKER, "(-?\\d+)"); + if (exit) { + return { + output: withoutMarker(output, exit.index), + outcome: { kind: "exit", code: Number(exit.values[0]) }, + }; + } + return undefined; +} +function parseBackgroundSpawnOutcome(output, policy) { + const match = lastPrivateMarker(output, BACKGROUND_SHELL_MARKER, "(\\d+):([^\\r\\n]+)"); + if (!match) + return undefined; + const pid = Number(match.values[0]); + if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff) + return undefined; + return { + output: withoutMarker(output, match.index), + outcome: { + kind: "backgrounded", + shellId: pid, + pid, + command: policy?.command ?? "", + workingDirectory: policy?.workingDirectory ?? "", + msToWait: 0, + reason: 1, + }, + }; +} +/** + * Strip private wrapper sentinels / OpenCode timeout envelopes for display. + * Does not record outcomes — use {@link captureCursorShellResult} for that. + * + * OpenCode 2.0 `Tool.Result.output` is structured (an object for shell), not + * a string. Non-string input is returned unchanged so the 2.0 after-hook can + * pass structured output through safely. + */ +export function sanitizeCursorShellDisplayOutput(output, policy) { + if (typeof output !== "string") + return output; + if (policy?.backgroundSpawn) { + const spawn = parseBackgroundSpawnOutcome(output, policy); + if (spawn) + return formatShellOutcomeDisplay(spawn.output, spawn.outcome); + } + if (policy?.timeoutBehavior === CURSOR_TIMEOUT_BACKGROUND) { + const wrapper = parseSoftBackgroundOutcome(output, policy); + if (wrapper) + return formatShellOutcomeDisplay(wrapper.output, wrapper.outcome); + } + const timeout = parseOpenCodeTimeout(output); + if (timeout) { + return formatShellOutcomeDisplay(timeout.output, { + kind: "timeout", + timeoutMs: timeout.timeoutMs, + }); + } + return output; +} +/** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */ +export function sanitizeRegisteredCursorShellOutput(toolCallId, output) { + if (typeof output !== "string") + return output; + if (typeof toolCallId !== "string" || !toolCallId) + return output; + return sanitizeCursorShellDisplayOutput(output, policies.get(toolCallId)); +} +/** + * Capture Bash completion in the classic plugin's after hook. Returns the + * sanitized output that OpenCode should store and render. + * + * Guards non-string output (OpenCode 2.0 structured `Tool.Result.output`) + * by returning it unchanged. + */ +export function captureCursorShellResult(toolCallId, output, metadata) { + if (typeof output !== "string") + return output; + if (typeof toolCallId !== "string" || !toolCallId.startsWith("cursor_")) + return output; + const policy = policies.get(toolCallId); + if (policy?.backgroundSpawn) { + const spawn = parseBackgroundSpawnOutcome(output, policy); + if (spawn) { + remember(outcomes, toolCallId, spawn.outcome); + return formatShellOutcomeDisplay(spawn.output, spawn.outcome); + } + } + // Private wrapper sentinels are meaningful only for calls we transformed. + // A normal foreground command is allowed to print the same text verbatim. + const wrapper = policy?.timeoutBehavior === CURSOR_TIMEOUT_BACKGROUND + ? parseSoftBackgroundOutcome(output, policy) + : undefined; + if (wrapper) { + remember(outcomes, toolCallId, wrapper.outcome); + return formatShellOutcomeDisplay(wrapper.output, wrapper.outcome); + } + const timeout = parseOpenCodeTimeout(output); + if (timeout) { + const outcome = { kind: "timeout", timeoutMs: timeout.timeoutMs }; + remember(outcomes, toolCallId, outcome); + return formatShellOutcomeDisplay(timeout.output, outcome); + } + const exitCode = finiteNonNegative(metadata?.exit); + if (exitCode !== undefined) + remember(outcomes, toolCallId, { kind: "exit", code: exitCode }); + return output; +} +/** Consume the structured result, with an inline fallback when no plugin hook ran. */ +export function consumeCursorShellResult(toolCallId, output) { + if (typeof toolCallId !== "string" || !toolCallId) { + return { output }; + } + let clean = output; + if (!outcomes.has(toolCallId)) + clean = captureCursorShellResult(toolCallId, output); + const outcome = outcomes.get(toolCallId); + outcomes.delete(toolCallId); + policies.delete(toolCallId); + releaseCursorShellEnv(toolCallId); + return { output: clean, outcome }; +} +/** Test/process cleanup. */ +export function resetCursorShellCalls() { + for (const wrap of activeEnvWraps.values()) + wrap.cleanup(); + activeEnvWraps.clear(); + pendingEnvWraps.clear(); + policies.clear(); + outcomes.clear(); + configuredShell = undefined; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/todo-store.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/todo-store.d.ts new file mode 100644 index 00000000..cc7b7448 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/todo-store.d.ts @@ -0,0 +1,29 @@ +/** + * Per-session todo list used when the host does not ship `todowrite` / + * `todoread` (OpenCode 2 dropped those builtins; OpenCode 1.x still has + * `todowrite`). + * + * In-memory only — same lifetime as the provider process. A restart clears + * the list until the next write/read, matching the mirrored Cursor-todo + * snapshot in `language-model.ts`. + */ +export declare const TODOWRITE_TOOL = "todowrite"; +export declare const TODOREAD_TOOL = "todoread"; +export declare const TODO_STATUSES: readonly ["pending", "in_progress", "completed", "cancelled"]; +export type TodoStatus = (typeof TODO_STATUSES)[number]; +export declare const TODO_PRIORITIES: readonly ["high", "medium", "low"]; +export type TodoPriority = (typeof TODO_PRIORITIES)[number]; +export type SessionTodo = { + id: string; + content: string; + status: TodoStatus; + priority: TodoPriority; +}; +/** Normalize one host/Cursor todo item. Drops entries with empty content. */ +export declare function normalizeSessionTodo(value: unknown, index: number): SessionTodo | undefined; +export declare function normalizeSessionTodos(value: unknown): SessionTodo[]; +export declare function getSessionTodos(sessionID: string): SessionTodo[]; +export declare function setSessionTodos(sessionID: string, todos: unknown): SessionTodo[]; +export declare function clearSessionTodos(sessionID: string): void; +/** Test-only: drop every session list. */ +export declare function clearAllSessionTodos(): void; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/todo-store.js b/references/upstream-plugins/cursor-opencode-provider/dist/todo-store.js new file mode 100644 index 00000000..a3de4aa3 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/todo-store.js @@ -0,0 +1,78 @@ +/** + * Per-session todo list used when the host does not ship `todowrite` / + * `todoread` (OpenCode 2 dropped those builtins; OpenCode 1.x still has + * `todowrite`). + * + * In-memory only — same lifetime as the provider process. A restart clears + * the list until the next write/read, matching the mirrored Cursor-todo + * snapshot in `language-model.ts`. + */ +export const TODOWRITE_TOOL = "todowrite"; +export const TODOREAD_TOOL = "todoread"; +export const TODO_STATUSES = ["pending", "in_progress", "completed", "cancelled"]; +export const TODO_PRIORITIES = ["high", "medium", "low"]; +const lists = new Map(); +const MAX_TRACKED_SESSIONS = 256; +function isStatus(value) { + return typeof value === "string" && TODO_STATUSES.includes(value); +} +function isPriority(value) { + return typeof value === "string" && TODO_PRIORITIES.includes(value); +} +/** Normalize one host/Cursor todo item. Drops entries with empty content. */ +export function normalizeSessionTodo(value, index) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return undefined; + const record = value; + const content = typeof record.content === "string" ? record.content : ""; + if (!content.trim()) + return undefined; + const id = typeof record.id === "string" && record.id.trim() + ? record.id.trim() + : String(index + 1); + return { + id, + content, + status: isStatus(record.status) ? record.status : "pending", + priority: isPriority(record.priority) ? record.priority : "medium", + }; +} +export function normalizeSessionTodos(value) { + if (!Array.isArray(value)) + return []; + const out = []; + const seen = new Set(); + for (const item of value) { + const todo = normalizeSessionTodo(item, out.length); + if (!todo) + continue; + let id = todo.id; + if (seen.has(id)) + id = `${id}-${out.length + 1}`; + seen.add(id); + out.push(id === todo.id ? todo : { ...todo, id }); + } + return out; +} +export function getSessionTodos(sessionID) { + return lists.get(sessionID)?.map((todo) => ({ ...todo })) ?? []; +} +export function setSessionTodos(sessionID, todos) { + const normalized = normalizeSessionTodos(todos); + lists.delete(sessionID); + lists.set(sessionID, normalized.map((todo) => ({ ...todo }))); + while (lists.size > MAX_TRACKED_SESSIONS) { + const oldest = lists.keys().next().value; + if (oldest === undefined) + break; + lists.delete(oldest); + } + return getSessionTodos(sessionID); +} +export function clearSessionTodos(sessionID) { + lists.delete(sessionID); +} +/** Test-only: drop every session list. */ +export function clearAllSessionTodos() { + lists.clear(); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/transport/connect.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/transport/connect.d.ts new file mode 100644 index 00000000..791523bf --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/transport/connect.d.ts @@ -0,0 +1,86 @@ +import { CursorTransportError, CursorProviderError } from "../errors.js"; +import http2 from "node:http2"; +export declare function buildBaseHeaders(token: string, clientVersion: string, extra?: Record): Record; +export declare function unaryAvailableModels(token: string, options?: { + apiBaseURL?: string; + baseURL?: string; + headers?: Record; + timeoutMs?: number; +}): Promise>; +/** + * Cursor Run stream hosts from GetServerConfig / agentBaseURL. + * Any HTTPS subdomain of cursor.sh is accepted — hostnames vary + * (agentn.*, agent.*, agent-gcpp-*, api5 / api5lat, …) and may change. + */ +export declare function isAllowedAgentHost(hostname: string): boolean; +/** + * Normalize a GetServerConfig agent URL to an https origin. + * Returns null for missing, malformed, non-https, or non-*.cursor.sh hosts. + */ +export declare function normalizeAgentRunOrigin(raw: string | undefined): string | null; +/** + * Fetch the Cursor server config (a Connect unary RPC on the API host) and + * return the `agentUrlConfig.agentnUrl` field — the region-specific Run stream + * origin the server routes this account/team to (e.g. `agentn.us.api5.cursor.sh`). + * + * Region-routed accounts can be silently rejected by the wrong regional host, so + * this lookup fails closed instead of substituting any host on error. Any + * authoritative `*.cursor.sh` origin from GetServerConfig is accepted. + */ +export declare function fetchAgentUrl(token: string, options?: { + apiBaseURL?: string; + baseURL?: string; + telemetryEnabled?: boolean; + timeoutMs?: number; +}): Promise; +export type BidiStream = { + write(msg: Uint8Array): boolean | void; + waitForDrain?(timeoutMs: number): Promise; + end(): void; + frames(): AsyncIterable<{ + flags: number; + payload: Uint8Array; + }>; + destroy(): void; + isClosed(): boolean; + onTerminal(listener: (event: BidiTerminalEvent) => void): () => void; +}; +export type BidiTerminalEvent = { + kind: "remote-clean-close"; +} | { + kind: "remote-error"; + error: CursorProviderError; +} | { + kind: "local-close"; +}; +export declare class CursorRunInterruptedError extends CursorTransportError { + constructor(message?: string, options?: ErrorOptions); +} +export declare function cursorRunTerminationError(input: { + responseStatus: number; + responseHeaders?: Record; + responseTrailers?: Record; + streamError?: Error | null; +}): CursorProviderError; +export declare const HTTP2_SESSION_MAX_AGE_MS: number; +export declare function shouldReuseHttp2Session(state: { + destroyed: boolean; + closed: boolean; +}, createdAt: number, now?: number): boolean; +/** Resolve the HTTP/2 connect origin for a Run stream (exported for tests). */ +export declare function resolveAgentOrigin(baseURL: string): string; +/** Test cleanup for local HTTP/2 fixtures; production sessions stay process-cached. */ +export declare function closeCachedHttp2SessionsForTests(): void; +/** Node-runtime regression hook; production callers use getSession(). */ +export declare function installSessionInvalidationForTests(origin: string, session: http2.ClientHttp2Session): void; +export declare function getSession(baseURL: string, options?: { + pingTimeoutMs?: number; +}): Promise; +export declare function bidiRunStream(token: string, options: { + signal?: AbortSignal; + baseURL: string; + headers?: Record; + readIdleMs?: number; + pingTimeoutMs?: number; +}): Promise; +export declare function makeRequestId(): string; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/transport/connect.js b/references/upstream-plugins/cursor-opencode-provider/dist/transport/connect.js new file mode 100644 index 00000000..1ff4205e --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/transport/connect.js @@ -0,0 +1,1002 @@ +import { CURSOR_API_HOST, CONNECT_PROTOCOL_VERSION, SERVER_CONFIG_PATH } from "../shared.js"; +import { encodeFrame, streamFrames } from "../protocol/framing.js"; +import { createCursorChecksumHeader } from "../protocol/checksum.js"; +import { getDeviceIds } from "../protocol/device-id.js"; +import { resolveClientVersion } from "../protocol/client-version.js"; +import { trace } from "../debug.js"; +import { CursorLocalCancellationError, CursorProtocolError, CursorTransportError, cursorGrpcError, cursorHttpError, errorCode, CursorProviderError, } from "../errors.js"; +import { withAbortDeadline } from "../deadline.js"; +import http2 from "node:http2"; +import { openProxiedTlsSocket, proxyEndpoint, resolveHttpsProxyUrl } from "./https-proxy.js"; +const API_BASE = `https://${CURSOR_API_HOST}`; +const DEFAULT_UNARY_TIMEOUT_MS = 5_000; +const MAX_TIMER_MS = 2_147_483_647; +function unaryTimeoutMs(operation, value) { + const timeoutMs = value ?? DEFAULT_UNARY_TIMEOUT_MS; + if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMER_MS) { + throw new CursorProtocolError(`${operation} timeout must be a positive integer no greater than ${MAX_TIMER_MS}`, { code: "CURSOR_INVALID_TIMEOUT" }); + } + return timeoutMs; +} +async function withUnaryDeadline(operation, timeoutMs, timeoutCode, run) { + return withAbortDeadline(timeoutMs, () => new CursorTransportError(`${operation} timed out after ${timeoutMs}ms`, { + transient: true, + replaySafe: true, + code: timeoutCode, + }), run); +} +function resolveApiBaseURL(options) { + return new URL(options.apiBaseURL ?? options.baseURL ?? API_BASE).origin; +} +trace("connect.ts module loaded"); +export function buildBaseHeaders(token, clientVersion, extra) { + const { machineId, macMachineId } = getDeviceIds(); + const headers = { + authorization: `Bearer ${token}`, + "connect-protocol-version": CONNECT_PROTOCOL_VERSION, + "x-cursor-client-type": "cli", + "x-cursor-client-version": clientVersion, + "x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId), + "x-ghost-mode": "true", + ...extra, + }; + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === "x-request-id") + delete headers[key]; + } + headers["x-request-id"] = crypto.randomUUID(); + return headers; +} +// ── Unary (AvailableModels) ── +export async function unaryAvailableModels(token, options = {}) { + const base = resolveApiBaseURL(options); + const url = `${base}/aiserver.v1.AiService/AvailableModels`; + const timeoutMs = unaryTimeoutMs("AvailableModels", options.timeoutMs); + return withUnaryDeadline("AvailableModels", timeoutMs, "CURSOR_AVAILABLE_MODELS_TIMEOUT", async (signal) => { + const clientVersion = await resolveClientVersion(); + const headers = buildBaseHeaders(token, clientVersion, options.headers); + let res; + try { + res = await fetch(url, { + method: "POST", + headers: { + ...headers, + "content-type": "application/json", + accept: "application/json", + }, + // Request parameterized effort/context/fast variants like Cursor IDE. + body: JSON.stringify({ + includeLongContextModels: true, + useModelParameters: true, + useCloudAgentEffortModes: true, + }), + signal, + }); + } + catch (cause) { + throw new CursorTransportError("AvailableModels network request failed", { + transient: true, + replaySafe: true, + code: errorCode(cause), + cause, + }); + } + if (!res.ok) { + throw await cursorHttpResponseError("AvailableModels failed:", res); + } + try { + return (await res.json()); + } + catch (cause) { + throw new CursorProtocolError("AvailableModels returned malformed JSON", { cause }); + } + }); +} +// ── Unary (GetServerConfig) ── +/** + * Cursor Run stream hosts from GetServerConfig / agentBaseURL. + * Any HTTPS subdomain of cursor.sh is accepted — hostnames vary + * (agentn.*, agent.*, agent-gcpp-*, api5 / api5lat, …) and may change. + */ +export function isAllowedAgentHost(hostname) { + return /^([a-z0-9-]+\.)+cursor\.sh$/i.test(hostname); +} +/** + * Normalize a GetServerConfig agent URL to an https origin. + * Returns null for missing, malformed, non-https, or non-*.cursor.sh hosts. + */ +export function normalizeAgentRunOrigin(raw) { + if (raw === undefined || raw === null) + return null; + const trimmed = String(raw).trim(); + if (!trimmed) + return null; + try { + const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; + const parsed = new URL(withScheme); + if (parsed.protocol !== "https:") + return null; + if (parsed.username || parsed.password) + return null; + if (!isAllowedAgentHost(parsed.hostname)) + return null; + return parsed.origin; + } + catch { + return null; + } +} +/** + * Fetch the Cursor server config (a Connect unary RPC on the API host) and + * return the `agentUrlConfig.agentnUrl` field — the region-specific Run stream + * origin the server routes this account/team to (e.g. `agentn.us.api5.cursor.sh`). + * + * Region-routed accounts can be silently rejected by the wrong regional host, so + * this lookup fails closed instead of substituting any host on error. Any + * authoritative `*.cursor.sh` origin from GetServerConfig is accepted. + */ +export async function fetchAgentUrl(token, options = {}) { + const base = resolveApiBaseURL(options); + const url = `${base}${SERVER_CONFIG_PATH}`; + const timeoutMs = unaryTimeoutMs("GetServerConfig", options.timeoutMs); + return withUnaryDeadline("GetServerConfig", timeoutMs, "CURSOR_AGENT_URL_TIMEOUT", async (signal) => { + const clientVersion = await resolveClientVersion(); + // Match Cursor CLI: GetServerConfig uses base headers only — session headers belong on Run. + const headers = buildBaseHeaders(token, clientVersion); + trace(`GetServerConfig POST ${url}`); + let res; + try { + res = await fetch(url, { + method: "POST", + headers: { + ...headers, + "content-type": "application/json", + accept: "application/json", + }, + body: JSON.stringify({ telem_enabled: options.telemetryEnabled ?? false }), + signal, + }); + } + catch (cause) { + throw new CursorTransportError("GetServerConfig network request failed", { + transient: true, + replaySafe: true, + code: errorCode(cause), + cause, + }); + } + if (!res.ok) { + throw await cursorHttpResponseError("GetServerConfig failed:", res); + } + let body; + try { + body = (await res.json()); + } + catch (cause) { + throw new CursorProtocolError("GetServerConfig returned malformed JSON", { cause }); + } + const cfg = body.agentUrlConfig; + const raw = cfg?.agentnUrl || cfg?.agentUrl; + const normalized = normalizeAgentRunOrigin(raw); + trace(`GetServerConfig reply: agentnUrl=${cfg?.agentnUrl ?? ""} ` + + `agentUrl=${cfg?.agentUrl ?? ""} → ${normalized ?? ""}`); + if (!normalized) { + throw new CursorProtocolError(raw + ? "GetServerConfig returned an invalid Cursor agent URL" + : "GetServerConfig response missing agentUrlConfig.agentnUrl"); + } + return normalized; + }); +} +export class CursorRunInterruptedError extends CursorTransportError { + constructor(message = "Cursor Run ended before turn_ended", options) { + super(message, { + transient: true, + replaySafe: true, + cause: options?.cause, + }); + this.name = "CursorRunInterruptedError"; + } +} +export function cursorRunTerminationError(input) { + const headers = input.responseHeaders ?? {}; + const trailers = input.responseTrailers ?? {}; + if (input.streamError) { + return new CursorRunInterruptedError(`Cursor Run transport interrupted: ${input.streamError.message}`, { cause: input.streamError }); + } + if (input.responseStatus !== 0 && input.responseStatus !== 200) { + return cursorRunHttpError(input.responseStatus, headers); + } + const grpcStatus = trailers["grpc-status"] ?? headers["grpc-status"]; + if (grpcStatus !== undefined && String(grpcStatus) !== "0") { + return cursorRunGrpcError(String(grpcStatus), headers, trailers); + } + return new CursorRunInterruptedError(); +} +function withErrorMessageSuffix(error, suffix) { + if (suffix) + error.message += suffix; + return error; +} +async function cursorHttpResponseError(operation, res) { + const text = await res.text().catch(() => ""); + return withErrorMessageSuffix(cursorHttpError(operation, res.status), `${res.statusText ? ` ${res.statusText}` : ""}${text ? ` - ${text.slice(0, 200)}` : ""}`); +} +function cursorRunHttpError(statusCode, headers) { + return withErrorMessageSuffix(cursorHttpError("Cursor Run failed by remote:", statusCode), ` ${JSON.stringify(stripPseudo(headers))}`); +} +function cursorRunGrpcError(grpcStatus, headers, trailers) { + const message = trailers["grpc-message"] ?? headers["grpc-message"]; + return withErrorMessageSuffix(cursorGrpcError("Cursor Run failed by remote:", grpcStatus), message === undefined ? "" : `: ${message}`); +} +// Cache http2 sessions keyed by origin so a custom baseURL never reuses a +// connection opened to the default agent host — and vice versa. +const _http2Sessions = new Map(); +const _http2SessionCreatedAt = new WeakMap(); +const _http2SessionListenerCleanup = new WeakMap(); +// Validation and connect work for the same origin shares one Promise. +const _http2Connecting = new Map(); +// Bound the time we'll wait for the initial connect. Without this, a dead or +// unreachable host (DNS failure, network partition) hangs the provider forever +// because the 'connect' / 'error' event may never fire. +const CONNECT_TIMEOUT_MS = 15_000; +const DEFAULT_SESSION_PING_TIMEOUT_MS = 5_000; +const DEFAULT_READ_IDLE_MS = 120_000; +const WRITE_DRAIN_TIMEOUT_CODE = "CURSOR_WRITE_DRAIN_TIMEOUT"; +export const HTTP2_SESSION_MAX_AGE_MS = 15 * 60_000; +export function shouldReuseHttp2Session(state, createdAt, now = Date.now()) { + return !state.destroyed && !state.closed && now - createdAt < HTTP2_SESSION_MAX_AGE_MS; +} +/** Resolve the HTTP/2 connect origin for a Run stream (exported for tests). */ +export function resolveAgentOrigin(baseURL) { + const origin = normalizeAgentRunOrigin(baseURL); + if (!origin) { + throw new CursorProtocolError("Cursor Run stream requires an allowlisted Cursor agent base URL"); + } + return origin; +} +function dropSession(origin, session) { + if (_http2Sessions.get(origin) === session) + _http2Sessions.delete(origin); +} +/** Test cleanup for local HTTP/2 fixtures; production sessions stay process-cached. */ +export function closeCachedHttp2SessionsForTests() { + for (const session of _http2Sessions.values()) { + _http2SessionListenerCleanup.get(session)?.(); + try { + session.destroy(); + } + catch { /* already closed */ } + } + _http2Sessions.clear(); + _http2Connecting.clear(); +} +function timeoutMs(name, value, fallback) { + const resolved = value ?? fallback; + if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_TIMER_MS) { + throw new CursorProtocolError(`${name} must be a positive integer no greater than ${MAX_TIMER_MS}`); + } + return resolved; +} +function toTransportError(value, fallback) { + if (value instanceof CursorTransportError) + return value; + return new CursorTransportError(fallback, { + transient: true, + replaySafe: true, + code: errorCode(value), + cause: value, + }); +} +function installSessionInvalidation(origin, session) { + const socket = session.socket; + let cleaned = false; + let removeSocketListeners; + const cleanup = () => { + if (cleaned) + return; + cleaned = true; + session.removeListener("close", onSessionClose); + session.removeListener("goaway", onSessionGoaway); + session.removeListener("error", onSessionError); + removeSocketListeners?.(); + removeSocketListeners = undefined; + _http2SessionListenerCleanup.delete(session); + }; + const onSessionClose = () => { + trace(`h2 session closed: origin=${origin}`); + dropSession(origin, session); + cleanup(); + }; + const onSessionGoaway = (errorCode, lastStreamID) => { + trace(`h2 session GOAWAY: origin=${origin} errorCode=${errorCode} lastStreamID=${lastStreamID}`); + // Existing streams may drain, but future Runs must use a fresh session. + dropSession(origin, session); + }; + const onSessionError = (error) => { + trace(`h2 session error: origin=${origin} err=${error.message}`); + dropSession(origin, session); + }; + const onSocketEnd = () => { + trace(`h2 socket ended: origin=${origin}`); + dropSession(origin, session); + }; + const onSocketClose = () => { + trace(`h2 socket closed: origin=${origin}`); + dropSession(origin, session); + }; + session.once("close", onSessionClose); + session.on("goaway", onSessionGoaway); + // Keep an error listener until close so teardown errors cannot become an + // unhandled EventEmitter error. + session.on("error", onSessionError); + if (socket) { + socket.once("end", onSocketEnd); + socket.once("close", onSocketClose); + // Http2Session.socket is a lifecycle-bound proxy. Looking up methods after + // detach throws ERR_HTTP2_SOCKET_UNBOUND, so bind removers while attached. + const removeEndListener = socket.removeListener.bind(socket, "end", onSocketEnd); + const removeCloseListener = socket.removeListener.bind(socket, "close", onSocketClose); + removeSocketListeners = () => { + try { + removeEndListener(); + } + catch { /* teardown is best-effort */ } + try { + removeCloseListener(); + } + catch { /* teardown is best-effort */ } + }; + } + _http2SessionListenerCleanup.set(session, cleanup); +} +/** Node-runtime regression hook; production callers use getSession(). */ +export function installSessionInvalidationForTests(origin, session) { + installSessionInvalidation(origin, session); +} +function invalidateSession(origin, session) { + dropSession(origin, session); + _http2SessionListenerCleanup.get(session)?.(); + try { + session.destroy(); + } + catch { /* already closed */ } +} +function validateCachedSession(session, pingTimeoutMs) { + if (session.destroyed || session.closed) { + return Promise.reject(new CursorTransportError("Cursor HTTP/2 cached session is already closed")); + } + return new Promise((resolve, reject) => { + let settled = false; + const finish = (error) => { + if (settled) + return; + settled = true; + clearTimeout(timer); + session.removeListener("close", onClosed); + session.removeListener("goaway", onGoaway); + session.removeListener("error", onError); + if (error) + reject(error); + else + resolve(); + }; + const onClosed = () => finish(new CursorTransportError("Cursor HTTP/2 cached session closed during ping")); + const onGoaway = () => finish(new CursorTransportError("Cursor HTTP/2 cached session received GOAWAY during ping")); + const onError = (error) => finish(toTransportError(error, "Cursor HTTP/2 cached session ping failed")); + const timer = setTimeout(() => { + finish(new CursorTransportError(`Cursor HTTP/2 cached session ping timed out after ${pingTimeoutMs}ms`)); + }, pingTimeoutMs); + timer.unref?.(); + session.once("close", onClosed); + session.once("goaway", onGoaway); + session.once("error", onError); + try { + const accepted = session.ping((error) => { + if (error) + finish(toTransportError(error, "Cursor HTTP/2 cached session ping failed")); + else + finish(); + }); + if (!accepted) + finish(new CursorTransportError("Cursor HTTP/2 cached session refused a health-check ping")); + } + catch (error) { + finish(toTransportError(error, "Cursor HTTP/2 cached session ping failed")); + } + }); +} +function connectSession(origin) { + return new Promise((resolve, reject) => { + let settled = false; + let session; + let tunnelSocket; + const abort = new AbortController(); + const clearConnectTimer = () => { + clearTimeout(timer); + }; + const detachSessionListeners = () => { + if (!session) + return; + session.removeListener("error", onError); + session.removeListener("close", onClose); + session.removeListener("connect", onConnect); + }; + const destroyTunnel = () => { + try { + tunnelSocket?.destroy(); + } + catch { /* ignore */ } + tunnelSocket = undefined; + }; + const fail = (error) => { + if (settled) + return; + settled = true; + clearConnectTimer(); + try { + abort.abort(error); + } + catch { /* ignore */ } + detachSessionListeners(); + if (session) { + dropSession(origin, session); + try { + session.destroy(); + } + catch { /* ignore */ } + } + destroyTunnel(); + reject(error); + }; + const onError = (error) => fail(toTransportError(error, "Cursor HTTP/2 connection failed")); + const onClose = () => fail(new CursorTransportError(`HTTP/2 connection to ${origin} closed before connecting`)); + const onConnect = () => { + if (settled || !session) + return; + settled = true; + clearConnectTimer(); + detachSessionListeners(); + installSessionInvalidation(origin, session); + if (session.destroyed || session.closed) { + const error = new CursorTransportError(`HTTP/2 connection to ${origin} closed while connecting`); + invalidateSession(origin, session); + destroyTunnel(); + reject(error); + return; + } + // The TLS socket (if any) is owned by the HTTP/2 session from here on. + tunnelSocket = undefined; + _http2SessionCreatedAt.set(session, Date.now()); + _http2Sessions.set(origin, session); + trace(`h2 session connected: origin=${origin}`); + resolve(session); + }; + const timer = setTimeout(() => { + fail(new CursorTransportError(`HTTP/2 connect to ${origin} timed out after ${CONNECT_TIMEOUT_MS}ms`)); + }, CONNECT_TIMEOUT_MS); + timer.unref?.(); + void (async () => { + try { + const { hostname, port: originPort } = new URL(origin); + const targetPort = originPort ? Number(originPort) : 443; + const proxy = resolveHttpsProxyUrl(hostname, process.env, targetPort); + if (proxy) { + const endpoint = proxyEndpoint(proxy); + trace(`h2 connect via HTTPS proxy: origin=${origin} proxy=${endpoint.host}:${endpoint.port}`); + const tlsSocket = await openProxiedTlsSocket({ + proxy, + targetHost: hostname, + targetPort, + signal: abort.signal, + }); + if (settled) { + try { + tlsSocket.destroy(); + } + catch { /* ignore */ } + return; + } + tunnelSocket = tlsSocket; + session = http2.connect(origin, { + createConnection: () => tlsSocket, + }); + } + else { + session = http2.connect(origin); + } + if (settled) { + try { + session.destroy(); + } + catch { /* ignore */ } + destroyTunnel(); + return; + } + session.on("error", onError); + session.once("close", onClose); + session.once("connect", onConnect); + } + catch (error) { + fail(toTransportError(error, "Cursor HTTP/2 connection failed")); + } + })(); + }); +} +export function getSession(baseURL, options = {}) { + const origin = resolveAgentOrigin(baseURL); + const pingTimeoutMs = timeoutMs("Cursor provider pingTimeoutMs", options.pingTimeoutMs, DEFAULT_SESSION_PING_TIMEOUT_MS); + const inflight = _http2Connecting.get(origin); + if (inflight) + return inflight; + const promise = (async () => { + const existing = _http2Sessions.get(origin); + if (existing) { + const createdAt = _http2SessionCreatedAt.get(existing) ?? 0; + if (!shouldReuseHttp2Session(existing, createdAt)) { + trace(`h2 session rotate: origin=${origin} ageMs=${Math.max(0, Date.now() - createdAt)}`); + dropSession(origin, existing); + try { + existing.close(); + } + catch { /* already closed */ } + } + else { + try { + await validateCachedSession(existing, pingTimeoutMs); + if (_http2Sessions.get(origin) === existing && !existing.destroyed && !existing.closed) { + trace(`h2 cached session ping ok: origin=${origin}`); + return existing; + } + } + catch (error) { + trace(`h2 cached session ping failed: origin=${origin} err=${error.message}`); + invalidateSession(origin, existing); + } + } + } + return connectSession(origin); + })(); + _http2Connecting.set(origin, promise); + const cleanup = () => { + if (_http2Connecting.get(origin) === promise) + _http2Connecting.delete(origin); + }; + promise.then(cleanup, cleanup); + return promise; +} +export async function bidiRunStream(token, options) { + const origin = resolveAgentOrigin(options.baseURL); + const readIdleMs = timeoutMs("Cursor provider readIdleMs", options.readIdleMs, DEFAULT_READ_IDLE_MS); + const [session, clientVersion] = await Promise.all([ + getSession(options.baseURL, { pingTimeoutMs: options.pingTimeoutMs }), + resolveClientVersion(), + ]); + const headers = { + ...buildBaseHeaders(token, clientVersion, options.headers), + ":method": "POST", + ":path": "/agent.v1.AgentService/Run", + "content-type": "application/connect+proto", + "connect-accept-encoding": "gzip,br", + // The CLI's streaming interceptor sets this on the bidi Run stream + // (decompiled client.ts:1190). Without it Cursor may treat the stream as + // non-streaming. + "x-cursor-streaming": "true", + "user-agent": "connect-es/1.6.1", + }; + const stream = session.request(headers, { + endStream: false, + }); + let writable = true; + let locallyClosed = false; + let remotelyClosed = false; + let backpressured = false; + let responseStatus = 0; + let responseHeaders = {}; + let responseTrailers = {}; + let rawStreamError; + let streamFailure = null; + let terminalEvent; + let terminalSettlementScheduled = false; + const terminalListeners = new Set(); + const inboundFrames = []; + const pendingChunks = []; + let inboundEnded = false; + let inboundFailure; + let inboundWaiter; + let readIdleTimer; + let inboundReaderStarted = false; + const abortSignal = options.signal; + let abortHandler; + const observedFailure = () => { + if (streamFailure) + return streamFailure; + if (responseStatus !== 0 && responseStatus !== 200) { + streamFailure = cursorRunHttpError(responseStatus, responseHeaders); + return streamFailure; + } + const grpcStatus = responseTrailers["grpc-status"] ?? responseHeaders["grpc-status"]; + if (grpcStatus !== undefined && String(grpcStatus) !== "0") { + streamFailure = cursorRunGrpcError(String(grpcStatus), responseHeaders, responseTrailers); + return streamFailure; + } + if (rawStreamError) { + streamFailure = toTransportError(rawStreamError, "Cursor Run transport interrupted"); + return streamFailure; + } + return undefined; + }; + const settleTerminal = (event) => { + if (terminalEvent) + return; + terminalEvent = event; + for (const listener of terminalListeners) { + try { + listener(event); + } + catch { /* observers cannot destabilize teardown */ } + } + terminalListeners.clear(); + }; + const clearReadIdleTimer = () => { + if (readIdleTimer) + clearTimeout(readIdleTimer); + readIdleTimer = undefined; + }; + const cleanupTerminalResources = () => { + clearReadIdleTimer(); + if (abortSignal && abortHandler) { + abortSignal.removeEventListener("abort", abortHandler); + abortHandler = undefined; + } + }; + const finishInbound = (error) => { + if (error) { + inboundFailure = error; + pendingChunks.length = 0; + } + else if (!inboundFailure) { + inboundEnded = true; + } + const waiter = inboundWaiter; + inboundWaiter = undefined; + if (!waiter) + return; + if (inboundFailure) + waiter.reject(inboundFailure); + else + waiter.resolve(undefined); + }; + const stopInboundReader = () => { + cleanupTerminalResources(); + if (inboundReaderStarted) { + stream.removeListener("data", onInboundChunk); + inboundReaderStarted = false; + } + }; + const nextInboundFrame = () => { + const queued = inboundFrames.shift(); + if (queued) + return Promise.resolve(queued); + if (inboundFailure) + return Promise.reject(inboundFailure); + if (inboundEnded) + return Promise.resolve(undefined); + return new Promise((resolve, reject) => { + inboundWaiter = { resolve, reject }; + }); + }; + const enqueueFrame = (frame) => { + const waiter = inboundWaiter; + inboundWaiter = undefined; + if (waiter) + waiter.resolve(frame); + else + inboundFrames.push(frame); + }; + const settleObservedTerminal = () => { + terminalSettlementScheduled = false; + if (terminalEvent) + return; + if (locallyClosed) { + finishInbound(); + stopInboundReader(); + settleTerminal({ kind: "local-close" }); + return; + } + const failure = observedFailure(); + finishInbound(failure); + stopInboundReader(); + settleTerminal(failure + ? { kind: "remote-error", error: failure } + : { kind: "remote-clean-close" }); + }; + const scheduleTerminalSettlement = () => { + if (terminalEvent || terminalSettlementScheduled) + return; + terminalSettlementScheduled = true; + // HTTP/2 often emits end/aborted immediately before error/close. One turn + // lets the strongest terminal metadata win. + setImmediate(settleObservedTerminal); + }; + const onReadIdleTimeout = () => { + readIdleTimer = undefined; + if (locallyClosed || remotelyClosed || streamFailure) + return; + streamFailure = new CursorTransportError(`Cursor Run stream read-idle timeout after ${readIdleMs}ms — connection presumed dead`, { transient: true, replaySafe: true, code: "CURSOR_READ_IDLE_TIMEOUT" }); + writable = false; + finishInbound(streamFailure); + stopInboundReader(); + settleTerminal({ kind: "remote-error", error: streamFailure }); + try { + stream.destroy(streamFailure); + } + catch { /* already closing */ } + }; + const armReadIdleWatchdog = () => { + clearReadIdleTimer(); + if (locallyClosed || remotelyClosed || streamFailure) + return; + readIdleTimer = setTimeout(onReadIdleTimeout, readIdleMs); + readIdleTimer.unref?.(); + }; + const onInboundChunk = (chunk) => { + armReadIdleWatchdog(); + if (inboundEnded || inboundFailure) + return; + pendingChunks.push(new Uint8Array(chunk)); + const merged = mergeBuffers(pendingChunks); + const parsed = Array.from(streamFrames(merged)); + const consumed = parsed.reduce((sum, frame) => sum + 5 + frame.payload.length, 0); + pendingChunks.length = 0; + if (consumed < merged.length) + pendingChunks.push(merged.subarray(consumed)); + for (const frame of parsed) + enqueueFrame(frame); + }; + const startInboundReader = () => { + if (inboundReaderStarted || locallyClosed || remotelyClosed || streamFailure) + return; + inboundReaderStarted = true; + stream.on("data", onInboundChunk); + armReadIdleWatchdog(); + }; + // Capture the HTTP/2 response status/headers and any stream-level error. + // Without this, a non-200 or RST_STREAM surfaces as a silent clean end + // (frames() just stops) — which looks exactly like "no response, no error". + stream.on("response", (h) => { + responseHeaders = h; + responseStatus = h[":status"] !== undefined ? Number(h[":status"]) : 0; + trace(`h2 response: status=${responseStatus} headers=${JSON.stringify(stripPseudo(h))}`); + }); + stream.on("trailers", (h) => { + responseTrailers = h; + trace(`h2 trailers: ${JSON.stringify(stripPseudo(h))}`); + }); + stream.on("error", (err) => { + rawStreamError = err; + writable = false; + trace(`h2 stream error: ${err?.name}: ${err?.message}`); + scheduleTerminalSettlement(); + }); + stream.on("aborted", () => { + writable = false; + rawStreamError ??= new CursorTransportError("Cursor Run stream aborted by remote", { + transient: true, + replaySafe: true, + rstCode: stream.rstCode, + code: "ERR_HTTP2_STREAM_CANCEL", + }); + scheduleTerminalSettlement(); + }); + stream.on("end", () => { + writable = false; + scheduleTerminalSettlement(); + }); + stream.on("close", () => { + writable = false; + remotelyClosed = !locallyClosed; + if (remotelyClosed) { + dropSession(origin, session); + // Stop assigning sibling Runs to a connection that remotely lost one + // of its streams. close() drains existing streams without destroying + // them; the next Run opens a fresh HTTP/2 session. + try { + session.close(); + } + catch { /* already closed */ } + } + trace(`h2 stream closed (status=${responseStatus}, local=${locallyClosed}, ` + + `err=${rawStreamError instanceof Error ? rawStreamError.message : "none"})`); + scheduleTerminalSettlement(); + }); + if (abortSignal) { + abortHandler = () => { + if (!locallyClosed) { + locallyClosed = true; + writable = false; + inboundFrames.length = 0; + pendingChunks.length = 0; + finishInbound(); + stopInboundReader(); + settleTerminal({ kind: "local-close" }); + stream.close(); + } + }; + abortSignal.addEventListener("abort", abortHandler, { once: true }); + if (abortSignal.aborted) + abortHandler(); + } + startInboundReader(); + return { + write(msg) { + if (!writable || remotelyClosed || stream.closed || stream.destroyed) { + if (locallyClosed) { + throw new CursorLocalCancellationError("Cursor Run stream is closed locally"); + } + throw observedFailure() ?? new CursorRunInterruptedError("Cursor Run stream is no longer writable"); + } + const frame = encodeFrame(0x00, msg); + try { + const accepted = stream.write(frame); + backpressured = !accepted; + return accepted; + } + catch (cause) { + rawStreamError = cause; + streamFailure = toTransportError(cause, "Cursor Run stream write failed"); + streamFailure.replaySafe = false; + writable = false; + scheduleTerminalSettlement(); + throw streamFailure; + } + }, + waitForDrain(timeout) { + if (!backpressured) + return Promise.resolve(); + const drainTimeoutMs = timeoutMs("Cursor write drain timeout", timeout, timeout); + return new Promise((resolve, reject) => { + let settled = false; + const finish = (error) => { + if (settled) + return; + settled = true; + clearTimeout(timer); + stream.removeListener("drain", onDrain); + stream.removeListener("error", onError); + stream.removeListener("close", onClose); + if (error) + reject(error); + else + resolve(); + }; + const onDrain = () => { + backpressured = false; + finish(); + }; + const onError = (cause) => { + streamFailure ??= toTransportError(cause, "Cursor Run stream drain failed"); + streamFailure.replaySafe = false; + finish(streamFailure); + }; + const onClose = () => { + finish(locallyClosed + ? new CursorLocalCancellationError("Cursor Run stream closed locally during drain") + : observedFailure() ?? new CursorRunInterruptedError("Cursor Run stream closed before drain")); + }; + const timer = setTimeout(() => { + streamFailure ??= new CursorTransportError(`Cursor Run stream backpressure did not drain after ${drainTimeoutMs}ms`, { transient: false, replaySafe: false, code: WRITE_DRAIN_TIMEOUT_CODE }); + finish(streamFailure); + try { + stream.destroy(streamFailure); + } + catch { /* already closing */ } + }, drainTimeoutMs); + timer.unref?.(); + stream.once("drain", onDrain); + stream.once("error", onError); + stream.once("close", onClose); + }); + }, + end() { + locallyClosed = true; + writable = false; + inboundFrames.length = 0; + pendingChunks.length = 0; + finishInbound(); + stopInboundReader(); + settleTerminal({ kind: "local-close" }); + stream.end(); + }, + async *frames() { + try { + while (true) { + const frame = await nextInboundFrame(); + if (!frame) + break; + trace(`frame yield: flags=0x${frame.flags.toString(16)} payload=${frame.payload.length}B`); + yield frame; + } + remotelyClosed = !locallyClosed; + if (locallyClosed) + return; + // A clean HTTP/2 end before Cursor's turn_ended is still an interrupted + // Run at the provider layer. + throw observedFailure() ?? cursorRunTerminationError({ + responseStatus, + responseHeaders, + responseTrailers, + }); + } + catch (error) { + if (locallyClosed) + return; + if (error instanceof CursorProviderError) + throw error; + throw toTransportError(error, `Cursor Run transport interrupted: ${error instanceof Error ? error.message : "unknown error"}`); + } + finally { + stopInboundReader(); + } + }, + destroy() { + if (!locallyClosed) { + locallyClosed = true; + writable = false; + inboundFrames.length = 0; + pendingChunks.length = 0; + finishInbound(); + stopInboundReader(); + settleTerminal({ kind: "local-close" }); + stream.close(); + } + }, + isClosed() { + return remotelyClosed || locallyClosed || stream.closed || stream.destroyed; + }, + onTerminal(listener) { + if (terminalEvent) { + try { + listener(terminalEvent); + } + catch { /* observers cannot destabilize teardown */ } + return () => { }; + } + terminalListeners.add(listener); + return () => terminalListeners.delete(listener); + }, + }; +} +function mergeBuffers(buffers) { + if (buffers.length === 0) + return new Uint8Array(0); + if (buffers.length === 1) + return buffers[0]; + const total = buffers.reduce((s, b) => s + b.length, 0); + const merged = new Uint8Array(total); + let offset = 0; + for (const b of buffers) { + merged.set(b, offset); + offset += b.length; + } + return merged; +} +export function makeRequestId() { + return crypto.randomUUID(); +} +function stripPseudo(h) { + const out = {}; + for (const [k, v] of Object.entries(h)) { + if (k.startsWith(":")) { + if (k === ":status") + out[k] = v; + continue; + } + if (k === "authorization" || k === "x-cursor-checksum") { + out[k] = ""; + continue; + } + out[k] = v; + } + return out; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/transport/https-proxy.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/transport/https-proxy.d.ts new file mode 100644 index 00000000..45ea5afd --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/transport/https-proxy.d.ts @@ -0,0 +1,56 @@ +/** + * HTTPS proxy helpers for the Cursor Run HTTP/2 transport. + * + * Unary RPCs use Bun `fetch()`, which already honors HTTPS_PROXY / NO_PROXY. + * `node:http2` does not, so Run sessions tunnel through an HTTP CONNECT proxy + * when those env vars apply. See GitHub issue #26. + * + * Important (Bun 1.3.x): do the CONNECT handshake on a native `net.Socket`, + * then `tls.connect({ socket })` on that same socket. Do not insert a custom + * Duplex between the proxy socket and TLS — destroying such a session mid- + * handshake can segfault Bun. + */ +import net from "node:net"; +import tls from "node:tls"; +export type ProxyEnv = Record; +/** + * HTTPS_PROXY / https_proxy for an HTTPS target. Like curl, HTTP_PROXY is not + * consulted for HTTPS. Only `http://` proxies can carry the CONNECT tunnel; + * `https://` (TLS to the proxy) and other schemes are ignored so the Run keeps + * its direct connection rather than failing on every connect. + */ +export declare function resolveHttpsProxyUrl(targetHost: string, env?: ProxyEnv, targetPort?: number): URL | undefined; +/** + * curl/Node-style NO_PROXY matching: `*`, exact host, optional `:port`, and + * leading-dot / bare-domain suffix forms (`.corp.example` / `corp.example`). + * A port-qualified entry only bypasses that port. + */ +export declare function hostMatchesNoProxy(targetHost: string, noProxy: string | undefined, targetPort?: number): boolean; +/** Dialable proxy address: IPv6 literals without URL brackets, default port 80. */ +export declare function proxyEndpoint(proxy: URL): { + host: string; + port: number; +}; +export type OpenHttpsConnectTunnelOptions = { + proxy: URL; + targetHost: string; + targetPort?: number; + signal?: AbortSignal; + /** Injected for tests. */ + connect?: typeof net.connect; +}; +/** + * Open a plain TCP connection to the proxy and establish an HTTP CONNECT tunnel + * to `targetHost:targetPort`. Returns the tunneled socket ready for TLS. + */ +export declare function openHttpsConnectTunnel(options: OpenHttpsConnectTunnelOptions): Promise; +export type OpenProxiedTlsSocketOptions = { + proxy: URL; + targetHost: string; + targetPort?: number; + signal?: AbortSignal; + connect?: typeof net.connect; + tlsConnect?: typeof tls.connect; +}; +/** CONNECT through the proxy, then complete TLS (ALPN h2) on the native socket. */ +export declare function openProxiedTlsSocket(options: OpenProxiedTlsSocketOptions): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/transport/https-proxy.js b/references/upstream-plugins/cursor-opencode-provider/dist/transport/https-proxy.js new file mode 100644 index 00000000..6be1ff24 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/transport/https-proxy.js @@ -0,0 +1,348 @@ +/** + * HTTPS proxy helpers for the Cursor Run HTTP/2 transport. + * + * Unary RPCs use Bun `fetch()`, which already honors HTTPS_PROXY / NO_PROXY. + * `node:http2` does not, so Run sessions tunnel through an HTTP CONNECT proxy + * when those env vars apply. See GitHub issue #26. + * + * Important (Bun 1.3.x): do the CONNECT handshake on a native `net.Socket`, + * then `tls.connect({ socket })` on that same socket. Do not insert a custom + * Duplex between the proxy socket and TLS — destroying such a session mid- + * handshake can segfault Bun. + */ +import net from "node:net"; +import tls from "node:tls"; +import { CursorTransportError } from "../errors.js"; +/** + * HTTPS_PROXY / https_proxy for an HTTPS target. Like curl, HTTP_PROXY is not + * consulted for HTTPS. Only `http://` proxies can carry the CONNECT tunnel; + * `https://` (TLS to the proxy) and other schemes are ignored so the Run keeps + * its direct connection rather than failing on every connect. + */ +export function resolveHttpsProxyUrl(targetHost, env = process.env, targetPort = 443) { + if (!targetHost) + return undefined; + if (hostMatchesNoProxy(targetHost, env.NO_PROXY ?? env.no_proxy, targetPort)) + return undefined; + const raw = (env.HTTPS_PROXY ?? env.https_proxy)?.trim(); + if (!raw) + return undefined; + try { + const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(raw) ? raw : `http://${raw}`; + const url = new URL(withScheme); + if (url.protocol !== "http:") + return undefined; + if (!url.hostname) + return undefined; + return url; + } + catch { + return undefined; + } +} +/** + * curl/Node-style NO_PROXY matching: `*`, exact host, optional `:port`, and + * leading-dot / bare-domain suffix forms (`.corp.example` / `corp.example`). + * A port-qualified entry only bypasses that port. + */ +export function hostMatchesNoProxy(targetHost, noProxy, targetPort = 443) { + if (!noProxy?.trim()) + return false; + const host = targetHost.trim().toLowerCase().replace(/\.$/, ""); + if (!host) + return false; + for (const part of noProxy.split(/[\s,]+/)) { + const entry = part.trim().toLowerCase(); + if (!entry) + continue; + if (entry === "*") + return true; + const [entryHostRaw, entryPort] = splitHostPort(entry); + const entryHost = entryHostRaw.replace(/^\./, "").replace(/\.$/, ""); + if (!entryHost) + continue; + if (entryPort !== undefined && Number(entryPort) !== targetPort) + continue; + if (host === entryHost) + return true; + if (host.endsWith(`.${entryHost}`)) + return true; + } + return false; +} +function splitHostPort(entry) { + if (entry.startsWith("[")) { + const end = entry.indexOf("]"); + if (end === -1) + return [entry, undefined]; + const host = entry.slice(1, end); + const rest = entry.slice(end + 1); + if (rest.startsWith(":") && rest.length > 1) + return [host, rest.slice(1)]; + return [host, undefined]; + } + const idx = entry.lastIndexOf(":"); + if (idx > 0 && /^\d+$/.test(entry.slice(idx + 1))) { + return [entry.slice(0, idx), entry.slice(idx + 1)]; + } + return [entry, undefined]; +} +/** Dialable proxy address: IPv6 literals without URL brackets, default port 80. */ +export function proxyEndpoint(proxy) { + return { + host: proxy.hostname.replace(/^\[(.*)\]$/, "$1"), + port: proxy.port ? Number(proxy.port) : 80, + }; +} +/** Userinfo as typed when it is not valid percent-encoding (e.g. a bare `%`). */ +function decodeUserinfo(value) { + try { + return decodeURIComponent(value); + } + catch { + return value; + } +} +function proxyAuthorizationHeader(proxy) { + if (!proxy.username && !proxy.password) + return undefined; + const user = decodeUserinfo(proxy.username); + const pass = decodeUserinfo(proxy.password); + const token = Buffer.from(`${user}:${pass}`, "utf8").toString("base64"); + return `Basic ${token}`; +} +/** + * Open a plain TCP connection to the proxy and establish an HTTP CONNECT tunnel + * to `targetHost:targetPort`. Returns the tunneled socket ready for TLS. + */ +export async function openHttpsConnectTunnel(options) { + const targetPort = options.targetPort ?? 443; + const proxy = options.proxy; + if (proxy.protocol !== "http:") { + throw new CursorTransportError(`HTTPS CONNECT proxy must use http:// (got ${proxy.protocol}); https:// proxies are not supported`, { transient: false, replaySafe: true, code: "CURSOR_PROXY_UNSUPPORTED" }); + } + const { host: proxyHost, port: proxyPort } = proxyEndpoint(proxy); + const connect = options.connect ?? net.connect; + const authority = `${options.targetHost}:${targetPort}`; + const auth = proxyAuthorizationHeader(proxy); + const request = [ + `CONNECT ${authority} HTTP/1.1`, + `Host: ${authority}`, + "Proxy-Connection: keep-alive", + ...(auth ? [`Proxy-Authorization: ${auth}`] : []), + "", + "", + ].join("\r\n"); + if (options.signal?.aborted) { + throw new CursorTransportError("HTTPS CONNECT tunnel aborted before connect", { + transient: true, + replaySafe: true, + code: "CURSOR_PROXY_ABORTED", + cause: options.signal.reason, + }); + } + const socket = await new Promise((resolve, reject) => { + const s = connect(proxyPort, proxyHost); + const onAbort = () => { + cleanup(); + try { + s.destroy(); + } + catch { /* ignore */ } + reject(new CursorTransportError("HTTPS CONNECT tunnel aborted during TCP connect", { + transient: true, + replaySafe: true, + code: "CURSOR_PROXY_ABORTED", + cause: options.signal?.reason, + })); + }; + const cleanup = () => { + s.removeListener("connect", onConnect); + s.removeListener("error", onError); + options.signal?.removeEventListener("abort", onAbort); + }; + const onError = (error) => { + cleanup(); + reject(new CursorTransportError(`HTTPS CONNECT proxy TCP connect to ${proxyHost}:${proxyPort} failed`, { transient: true, replaySafe: true, code: "CURSOR_PROXY_CONNECT_FAILED", cause: error })); + }; + const onConnect = () => { + cleanup(); + resolve(s); + }; + options.signal?.addEventListener("abort", onAbort, { once: true }); + s.once("connect", onConnect); + s.once("error", onError); + }); + await new Promise((resolve, reject) => { + let settled = false; + let buffer = Buffer.alloc(0); + const onAbort = () => { + fail(new CursorTransportError("HTTPS CONNECT tunnel aborted during handshake", { + transient: true, + replaySafe: true, + code: "CURSOR_PROXY_ABORTED", + cause: options.signal?.reason, + })); + }; + const cleanup = () => { + socket.removeListener("data", onData); + socket.removeListener("error", onError); + socket.removeListener("close", onClose); + options.signal?.removeEventListener("abort", onAbort); + }; + const fail = (error) => { + if (settled) + return; + settled = true; + cleanup(); + try { + socket.destroy(); + } + catch { /* ignore */ } + reject(error); + }; + const onError = (error) => { + fail(new CursorTransportError("HTTPS CONNECT tunnel socket error", { + transient: true, + replaySafe: true, + code: "CURSOR_PROXY_CONNECT_FAILED", + cause: error, + })); + }; + const onClose = () => { + fail(new CursorTransportError("HTTPS CONNECT tunnel closed before response", { + transient: true, + replaySafe: true, + code: "CURSOR_PROXY_CONNECT_FAILED", + })); + }; + const onData = (chunk) => { + buffer = Buffer.concat([buffer, chunk]); + const headerEnd = buffer.indexOf("\r\n\r\n"); + if (headerEnd === -1) { + if (buffer.length > 64_000) { + fail(new CursorTransportError("HTTPS CONNECT response headers too large", { + transient: true, + replaySafe: true, + code: "CURSOR_PROXY_CONNECT_FAILED", + })); + } + return; + } + const headerText = buffer.subarray(0, headerEnd).toString("utf8"); + const statusLine = headerText.split("\r\n", 1)[0] ?? ""; + const match = /^HTTP\/\d\.\d\s+(\d{3})\b/i.exec(statusLine); + const status = match ? Number(match[1]) : NaN; + // RFC 9110 §9.3.6: any 2xx response to CONNECT establishes the tunnel. + if (!(status >= 200 && status < 300)) { + // Same classification as other HTTP failures: a proxy denial (403/407) + // will not succeed on retry; 429/5xx and unparseable replies might. + const transient = Number.isNaN(status) || status === 429 || status >= 500; + fail(new CursorTransportError(`HTTPS CONNECT to ${authority} via ${proxyHost}:${proxyPort} failed: ${statusLine || "no status"}`, { transient, replaySafe: true, code: "CURSOR_PROXY_CONNECT_REJECTED" })); + return; + } + const rest = buffer.subarray(headerEnd + 4); + if (settled) + return; + settled = true; + cleanup(); + if (rest.length > 0) + socket.unshift(rest); + resolve(); + }; + options.signal?.addEventListener("abort", onAbort, { once: true }); + socket.on("data", onData); + socket.once("error", onError); + socket.once("close", onClose); + try { + socket.write(request); + } + catch (error) { + fail(new CursorTransportError("HTTPS CONNECT request write failed", { + transient: true, + replaySafe: true, + code: "CURSOR_PROXY_CONNECT_FAILED", + cause: error, + })); + } + }); + return socket; +} +/** CONNECT through the proxy, then complete TLS (ALPN h2) on the native socket. */ +export async function openProxiedTlsSocket(options) { + const targetPort = options.targetPort ?? 443; + const plain = await openHttpsConnectTunnel({ + proxy: options.proxy, + targetHost: options.targetHost, + targetPort, + signal: options.signal, + connect: options.connect, + }); + const tlsConnect = options.tlsConnect ?? tls.connect; + return await new Promise((resolve, reject) => { + const tlsError = (cause) => new CursorTransportError(`HTTPS CONNECT TLS to ${options.targetHost} failed`, { transient: true, replaySafe: true, code: "CURSOR_PROXY_TLS_FAILED", cause }); + const abortError = () => new CursorTransportError("HTTPS CONNECT TLS handshake aborted", { + transient: true, + replaySafe: true, + code: "CURSOR_PROXY_ABORTED", + cause: options.signal?.reason, + }); + if (options.signal?.aborted) { + try { + plain.destroy(); + } + catch { /* ignore */ } + reject(abortError()); + return; + } + let socket; + try { + socket = tlsConnect({ + socket: plain, + servername: options.targetHost, + ALPNProtocols: ["h2"], + }); + } + catch (error) { + try { + plain.destroy(); + } + catch { /* ignore */ } + reject(tlsError(error)); + return; + } + let settled = false; + const cleanup = () => { + socket.removeListener("secureConnect", onSecure); + socket.removeListener("error", onError); + options.signal?.removeEventListener("abort", onAbort); + }; + const fail = (error) => { + if (settled) + return; + settled = true; + cleanup(); + try { + socket.destroy(); + } + catch { /* ignore */ } + try { + plain.destroy(); + } + catch { /* ignore */ } + reject(error); + }; + const onAbort = () => fail(abortError()); + const onError = (error) => fail(tlsError(error)); + const onSecure = () => { + if (settled) + return; + settled = true; + cleanup(); + resolve(socket); + }; + options.signal?.addEventListener("abort", onAbort, { once: true }); + socket.once("secureConnect", onSecure); + socket.once("error", onError); + }); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/usage.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/usage.d.ts new file mode 100644 index 00000000..68f91f56 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/usage.d.ts @@ -0,0 +1,86 @@ +import type { LanguageModelV3Usage } from "@ai-sdk/provider"; +import type { CursorContextUsageSource, CursorConversationTokenDetails } from "./protocol/token-details.js"; +export type CursorUsageCounters = { + inputTokens: number; + outputTokens: number; + cacheRead: number; + cacheWrite: number; + reasoningTokens: number; +}; +export type CursorUsageOptions = { + /** Cursor checkpoint occupancy, including the current turn's output. */ + contextTotalTokens?: number; + /** Previous turn's occupancy. When Cursor's cache read covers this window, do not dilute the hit by multi-step TurnEnded aggregates. */ + priorContextTokens?: number; +}; +export type CursorCacheDiagnosticStats = { + sessionKey?: string; + conversationId: string; + conversationGroupId?: string; + modelId?: string; + startedWithCheckpoint: boolean; + requestContextReused: boolean; + requestContextHash: string; + systemPromptHash?: string; + checkpointUpdates: number; + tokenDetailUpdates: number; + pumpPasses: number; + stepStarts: number; + stepCompletes: number; + displayToolCalls: number; + execRequests: number; + /** A CreatePlan interaction ran this Run (any outcome). Tags one-time upstream tools expansion. */ + createPlanInTurn?: boolean; + /** A SwitchMode interaction ran this Run (any outcome). */ + switchModeInTurn?: boolean; +}; +/** Non-negative integer counter from a Cursor `turn_ended` field. */ +export declare function turnEndedCounter(te: Record, key: string): number; +export declare function cursorUsageCountersFromTurnEnded(te: Record): CursorUsageCounters; +/** + * Map Cursor counters to AI SDK V3. Cursor's `input_tokens` already includes + * cache reads/writes, and `output_tokens` already includes reasoning. + */ +export declare function buildLanguageModelV3UsageFromCounters(counters: CursorUsageCounters, options?: CursorUsageOptions): LanguageModelV3Usage; +export declare function buildLanguageModelV3UsageFromTurnEnded(te: Record, options?: CursorUsageOptions): LanguageModelV3Usage; +/** Compact, parseable category snapshot for checkpoint-by-checkpoint traces. */ +export declare function formatCursorTokenCategories(details: CursorConversationTokenDetails | undefined): string; +/** + * Explain Cursor's aggregate cache counters using the state visible to this + * client. `stepStarts` is deliberately not called a model-call count: Cursor + * does not expose per-model-call cache accounting on the Run stream. + */ +export declare function formatCursorCacheDiagnostics(counters: CursorUsageCounters, current: CursorConversationTokenDetails | undefined, prior: CursorConversationTokenDetails | undefined, stats: CursorCacheDiagnosticStats): string; +/** One-line proof that Cursor, AI SDK, and projected OpenCode totals agree. */ +export declare function formatTurnUsageValidation(counters: CursorUsageCounters, usage: LanguageModelV3Usage, tokenDetails?: CursorConversationTokenDetails, contextSource?: CursorContextUsageSource): string; +/** OpenCode requires a usage object at every step boundary. */ +export declare function emptyLanguageModelV3Usage(): LanguageModelV3Usage; +/** + * OpenCode TUI/GUI replace each assistant message's `tokens` (they do not sum + * occupancy). The TUI footer picks the last assistant with `tokens.output > 0`. + * Session cost, however, adds every step-finish. Checkpoint occupancy is + * therefore sent as a snapshot with `output=1` so the footer accepts it, and + * callers attach {@link OPENCODE_DISPLAY_ONLY_COST_METADATA} so getUsage + * reports $0 instead of billing the snapshot as a new prompt. + */ +export declare const OPENCODE_DISPLAY_ONLY_COST_METADATA: { + readonly copilot: { + readonly totalNanoAiu: 0; + }; +}; +/** + * Counters that mirror {@link occupancyUsageFromTokenDetails} for + * {@link formatTurnUsageValidation}. Always validate occupancy finishes — + * including TurnEnded/stop — against these, not against aggregate TurnEnded + * request counters. Request cache ratios stay on `finish:` / cache diagnosis. + */ +export declare function occupancyValidationCounters(details: CursorConversationTokenDetails, prior?: CursorConversationTokenDetails): CursorUsageCounters; +export declare function occupancyUsageFromTokenDetails(details: CursorConversationTokenDetails, prior?: CursorConversationTokenDetails): LanguageModelV3Usage; +/** Project nested V3 usage into the common flat AI-SDK counter shape. */ +export declare function flatUsageFromV3(usage: LanguageModelV3Usage): { + inputTokens: number; + outputTokens: number; + reasoningTokens: number; + cacheReadInputTokens: number; + cacheWriteInputTokens: number; +}; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/usage.js b/references/upstream-plugins/cursor-opencode-provider/dist/usage.js new file mode 100644 index 00000000..d08a1d4f --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/usage.js @@ -0,0 +1,300 @@ +/** Non-negative integer counter from a Cursor `turn_ended` field. */ +export function turnEndedCounter(te, key) { + const value = te[key]; + return typeof value === "number" && Number.isFinite(value) && value >= 0 + ? Math.trunc(value) + : 0; +} +export function cursorUsageCountersFromTurnEnded(te) { + return { + inputTokens: turnEndedCounter(te, "input_tokens"), + outputTokens: turnEndedCounter(te, "output_tokens"), + cacheRead: turnEndedCounter(te, "cache_read"), + cacheWrite: turnEndedCounter(te, "cache_write"), + reasoningTokens: turnEndedCounter(te, "reasoning_tokens"), + }; +} +/** + * Map Cursor counters to AI SDK V3. Cursor's `input_tokens` already includes + * cache reads/writes, and `output_tokens` already includes reasoning. + */ +export function buildLanguageModelV3UsageFromCounters(counters, options = {}) { + const rawInput = Math.max(0, Math.trunc(counters.inputTokens)); + const rawOutput = Math.max(0, Math.trunc(counters.outputTokens)); + const rawCacheRead = Math.min(Math.max(0, Math.trunc(counters.cacheRead)), rawInput); + const rawCacheWrite = Math.min(Math.max(0, Math.trunc(counters.cacheWrite)), rawInput - rawCacheRead); + const rawReasoning = Math.min(Math.max(0, Math.trunc(counters.reasoningTokens)), rawOutput); + const contextTotal = options.contextTotalTokens; + const hasContextTotal = typeof contextTotal === "number" && Number.isFinite(contextTotal) && contextTotal >= 0; + const total = hasContextTotal ? Math.trunc(contextTotal) : rawInput + rawOutput; + const output = Math.min(rawOutput, total); + const input = Math.max(0, total - output); + // TurnEnded can aggregate several internal model calls, so its absolute input + // and cache counts can exceed the final checkpoint occupancy. Preserve the + // cache proportions while normalizing the partition to Cursor's context total. + const proportionalRead = rawInput > 0 + ? Math.min(input, Math.round(input * rawCacheRead / rawInput)) + : 0; + const priorContext = options.priorContextTokens; + const prefixRead = typeof priorContext === "number" + && Number.isFinite(priorContext) + && priorContext > 0 + && rawCacheRead >= priorContext + ? Math.min(input, Math.trunc(priorContext)) + : 0; + const cacheRead = Math.min(input, Math.max(proportionalRead, prefixRead)); + const cacheWrite = rawInput > 0 + ? Math.min(input - cacheRead, Math.round(input * rawCacheWrite / rawInput)) + : 0; + const reasoning = rawOutput > 0 + ? Math.min(output, Math.round(output * rawReasoning / rawOutput)) + : 0; + return { + inputTokens: { + total: input, + noCache: Math.max(input - cacheRead - cacheWrite, 0), + cacheRead, + cacheWrite, + }, + outputTokens: { + total: output, + text: Math.max(output - reasoning, 0), + reasoning, + }, + }; +} +export function buildLanguageModelV3UsageFromTurnEnded(te, options = {}) { + return buildLanguageModelV3UsageFromCounters(cursorUsageCountersFromTurnEnded(te), options); +} +function usageCount(value) { + return typeof value === "number" && Number.isFinite(value) && value >= 0 + ? Math.trunc(value) + : 0; +} +function usageRatio(part, total) { + return total > 0 ? `${(part / total * 100).toFixed(1)}%` : "n/a"; +} +function categoryTokens(details) { + return new Map(details?.breakdown?.categories.map((category) => [ + category.id || category.label || "(unnamed)", + category.estimatedTokens, + ]) ?? []); +} +/** Compact, parseable category snapshot for checkpoint-by-checkpoint traces. */ +export function formatCursorTokenCategories(details) { + const categories = Object.fromEntries(categoryTokens(details)); + return Object.keys(categories).length > 0 ? JSON.stringify(categories) : "unavailable"; +} +/** + * Explain Cursor's aggregate cache counters using the state visible to this + * client. `stepStarts` is deliberately not called a model-call count: Cursor + * does not expose per-model-call cache accounting on the Run stream. + */ +export function formatCursorCacheDiagnostics(counters, current, prior, stats) { + const rawInput = Math.max(0, Math.trunc(counters.inputTokens)); + const rawRead = Math.min(rawInput, Math.max(0, Math.trunc(counters.cacheRead))); + const rawWrite = Math.min(rawInput - rawRead, Math.max(0, Math.trunc(counters.cacheWrite))); + const rawUncached = Math.max(0, rawInput - rawRead - rawWrite); + const priorCategories = categoryTokens(prior); + const currentCategories = categoryTokens(current); + const categoriesComparable = !!prior?.breakdown && !!current?.breakdown; + const categoryDelta = {}; + let sameSizedCategoryTokens = 0; + if (categoriesComparable) { + for (const id of new Set([...priorCategories.keys(), ...currentCategories.keys()])) { + const before = priorCategories.get(id); + const after = currentCategories.get(id); + if (before === undefined) + categoryDelta[id] = "new"; + else if (after === undefined) + categoryDelta[id] = "removed"; + else { + categoryDelta[id] = after - before; + if (after === before) + sameSizedCategoryTokens += after; + } + } + } + const continuity = stats.startedWithCheckpoint + ? prior ? "warm" : "checkpoint-without-token-details" + : "cold"; + const contextDelta = current && prior ? current.usedTokens - prior.usedTokens : undefined; + const toolsDelta = categoryDelta.tools; + // When our RequestContext tools overlay bytes are unchanged but Cursor's + // tools category still moves, the churn is upstream accounting (e.g. plan + // mode / CreatePlan internals), not a client prefix rebuild. + const toolsCategoryChurn = stats.requestContextReused + && typeof toolsDelta === "number" + && toolsDelta !== 0 + ? "upstream-stable-overlay" + : !stats.requestContextReused + && typeof toolsDelta === "number" + && toolsDelta !== 0 + ? "client-overlay-changed" + : "none"; + return [ + "cache diagnosis:", + `sessionKey=${stats.sessionKey ?? "-"}`, + `conversationId=${stats.conversationId}`, + `conversationGroupId=${stats.conversationGroupId ?? "-"}`, + `model=${stats.modelId ?? "-"}`, + `continuity=${continuity}`, + `rawInput=${rawInput}`, + `rawCacheRead=${rawRead}`, + `rawCacheWrite=${rawWrite}`, + `rawUncached=${rawUncached}`, + `rawReadRatio=${usageRatio(rawRead, rawInput)}`, + `rawWriteRatio=${usageRatio(rawWrite, rawInput)}`, + `priorContext=${prior?.usedTokens ?? "unavailable"}`, + `currentContext=${current?.usedTokens ?? "unavailable"}`, + `contextDelta=${contextDelta ?? "unavailable"}`, + `rawReadVsPriorContext=${prior ? usageRatio(rawRead, prior.usedTokens) : "n/a"}`, + `sameSizedCategoryTokens=${categoriesComparable ? sameSizedCategoryTokens : "unavailable"}`, + `categoryDelta=${categoriesComparable && Object.keys(categoryDelta).length > 0 ? JSON.stringify(categoryDelta) : "unavailable"}`, + `toolsCategoryChurn=${toolsCategoryChurn}`, + `requestContext=${stats.requestContextReused ? "reused" : "built"}`, + `requestContextHash=${stats.requestContextHash.slice(0, 16)}`, + `systemPromptHash=${stats.systemPromptHash?.slice(0, 16) ?? "none"}`, + `systemPromptSent=${!stats.startedWithCheckpoint}`, + `checkpointUpdates=${stats.checkpointUpdates}`, + `tokenDetailUpdates=${stats.tokenDetailUpdates}`, + `pumpPasses=${stats.pumpPasses}`, + `steps=${stats.stepStarts}/${stats.stepCompletes}`, + `displayToolCalls=${stats.displayToolCalls}`, + `execRequests=${stats.execRequests}`, + `createPlanInTurn=${stats.createPlanInTurn === true}`, + `switchModeInTurn=${stats.switchModeInTurn === true}`, + "perModelCallCache=unavailable", + ].join(" "); +} +/** One-line proof that Cursor, AI SDK, and projected OpenCode totals agree. */ +export function formatTurnUsageValidation(counters, usage, tokenDetails, contextSource) { + const input = usageCount(usage.inputTokens.total); + const noCache = usageCount(usage.inputTokens.noCache); + const cacheRead = usageCount(usage.inputTokens.cacheRead); + const cacheWrite = usageCount(usage.inputTokens.cacheWrite); + const inputParts = noCache + cacheRead + cacheWrite; + const output = usageCount(usage.outputTokens.total); + const text = usageCount(usage.outputTokens.text); + const reasoning = usageCount(usage.outputTokens.reasoning); + const outputParts = text + reasoning; + const sentTotal = input + output; + const projectedOpenCodeTotal = inputParts + outputParts; + const rawTotal = counters.inputTokens + counters.outputTokens; + const cursor = tokenDetails + ? `${tokenDetails.usedTokens}/${tokenDetails.maxTokens}` + + `(${usageRatio(tokenDetails.usedTokens, tokenDetails.maxTokens)})` + : "unavailable"; + const totalMatch = tokenDetails ? String(sentTotal === tokenDetails.usedTokens) : "unavailable"; + const breakdown = tokenDetails?.breakdown; + const categorySum = breakdown?.categories.reduce((sum, category) => sum + category.estimatedTokens, 0); + const breakdownMatch = breakdown && categorySum !== undefined + ? breakdown.totalUsedTokens === tokenDetails.usedTokens && + categorySum === breakdown.totalUsedTokens + : undefined; + const rawCached = counters.cacheRead + counters.cacheWrite; + const sentCached = cacheRead + cacheWrite; + const proportionalCached = counters.inputTokens > 0 && input > 0 + ? Math.round(input * rawCached / counters.inputTokens) + : 0; + const cacheRatioMatch = tokenDetails + ? counters.inputTokens > 0 && input > 0 + ? Math.abs(rawCached / counters.inputTokens - sentCached / input) <= 1 / input + || (sentCached >= proportionalCached && sentCached <= input) + : rawCached === 0 && sentCached === 0 + : undefined; + const status = input === inputParts && + output === outputParts && + projectedOpenCodeTotal === sentTotal && + (cacheRatioMatch ?? true) && + (!tokenDetails || sentTotal === tokenDetails.usedTokens) && + (breakdownMatch ?? true) + ? "ok" + : "mismatch"; + return [ + "turn usage validation:", + `status=${status}`, + `source=${tokenDetails ? contextSource ?? "checkpoint-current-run" : "unavailable"}`, + `cursor=${cursor}`, + `rawTotal=${rawTotal}`, + `sentTotal=${sentTotal}`, + `totalMatch=${totalMatch}`, + `input=${input}`, + `inputParts=${inputParts}`, + `inputMatch=${input === inputParts}`, + `output=${output}`, + `outputParts=${outputParts}`, + `outputMatch=${output === outputParts}`, + `opencodeProjectedTotal=${projectedOpenCodeTotal}`, + `opencodeMatch=${projectedOpenCodeTotal === sentTotal}`, + `breakdownTotal=${breakdown?.totalUsedTokens ?? "unavailable"}`, + `categorySum=${categorySum ?? "unavailable"}`, + `breakdownMatch=${breakdownMatch ?? "unavailable"}`, + `rawCachedRatio=${usageRatio(rawCached, counters.inputTokens)}`, + `sentCachedRatio=${usageRatio(sentCached, input)}`, + `cacheRatioMatch=${cacheRatioMatch ?? "unavailable"}`, + ].join(" "); +} +/** OpenCode requires a usage object at every step boundary. */ +export function emptyLanguageModelV3Usage() { + return buildLanguageModelV3UsageFromCounters({ + inputTokens: 0, + outputTokens: 0, + cacheRead: 0, + cacheWrite: 0, + reasoningTokens: 0, + }); +} +/** + * OpenCode TUI/GUI replace each assistant message's `tokens` (they do not sum + * occupancy). The TUI footer picks the last assistant with `tokens.output > 0`. + * Session cost, however, adds every step-finish. Checkpoint occupancy is + * therefore sent as a snapshot with `output=1` so the footer accepts it, and + * callers attach {@link OPENCODE_DISPLAY_ONLY_COST_METADATA} so getUsage + * reports $0 instead of billing the snapshot as a new prompt. + */ +export const OPENCODE_DISPLAY_ONLY_COST_METADATA = { + copilot: { totalNanoAiu: 0 }, +}; +/** + * Counters that mirror {@link occupancyUsageFromTokenDetails} for + * {@link formatTurnUsageValidation}. Always validate occupancy finishes — + * including TurnEnded/stop — against these, not against aggregate TurnEnded + * request counters. Request cache ratios stay on `finish:` / cache diagnosis. + */ +export function occupancyValidationCounters(details, prior) { + const used = Math.max(0, Math.trunc(details.usedTokens)); + return { + inputTokens: used, + outputTokens: used > 0 ? 1 : 0, + cacheRead: Math.max(0, Math.trunc(prior?.usedTokens ?? 0)), + cacheWrite: 0, + reasoningTokens: 0, + }; +} +export function occupancyUsageFromTokenDetails(details, prior) { + const used = Math.max(0, Math.trunc(details.usedTokens)); + if (used <= 0) + return emptyLanguageModelV3Usage(); + return buildLanguageModelV3UsageFromCounters(occupancyValidationCounters(details, prior), { + contextTotalTokens: used, + priorContextTokens: prior?.usedTokens, + }); +} +/** Project nested V3 usage into the common flat AI-SDK counter shape. */ +export function flatUsageFromV3(usage) { + const input = usage.inputTokens; + const output = usage.outputTokens; + const noCache = input.noCache ?? 0; + const cacheRead = input.cacheRead ?? 0; + const cacheWrite = input.cacheWrite ?? 0; + const reasoning = output.reasoning ?? 0; + const text = output.text ?? Math.max(0, (output.total ?? 0) - reasoning); + return { + inputTokens: noCache, + outputTokens: text, + reasoningTokens: reasoning, + cacheReadInputTokens: cacheRead, + cacheWriteInputTokens: cacheWrite, + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/web-search-tool.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/web-search-tool.d.ts new file mode 100644 index 00000000..3fc5fe74 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/web-search-tool.d.ts @@ -0,0 +1,29 @@ +import { executeOpenCodeWebSearch } from "./web-tools.js"; +type ToolFactory = { + tool: (input: Record) => Record; + schema: { + string: () => any; + number: () => any; + enum: (values: readonly string[]) => any; + }; +}; +/** Build the classic OpenCode web-search tool with the host's own Zod helper. */ +export declare function createOpenCodeWebSearchTool(factory: ToolFactory): Record; +/** + * Host-neutral JSON-schema fallback when the classic helper is unavailable. + * OpenCode's legacy schema adapter marks every listed property required, so + * expose only the genuinely required query rather than turning four optional + * tuning fields into mandatory inputs. + */ +export declare const openCodeWebSearchTool: { + description: string; + args: { + query: { + type: string; + description: string; + }; + }; + execute: typeof executeOpenCodeWebSearch; +}; +export type { ToolFactory as OpenCodeToolFactory }; +export declare function createOpenCodeWebSearchToolFromPlugin(pluginModule: any): Record; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/web-search-tool.js b/references/upstream-plugins/cursor-opencode-provider/dist/web-search-tool.js new file mode 100644 index 00000000..92e25726 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/web-search-tool.js @@ -0,0 +1,32 @@ +import { executeOpenCodeWebSearch } from "./web-tools.js"; +/** Build the classic OpenCode web-search tool with the host's own Zod helper. */ +export function createOpenCodeWebSearchTool(factory) { + const schema = factory.schema; + return factory.tool({ + description: "Search the web for current information using OpenCode's web search backend.", + args: { + query: schema.string().describe("Web search query"), + numResults: schema.number().int().min(1).max(20).optional(), + livecrawl: schema.enum(["fallback", "preferred"]).optional(), + type: schema.enum(["auto", "fast", "deep"]).optional(), + contextMaxCharacters: schema.number().int().positive().optional(), + }, + execute: executeOpenCodeWebSearch, + }); +} +/** + * Host-neutral JSON-schema fallback when the classic helper is unavailable. + * OpenCode's legacy schema adapter marks every listed property required, so + * expose only the genuinely required query rather than turning four optional + * tuning fields into mandatory inputs. + */ +export const openCodeWebSearchTool = { + description: "Search the web for current information using OpenCode's web search backend.", + args: { + query: { type: "string", description: "Web search query" }, + }, + execute: executeOpenCodeWebSearch, +}; +export function createOpenCodeWebSearchToolFromPlugin(pluginModule) { + return createOpenCodeWebSearchTool({ tool: pluginModule.tool, schema: pluginModule.tool.schema }); +} diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/web-tools.d.ts b/references/upstream-plugins/cursor-opencode-provider/dist/web-tools.d.ts new file mode 100644 index 00000000..de8bd103 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/web-tools.d.ts @@ -0,0 +1,31 @@ +import type { ToolContext, ToolResult } from "@opencode-ai/plugin"; +export type OpenCodeWebSearchArgs = { + query: string; + numResults?: number; + livecrawl?: "fallback" | "preferred"; + type?: "auto" | "fast" | "deep"; + contextMaxCharacters?: number; +}; +export declare function parseOpenCodeWebSearchResponse(raw: string): string | undefined; +/** + * Raw Exa web-search call: no host tool context, no permission prompt. + * + * Split out so both the classic plugin's `custom_websearch` tool and the + * OpenCode 2.0 plugin's tool registration share one implementation — 2.0's + * ToolContext has no `ask`, permissions being handled by the host instead. + */ +export type OpenCode2WebSearchResult = { + url: string; + title?: string; + content?: string; + time: { + published?: number; + }; +}; +/** + * Turn an Exa MCP text blob into OpenCode 2.0 `websearch` results + * (`{ url, title?, content?, time }`). Unknown shapes yield an empty list. + */ +export declare function parseExaWebSearchResults(raw: string): OpenCode2WebSearchResult[]; +export declare function fetchOpenCodeWebSearchText(args: OpenCodeWebSearchArgs, signal: AbortSignal | undefined, fetchImpl?: typeof fetch): Promise; +export declare function executeOpenCodeWebSearch(args: OpenCodeWebSearchArgs, context: ToolContext, fetchImpl?: typeof fetch): Promise; diff --git a/references/upstream-plugins/cursor-opencode-provider/dist/web-tools.js b/references/upstream-plugins/cursor-opencode-provider/dist/web-tools.js new file mode 100644 index 00000000..aab6e2e4 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/dist/web-tools.js @@ -0,0 +1,203 @@ +const EXA_MCP_URL = "https://mcp.exa.ai/mcp"; +const WEB_SEARCH_TIMEOUT_MS = 25_000; +function exaMcpUrl() { + const apiKey = process.env.EXA_API_KEY; + if (!apiKey) + return EXA_MCP_URL; + const url = new URL(EXA_MCP_URL); + url.searchParams.set("exaApiKey", apiKey); + return url.href; +} +function mcpText(value) { + if (!value || typeof value !== "object") + return undefined; + const result = value.result; + if (!result || typeof result !== "object") + return undefined; + const content = result.content; + if (!Array.isArray(content)) + return undefined; + for (const item of content) { + if (item && + typeof item === "object" && + item.type === "text" && + typeof item.text === "string") { + return item.text; + } + } + return undefined; +} +export function parseOpenCodeWebSearchResponse(raw) { + const trimmed = raw.trim(); + if (!trimmed) + return undefined; + try { + const text = mcpText(JSON.parse(trimmed)); + if (text) + return text; + } + catch { + // MCP may respond as an SSE stream instead of one JSON object. + } + for (const line of raw.split("\n")) { + if (!line.startsWith("data: ")) + continue; + try { + const text = mcpText(JSON.parse(line.slice(6))); + if (text) + return text; + } + catch { + // Ignore non-JSON SSE events. + } + } + return undefined; +} +function tryParseJson(raw) { + try { + return JSON.parse(raw); + } + catch { + return undefined; + } +} +function publishedMs(value) { + if (typeof value === "number" && Number.isFinite(value)) { + return value < 1e12 ? Math.floor(value * 1000) : Math.floor(value); + } + if (typeof value === "string" && value.trim()) { + const parsed = Date.parse(value); + return Number.isFinite(parsed) ? parsed : undefined; + } + return undefined; +} +function asWebSearchResult(row) { + if (!row || typeof row !== "object") + return undefined; + const record = row; + const url = typeof record.url === "string" ? record.url + : typeof record.href === "string" ? record.href + : undefined; + if (!url) + return undefined; + const title = typeof record.title === "string" ? record.title : undefined; + const content = typeof record.content === "string" ? record.content + : typeof record.text === "string" ? record.text + : typeof record.snippet === "string" ? record.snippet + : undefined; + const published = publishedMs(record.publishedDate ?? record.published ?? record.date); + return { + url, + ...(title ? { title } : {}), + ...(content ? { content } : {}), + time: published !== undefined ? { published } : {}, + }; +} +function parseExaTextResults(text) { + return text.split(/\n\n---\n\n/).flatMap((block) => { + const url = block.match(/^URL:\s*(.+)$/m)?.[1]?.trim(); + if (!url) + return []; + const title = block.match(/^Title:\s*(.+)$/m)?.[1]?.trim(); + const publishedText = block.match(/^Published:\s*(.+)$/m)?.[1]?.trim(); + const published = publishedText && publishedText !== "N/A" + ? publishedMs(publishedText) + : undefined; + const content = block.match(/^(?:Highlights|Text):\s*\n?([\s\S]*)$/m)?.[1]?.trim(); + return [{ + url, + ...(title && title !== "N/A" ? { title } : {}), + ...(content ? { content } : {}), + time: published === undefined ? {} : { published }, + }]; + }); +} +/** + * Turn an Exa MCP text blob into OpenCode 2.0 `websearch` results + * (`{ url, title?, content?, time }`). Unknown shapes yield an empty list. + */ +export function parseExaWebSearchResults(raw) { + const text = parseOpenCodeWebSearchResponse(raw) ?? raw.trim(); + if (!text) + return []; + const parsed = tryParseJson(text); + const rows = Array.isArray(parsed) + ? parsed + : parsed && typeof parsed === "object" + ? Array.isArray(parsed.results) + ? parsed.results + : Array.isArray(parsed.data) + ? parsed.data + : [] + : []; + if (rows.length > 0) { + return rows.flatMap((row) => { + const result = asWebSearchResult(row); + return result ? [result] : []; + }); + } + return parseExaTextResults(text); +} +export async function fetchOpenCodeWebSearchText(args, signal, fetchImpl = fetch) { + const controller = new AbortController(); + const abort = () => controller.abort(signal?.reason); + if (signal?.aborted) + abort(); + else + signal?.addEventListener("abort", abort, { once: true }); + const timeout = setTimeout(() => controller.abort(new Error("Web search timed out")), WEB_SEARCH_TIMEOUT_MS); + try { + const response = await fetchImpl(exaMcpUrl(), { + method: "POST", + headers: { + accept: "application/json, text/event-stream", + "content-type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "web_search_exa", + arguments: { + query: args.query, + type: args.type ?? "auto", + numResults: args.numResults ?? 8, + livecrawl: args.livecrawl ?? "fallback", + contextMaxCharacters: args.contextMaxCharacters, + }, + }, + }), + signal: controller.signal, + }); + const raw = await response.text(); + if (!response.ok) + throw new Error(`Web search failed (${response.status}): ${raw.slice(0, 500)}`); + return parseOpenCodeWebSearchResponse(raw) ?? "No search results found. Please try a different query."; + } + finally { + clearTimeout(timeout); + signal?.removeEventListener("abort", abort); + } +} +export async function executeOpenCodeWebSearch(args, context, fetchImpl = fetch) { + await context.ask({ + permission: "websearch", + patterns: [args.query], + always: ["*"], + metadata: { + query: args.query, + numResults: args.numResults, + livecrawl: args.livecrawl, + type: args.type, + contextMaxCharacters: args.contextMaxCharacters, + provider: "exa", + }, + }); + const output = await fetchOpenCodeWebSearchText(args, context.abort, fetchImpl); + return { + title: `Exa Web Search: ${args.query}`, + output, + metadata: { provider: "exa" }, + }; +} diff --git a/references/upstream-plugins/cursor-opencode-provider/package.json b/references/upstream-plugins/cursor-opencode-provider/package.json new file mode 100644 index 00000000..b2fbbc80 --- /dev/null +++ b/references/upstream-plugins/cursor-opencode-provider/package.json @@ -0,0 +1,105 @@ +{ + "name": "cursor-opencode-provider", + "version": "0.7.3", + "description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol", + "type": "module", + "license": "MIT", + "author": "oakimov", + "repository": { + "type": "git", + "url": "git+https://github.com/oakimov/cursor-opencode-provider.git" + }, + "bugs": { + "url": "https://github.com/oakimov/cursor-opencode-provider/issues" + }, + "homepage": "https://github.com/oakimov/cursor-opencode-provider#readme", + "keywords": [ + "opencode", + "opencode-plugin", + "cursor", + "ai-sdk", + "provider", + "plugin" + ], + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./plugin": { + "types": "./dist/plugin.d.ts", + "import": "./dist/plugin.js" + }, + "./plugin/v2": { + "types": "./dist/plugin-v2.d.ts", + "import": "./dist/plugin-v2.js" + }, + "./plugin/opencode2": { + "types": "./dist/plugin-opencode2.d.ts", + "import": "./dist/plugin-opencode2.js" + }, + "./server": { + "types": "./dist/plugin-opencode2.d.ts", + "import": "./dist/plugin-opencode2.js" + }, + "./errors": { + "types": "./dist/errors.d.ts", + "import": "./dist/errors.js" + }, + "./models": { + "types": "./dist/models.d.ts", + "import": "./dist/models.js" + }, + "./pricing": { + "types": "./dist/pricing.d.ts", + "import": "./dist/pricing.js" + }, + "./auth": { + "types": "./dist/auth.d.ts", + "import": "./dist/auth.js" + }, + "./image-save": { + "types": "./dist/image-save.d.ts", + "import": "./dist/image-save.js" + } + }, + "files": [ + "dist", + "LICENSE", + "DISCLAIMER.md", + "SECURITY.md", + "README.md" + ], + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", + "test": "bun test", + "test:node-http2": "bun run build && node test/node-http2-cleanup.mjs", + "test:watch": "bun test --watch", + "generate:pricing": "bun run scripts/generate-cursor-pricing.ts", + "generate:pricing:check": "bun run scripts/generate-cursor-pricing.ts -- --check", + "check:pricing": "bun run scripts/check-cursor-pricing-coverage.ts", + "prepublishOnly": "bun run generate:pricing && bun run build" + }, + "dependencies": { + "@ai-sdk/provider": "3.0.15", + "protobufjs": "^7.6.5" + }, + "peerDependencies": { + "@opencode-ai/plugin": "^1.17.13" + }, + "peerDependenciesMeta": { + "@opencode-ai/plugin": { + "optional": true + } + }, + "devDependencies": { + "@opencode-ai/plugin": "^1.18.16", + "@tsconfig/node22": "^22.0.5", + "@types/bun": "^1.3.0", + "@types/node": "^22.20.1", + "typescript": "^5.9.3" + } +} diff --git a/references/upstream-plugins/lock.json b/references/upstream-plugins/lock.json index 801190ea..173edf66 100644 --- a/references/upstream-plugins/lock.json +++ b/references/upstream-plugins/lock.json @@ -1,20 +1,20 @@ { "plugins": { + "cursor-opencode-provider": { + "npmUrl": "https://www.npmjs.com/package/cursor-opencode-provider/v/0.7.3", + "packageName": "cursor-opencode-provider", + "publishedAt": "2026-09-25T23:54:22.656Z", + "referenceDir": "references/upstream-plugins/cursor-opencode-provider", + "repo": "oakimov/cursor-opencode-provider", + "version": "0.7.3" + }, "opencode-agy-auth": { - "npmUrl": "https://www.npmjs.com/package/%40anthonyhaussman/opencode-agy-auth/v/1.2.10", + "npmUrl": "https://www.npmjs.com/package/%40anthonyhaussman/opencode-agy-auth/v/1.2.11-alpha.0", "packageName": "@anthonyhaussman/opencode-agy-auth", - "publishedAt": "2026-09-24T08:25:32.764Z", + "publishedAt": "2026-09-25T06:34:38.455Z", "referenceDir": "references/upstream-plugins/opencode-agy-auth", "repo": "anthonyhaussman/opencode-agy-auth", - "version": "1.2.10" - }, - "opencode-cursor-oauth": { - "npmUrl": "https://www.npmjs.com/package/%40playwo/opencode-cursor-oauth/v/0.4.3", - "packageName": "@playwo/opencode-cursor-oauth", - "publishedAt": "2026-04-08T14:04:58.057Z", - "referenceDir": "references/upstream-plugins/opencode-cursor-oauth", - "repo": "PoolPirate/opencode-cursor", - "version": "0.4.3" + "version": "1.2.11-alpha.0" }, "opencode-gemini-auth": { "npmUrl": "https://www.npmjs.com/package/opencode-gemini-auth/v/2.0.1", diff --git a/references/upstream-plugins/opencode-agy-auth/LICENSE b/references/upstream-plugins/opencode-agy-auth/LICENSE new file mode 100644 index 00000000..d9a10c0d --- /dev/null +++ b/references/upstream-plugins/opencode-agy-auth/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/references/upstream-plugins/opencode-agy-auth/README.md b/references/upstream-plugins/opencode-agy-auth/README.md index e2f355f0..bb111bd8 100644 --- a/references/upstream-plugins/opencode-agy-auth/README.md +++ b/references/upstream-plugins/opencode-agy-auth/README.md @@ -8,6 +8,7 @@ An [OpenCode](https://opencode.ai/) authentication plugin that enables seamless - **Dynamic Model Retrieval**: Fetches available models based on user tier and current allocations. - **Quota Tracking**: Injects the `agy_quota` tool into OpenCode to check usage limits directly. - **Traffic Simulation**: Maintains background heartbeat with `agy` servers. +- **Dual OpenCode Compatibility**: Fully supports both OpenCode v1 (>= 1.18.29) and OpenCode v2 seamlessly without breaking changes. ## Installation diff --git a/references/upstream-plugins/opencode-agy-auth/dist/index.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/index.d.ts index b287489f..c24e4fc2 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/index.d.ts +++ b/references/upstream-plugins/opencode-agy-auth/dist/index.d.ts @@ -1,7 +1,8 @@ import { AgyCLIOAuthPlugin, GoogleOAuthPlugin } from "./src/plugin"; +import { setupOpenCodeV2, v2PluginDefinition } from "./src/plugin/v2"; +import type { DualOpenCodePlugin } from "./src/plugin/types"; export { AgyCLIOAuthPlugin, GoogleOAuthPlugin }; -declare const _default: { - id: string; - server: typeof AgyCLIOAuthPlugin; -}; -export default _default; +export { setupOpenCodeV2, v2PluginDefinition }; +export * from "./src/plugin/types"; +declare const dualPlugin: DualOpenCodePlugin; +export default dualPlugin; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/index.js b/references/upstream-plugins/opencode-agy-auth/dist/index.js index 3c6ce005..72978bfc 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/index.js +++ b/references/upstream-plugins/opencode-agy-auth/dist/index.js @@ -268,7 +268,7 @@ function createAgyActivityRequestId() { import os from "os"; // src/sdk/agy-cli-version.ts -var AGY_CLI_VERSION = "1.2.10"; +var AGY_CLI_VERSION = "1.2.11"; // src/sdk/user-agent.ts var cachedUserAgent = null; @@ -579,6 +579,12 @@ async function resolveQuotaResetDelay(accessToken, projectId, model, userAgentMo return null; } } +var retryInternals = { + parseRetryDelayValue, + parseRetryDelayFromMessage, + findResetTimeForModel, + resolveQuotaResetDelay +}; // src/sdk/retry/helpers.ts var DEFAULT_MAX_ATTEMPTS = 3; @@ -1575,7 +1581,11 @@ function createOAuthAuthorizeMethod(options) { const isHeadless = !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.OPENCODE_HEADLESS); const authorization = await authorizeAgy(); if (!isHeadless) { - openBrowserUrl(authorization.url); + if (options?.openBrowser) { + options.openBrowser(authorization.url); + } else { + openBrowserUrl(authorization.url); + } } return { url: authorization.url, @@ -1629,16 +1639,23 @@ function parseOAuthCallbackInput(input) { } return { code: trimmed }; } +function defaultOpenBrowserLauncher(command, args) { + const child = spawn(command, args, { + stdio: "ignore", + detached: true + }); + child.unref?.(); +} +var openBrowserLauncher = defaultOpenBrowserLauncher; function openBrowserUrl(url2) { + if ((process.env.NODE_ENV === "test" || process.env.VITEST) && openBrowserLauncher === defaultOpenBrowserLauncher) { + return; + } try { const platform2 = process.platform; const command = platform2 === "darwin" ? "open" : platform2 === "win32" ? "rundll32" : "xdg-open"; const args = platform2 === "win32" ? ["url.dll,FileProtocolHandler", url2] : [url2]; - const child = spawn(command, args, { - stdio: "ignore", - detached: true - }); - child.unref?.(); + openBrowserLauncher(command, args); } catch { } } @@ -15320,6 +15337,12 @@ function formatRelativeResetTime(resetTime) { // src/plugin/quota.ts var AGY_QUOTA_TOOL_NAME = "agy_quota"; +var AGY_V2_QUOTA_COMMAND = "agy-quota"; +var AGY_V2_QUOTA_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota usage for the current authenticated account. + +Immediately call \`${AGY_QUOTA_TOOL_NAME}\` with no arguments and return its output verbatim. +Do not call other tools. +`; function createAgyQuotaTool({ client, getAuthResolver, @@ -15559,6 +15582,12 @@ function splitModelVariant(modelId) { // src/plugin/quota-summary.ts var AGY_QUOTA_SUMMARY_TOOL_NAME = "agy_quota_summary"; +var AGY_V2_QUOTA_SUMMARY_COMMAND = "agy-quota-summary"; +var AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota summary (weekly and 5-hour limits by model group) for the current authenticated account. + +Immediately call \`${AGY_QUOTA_SUMMARY_TOOL_NAME}\` with no arguments and return its output verbatim. +Do not call other tools. +`; function createAgyQuotaSummaryTool({ client, getAuthResolver, @@ -16056,6 +16085,7 @@ var models_default = { 106143956, 105856899, 106064030, + 106264162, 106711598, 106106760, 106021688, @@ -16081,6 +16111,7 @@ var models_default = { 106121399, 106100654, 106064028, + 106264150, 105906495, 106283614, 106640124, @@ -16147,7 +16178,7 @@ var models_default = { modelProvider: "MODEL_PROVIDER_ANTHROPIC", quotaInfo: { remainingFraction: 1, - resetTime: "2026-09-24T13:14:02Z" + resetTime: "2026-09-25T11:24:59Z" }, recommended: true, supportedMimeTypes: { @@ -16186,7 +16217,7 @@ var models_default = { modelProvider: "MODEL_PROVIDER_ANTHROPIC", quotaInfo: { remainingFraction: 1, - resetTime: "2026-09-24T13:14:02Z" + resetTime: "2026-09-25T11:24:59Z" }, recommended: true, supportedMimeTypes: { @@ -16222,8 +16253,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -16305,8 +16336,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -16388,8 +16419,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -16468,8 +16499,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, requiresImageOutputOutsideFunctionResponses: true, @@ -16549,8 +16580,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -16654,8 +16685,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -16724,8 +16755,8 @@ var models_default = { model: "MODEL_PLACEHOLDER_M21", modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" } }, "gemini-3.1-flash-lite": { @@ -16743,8 +16774,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" } }, "gemini-3.1-pro-high": { @@ -16775,8 +16806,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -16866,8 +16897,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -16971,8 +17002,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17054,8 +17085,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17161,8 +17192,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17280,8 +17311,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17399,8 +17430,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17518,8 +17549,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17636,8 +17667,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17747,8 +17778,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17860,8 +17891,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -17973,8 +18004,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -18085,8 +18116,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -18196,8 +18227,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -18309,8 +18340,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -18422,8 +18453,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -18534,8 +18565,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -18624,8 +18655,8 @@ var models_default = { }, modelProvider: "MODEL_PROVIDER_GOOGLE", quotaInfo: { - remainingFraction: 0.9556033, - resetTime: "2026-09-24T13:08:46Z" + remainingFraction: 0.8656157, + resetTime: "2026-09-25T09:39:31Z" }, recommended: true, supportedMimeTypes: { @@ -18702,7 +18733,7 @@ var models_default = { modelProvider: "MODEL_PROVIDER_OPENAI", quotaInfo: { remainingFraction: 1, - resetTime: "2026-09-24T13:14:02Z" + resetTime: "2026-09-25T11:24:59Z" }, recommended: true, supportsThinking: true, @@ -19774,9 +19805,14 @@ function normalizeToolSchemaTypes(tools, toolMapper) { fn.name = toolMapper ? toolMapper.toGemini(fn.name) : sanitizeToolName(fn.name); } if (fn) { + if (!fn.parameters) { + fn.parameters = fn.parameters_json_schema ?? fn.parametersJsonSchema; + } if (!fn.parameters) { fn.parameters = { type: "OBJECT", properties: {} }; } + delete fn.parameters_json_schema; + delete fn.parametersJsonSchema; sanitizeSchema(fn.parameters); } } @@ -20201,22 +20237,81 @@ var ChatLoggerImpl = class { } }; -// src/plugin.ts -var AGY_QUOTA_COMMAND = "agyquota"; -var AGY_QUOTA_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota usage for the current authenticated account. - -Immediately call \`${AGY_QUOTA_TOOL_NAME}\` with no arguments and return its output verbatim. -Do not call other tools. -`; -var AGY_QUOTA_SUMMARY_COMMAND = "agyquotasummary"; -var AGY_QUOTA_SUMMARY_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota summary (weekly and 5-hour limits by model group) for the current authenticated account. +// src/plugin/headers.ts +function getSafeHeader(headers, key) { + if (!headers) { + return void 0; + } + const targetKey = key.toLowerCase(); + if (typeof headers.get === "function") { + try { + return headers.get(targetKey) || void 0; + } catch { + } + } + if (Array.isArray(headers)) { + const found = headers.find((item) => { + if (Array.isArray(item) && typeof item[0] === "string") { + return item[0].toLowerCase() === targetKey; + } + return false; + }); + return found ? String(found[1]) : void 0; + } + if (typeof headers === "object") { + const foundKey = Object.keys(headers).find((k) => k.toLowerCase() === targetKey); + return foundKey ? headers[foundKey] !== void 0 ? String(headers[foundKey]) : void 0 : void 0; + } + return void 0; +} +function setSafeHeaders(initHeaders, newHeaders) { + if (typeof globalThis.Headers !== "undefined") { + const headers = new globalThis.Headers(initHeaders ?? {}); + for (const [k, v] of Object.entries(newHeaders)) { + headers.set(k, v); + } + return headers; + } + if (Array.isArray(initHeaders)) { + const nextHeaders2 = [...initHeaders]; + for (const [k, v] of Object.entries(newHeaders)) { + const idx = nextHeaders2.findIndex((item) => Array.isArray(item) && typeof item[0] === "string" && item[0].toLowerCase() === k.toLowerCase()); + if (idx !== -1) { + nextHeaders2[idx] = [k, v]; + } else { + nextHeaders2.push([k, v]); + } + } + return nextHeaders2; + } + const nextHeaders = {}; + if (initHeaders && typeof initHeaders === "object") { + for (const [k, v] of Object.entries(initHeaders)) { + nextHeaders[k] = String(v); + } + } + for (const [k, v] of Object.entries(newHeaders)) { + const existingKey = Object.keys(nextHeaders).find((key) => key.toLowerCase() === k.toLowerCase()); + if (existingKey) { + nextHeaders[existingKey] = v; + } else { + nextHeaders[k] = v; + } + } + return nextHeaders; +} +function toUrlString(value) { + if (typeof value === "string") { + return value; + } + const candidate = value.url; + if (candidate) { + return candidate; + } + return value.toString(); +} -Immediately call \`${AGY_QUOTA_SUMMARY_TOOL_NAME}\` with no arguments and return its output verbatim. -Do not call other tools. -`; -var latestAgyAuthResolver; -var latestAgyConfiguredProjectId; -var latestAgyUserAgentModel; +// src/plugin/tier.ts var STATIC_MODELS_SIMPLE = { "gemini-3.8-flash": { name: "Gemini 3.8 Flash", @@ -20304,6 +20399,39 @@ var TIER_MAPPING = { high: "gemini-3.1-pro-high" } }; +function resolveModelTier(baseModelId, headersOrInit) { + const parts = baseModelId.split("@"); + const base = parts[0] || ""; + const suffixTier = parts[1]?.toLowerCase(); + const mapping = TIER_MAPPING[base]; + if (!mapping) { + return baseModelId; + } + const rawHeaders = headersOrInit && typeof headersOrInit === "object" && "headers" in headersOrInit ? headersOrInit.headers : headersOrInit; + const headerTier = getSafeHeader(rawHeaders, "x-agy-tier")?.toLowerCase() || null; + const requestedTier = headerTier || suffixTier; + if (requestedTier && Object.prototype.hasOwnProperty.call(mapping, requestedTier)) { + return mapping[requestedTier] || baseModelId; + } + return mapping["medium"] ?? mapping["high"] ?? baseModelId; +} + +// src/plugin.ts +var AGY_QUOTA_COMMAND = "agyquota"; +var AGY_QUOTA_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota usage for the current authenticated account. + +Immediately call \`${AGY_QUOTA_TOOL_NAME}\` with no arguments and return its output verbatim. +Do not call other tools. +`; +var AGY_QUOTA_SUMMARY_COMMAND = "agyquotasummary"; +var AGY_QUOTA_SUMMARY_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota summary (weekly and 5-hour limits by model group) for the current authenticated account. + +Immediately call \`${AGY_QUOTA_SUMMARY_TOOL_NAME}\` with no arguments and return its output verbatim. +Do not call other tools. +`; +var latestAgyAuthResolver; +var latestAgyConfiguredProjectId; +var latestAgyUserAgentModel; var buildModelFromSimple = (modelId, simple) => { const isClaude = modelId.startsWith("claude-"); const isGpt = modelId.startsWith("gpt-"); @@ -20397,83 +20525,6 @@ var STATIC_MODELS = {}; for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) { STATIC_MODELS[modelId] = buildModelFromSimple(modelId, simple); } -function getSafeHeader(headers, key) { - if (!headers) { - return void 0; - } - const targetKey = key.toLowerCase(); - if (typeof headers.get === "function") { - try { - return headers.get(targetKey) || void 0; - } catch { - } - } - if (Array.isArray(headers)) { - const found = headers.find((item) => { - if (Array.isArray(item) && typeof item[0] === "string") { - return item[0].toLowerCase() === targetKey; - } - return false; - }); - return found ? String(found[1]) : void 0; - } - if (typeof headers === "object") { - const foundKey = Object.keys(headers).find((k) => k.toLowerCase() === targetKey); - return foundKey ? headers[foundKey] !== void 0 ? String(headers[foundKey]) : void 0 : void 0; - } - return void 0; -} -function setSafeHeaders(initHeaders, newHeaders) { - if (typeof globalThis.Headers !== "undefined") { - const headers = new globalThis.Headers(initHeaders ?? {}); - for (const [k, v] of Object.entries(newHeaders)) { - headers.set(k, v); - } - return headers; - } - if (Array.isArray(initHeaders)) { - const nextHeaders2 = [...initHeaders]; - for (const [k, v] of Object.entries(newHeaders)) { - const idx = nextHeaders2.findIndex((item) => Array.isArray(item) && typeof item[0] === "string" && item[0].toLowerCase() === k.toLowerCase()); - if (idx !== -1) { - nextHeaders2[idx] = [k, v]; - } else { - nextHeaders2.push([k, v]); - } - } - return nextHeaders2; - } - const nextHeaders = {}; - if (initHeaders && typeof initHeaders === "object") { - for (const [k, v] of Object.entries(initHeaders)) { - nextHeaders[k] = String(v); - } - } - for (const [k, v] of Object.entries(newHeaders)) { - const existingKey = Object.keys(nextHeaders).find((key) => key.toLowerCase() === k.toLowerCase()); - if (existingKey) { - nextHeaders[existingKey] = v; - } else { - nextHeaders[k] = v; - } - } - return nextHeaders; -} -function resolveModelTier(baseModelId, init) { - const parts = baseModelId.split("@"); - const base = parts[0] || ""; - const suffixTier = parts[1]?.toLowerCase(); - const mapping = TIER_MAPPING[base]; - if (!mapping) { - return baseModelId; - } - const headerTier = getSafeHeader(init?.headers, "x-agy-tier")?.toLowerCase() || null; - const requestedTier = headerTier || suffixTier; - if (requestedTier && Object.prototype.hasOwnProperty.call(mapping, requestedTier)) { - return mapping[requestedTier] || baseModelId; - } - return mapping["medium"] ?? mapping["high"]; -} var AgyCLIOAuthPlugin = async ({ client }) => { let latestConfig; updateStaticModelsWithPricing(STATIC_MODELS); @@ -20801,25 +20852,782 @@ async function ensureProjectContextOrThrow(authRecord, client, configuredProject throw error45; } } -function toUrlString(value) { - if (typeof value === "string") { - return value; + +// src/plugin/types.ts +function defineOpenCodeV2Plugin(def) { + return def; +} + +// src/plugin/v2-storage.ts +import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs"; +import { randomUUID as randomUUID4 } from "crypto"; +import { homedir as homedir4 } from "os"; +import { join as join6 } from "path"; +var storedAuthOverrideForTesting = void 0; +function loadStoredAuthFromJson() { + if (storedAuthOverrideForTesting !== void 0) { + return storedAuthOverrideForTesting; + } + const authPath = join6(homedir4(), ".local", "share", "opencode", "auth.json"); + try { + if (existsSync6(authPath)) { + const content = readFileSync5(authPath, "utf8"); + const data = JSON.parse(content); + return data?.[AGY_PROVIDER_ID]; + } + } catch { } - const candidate = value.url; - if (candidate) { - return candidate; + return void 0; +} +function saveStoredAuthToJson(authRecord) { + if (process.env.NODE_ENV === "test" || process.env.VITEST || storedAuthOverrideForTesting !== void 0) { + return; } - return value.toString(); + const dirPath = join6(homedir4(), ".local", "share", "opencode"); + if (!existsSync6(dirPath)) { + mkdirSync5(dirPath, { recursive: true }); + } + const authPath = join6(dirPath, "auth.json"); + const data = existsSync6(authPath) ? JSON.parse(readFileSync5(authPath, "utf8")) : {}; + data[AGY_PROVIDER_ID] = { + type: "oauth", + refresh: authRecord.refresh, + access: authRecord.access, + expires: authRecord.expires + }; + const tempPath = `${authPath}.${process.pid}.${randomUUID4()}.tmp`; + let tempFileDescriptor; + let ownsTempFile = false; + try { + tempFileDescriptor = openSync(tempPath, "wx", 384); + ownsTempFile = true; + writeFileSync5(tempFileDescriptor, JSON.stringify(data, null, 2), { + encoding: "utf8" + }); + closeSync(tempFileDescriptor); + tempFileDescriptor = void 0; + renameSync4(tempPath, authPath); + } catch (error45) { + if (tempFileDescriptor !== void 0) { + try { + closeSync(tempFileDescriptor); + } catch { + } + } + if (ownsTempFile) { + try { + unlinkSync4(tempPath); + } catch { + } + } + throw error45; + } +} + +// src/plugin/v2-fetch.ts +function createV2FetchInterceptor(getAuthSnapshot) { + return async function agyV2Fetch(input, init) { + const isGL = isGenerativeLanguageRequest(input); + const isInternal = toUrlString(input).includes("cloudcode-pa.googleapis.com"); + if (!isGL && !isInternal) { + return (globalThis.fetch || fetch)(input, init); + } + let rawAuth = getAuthSnapshot ? await getAuthSnapshot() : void 0; + if (!rawAuth) { + rawAuth = loadStoredAuthFromJson(); + } + if (!rawAuth || !isOAuthAuth(rawAuth)) { + return (globalThis.fetch || fetch)(input, init); + } + let authRecord = resolveCachedAuth(rawAuth); + if (accessTokenExpired(authRecord)) { + const refreshed = await refreshAccessToken(authRecord, { auth: { set: async () => { + } } }); + if (refreshed) { + authRecord = refreshed; + } + } + if (!authRecord.access) { + return (globalThis.fetch || fetch)(input, init); + } + if (isInternal) { + const hasAuth = getSafeHeader(init?.headers, "Authorization") !== void 0; + if (hasAuth) { + return (globalThis.fetch || fetch)(input, init); + } + const userAgent = buildAgyCliUserAgent(); + const headers = setSafeHeaders(init?.headers, { + Authorization: `Bearer ${authRecord.access}`, + "User-Agent": userAgent + }); + return (globalThis.fetch || fetch)(input, { + ...init, + headers + }); + } + const requestTarget = parseGenerativeLanguageRequest(input); + const requestUserAgentModel = requestTarget?.effectiveModel; + let projectContext; + try { + projectContext = await ensureProjectContext( + authRecord, + { auth: { set: async () => { + } } }, + void 0, + requestUserAgentModel + ); + } catch { + projectContext = { + effectiveProjectId: parseRefreshParts(authRecord.refresh).projectId || "antigravity", + auth: authRecord + }; + } + const originalRequestedModel = parseGenerativeLanguageRequest(input)?.effectiveModel; + let modifiedInput = input; + if (isGL && originalRequestedModel) { + const originalBase = originalRequestedModel.replace("google-agy/", ""); + const resolvedBase = resolveModelTier(originalBase, init); + if (originalBase !== resolvedBase) { + if (typeof modifiedInput === "string") { + modifiedInput = modifiedInput.replace(`models/${originalBase}`, `models/${resolvedBase}`); + } else if (typeof Request !== "undefined" && modifiedInput instanceof Request) { + const newUrl = modifiedInput.url.replace(`models/${originalBase}`, `models/${resolvedBase}`); + modifiedInput = new Request(newUrl, modifiedInput); + } + } + } + const transformed = prepareAgyRequest( + modifiedInput, + init, + authRecord.access, + projectContext.effectiveProjectId, + void 0 + ); + const chatLogger = createChatLogger(); + if (chatLogger) { + chatLogger.logRequest( + toUrlString(transformed.request), + transformed.init.method || "GET", + transformed.init.headers, + transformed.init.body + ); + } + const response = await fetchWithRetry(transformed.request, transformed.init); + return transformAgyResponse( + response, + transformed.streaming, + null, + transformed.requestedModel, + transformed.sessionId, + chatLogger + ); + }; } +// src/plugin/v2-session.ts +function createV2HttpRequestHook(state = {}) { + return async (event) => { + if (!event) return; + const rawUrl = typeof event.url === "string" ? event.url : event.request?.url || ""; + const isGL = isGenerativeLanguageRequest(rawUrl); + const isInternal = rawUrl.includes("cloudcode-pa.googleapis.com"); + if (!isGL && !isInternal) { + return; + } + event.headers = event.headers || {}; + let modelName; + let rawAuth = event.auth; + if (rawAuth && !rawAuth.type) { + const tokenVal = rawAuth.access || rawAuth.token; + if (tokenVal || rawAuth.refresh) { + rawAuth = { type: "oauth", access: tokenVal, ...rawAuth }; + } + } + if (!rawAuth || !isOAuthAuth(rawAuth)) { + rawAuth = loadStoredAuthFromJson(); + } + let authRecord = rawAuth && isOAuthAuth(rawAuth) ? resolveCachedAuth(rawAuth) : void 0; + if (authRecord && accessTokenExpired(authRecord)) { + const refreshed = await refreshAccessToken(authRecord, { auth: { set: async () => { + } } }); + if (refreshed) { + authRecord = refreshed; + try { + saveStoredAuthToJson(refreshed); + } catch { + console.warn("[Agy Auth] Refreshed credentials could not be persisted; the current request will continue."); + } + } + } + if (isGL) { + const parsed = parseGenerativeLanguageRequest(rawUrl); + modelName = parsed?.effectiveModel; + if (modelName) { + state.lastInterceptedModel = modelName; + } + if (typeof event.headers.delete === "function") { + event.headers.delete("x-goog-api-key"); + event.headers.delete("api-key"); + } else { + for (const key of Object.keys(event.headers)) { + const lower = key.toLowerCase(); + if (lower === "x-goog-api-key" || lower === "api-key") { + delete event.headers[key]; + } + } + } + if (event.request?.headers) { + if (typeof event.request.headers.delete === "function") { + event.request.headers.delete("x-goog-api-key"); + event.request.headers.delete("api-key"); + } else { + for (const key of Object.keys(event.request.headers)) { + const lower = key.toLowerCase(); + if (lower === "x-goog-api-key" || lower === "api-key") { + delete event.request.headers[key]; + } + } + } + } + let cleanUrl = rawUrl; + try { + const parsedUrl = new URL(rawUrl); + let urlChanged = false; + for (const param of ["key", "x-goog-api-key", "api-key"]) { + if (parsedUrl.searchParams.has(param)) { + parsedUrl.searchParams.delete(param); + urlChanged = true; + } + } + if (urlChanged) { + cleanUrl = parsedUrl.toString(); + } + } catch { + } + if (authRecord && authRecord.access) { + let projectContext; + try { + projectContext = await ensureProjectContext( + authRecord, + { auth: { set: async () => { + } } }, + void 0, + modelName + ); + } catch { + projectContext = { + effectiveProjectId: parseRefreshParts(authRecord.refresh).projectId || "antigravity", + auth: authRecord + }; + } + const originalRequestedModel = modelName; + let modifiedUrl = cleanUrl; + if (originalRequestedModel) { + const originalBase = originalRequestedModel.replace("google-agy/", ""); + const initHeaders = event.headers instanceof Headers ? event.headers : event.request?.headers; + const resolvedBase = resolveModelTier(originalBase, { headers: initHeaders }); + if (originalBase !== resolvedBase) { + modifiedUrl = modifiedUrl.replace(`models/${originalBase}`, `models/${resolvedBase}`); + } + } + let bodyPayload = event.body; + if (bodyPayload === void 0 && event.request) { + if (typeof event.request.clone === "function") { + try { + bodyPayload = await event.request.clone().text(); + } catch { + } + } else if (event.request.body !== void 0) { + bodyPayload = event.request.body; + } + } + let preExistingUserAgent; + if (typeof event.headers?.get === "function") { + preExistingUserAgent = event.headers.get("user-agent") || event.headers.get("User-Agent") || void 0; + } else if (event.headers && typeof event.headers === "object") { + preExistingUserAgent = event.headers["user-agent"] || event.headers["User-Agent"] || void 0; + } + if (!preExistingUserAgent && typeof event.request?.headers?.get === "function") { + preExistingUserAgent = event.request.headers.get("user-agent") || event.request.headers.get("User-Agent") || void 0; + } else if (!preExistingUserAgent && event.request?.headers && typeof event.request.headers === "object") { + preExistingUserAgent = event.request.headers["user-agent"] || event.request.headers["User-Agent"] || void 0; + } + const method = event.method || event.request?.method || "POST"; + const requestHeaders = event.headers || event.request?.headers; + const transformed = prepareAgyRequest( + modifiedUrl, + { + method, + headers: requestHeaders, + body: bodyPayload + }, + authRecord.access, + projectContext.effectiveProjectId + ); + const targetUrl = toUrlString(transformed.request); + if (typeof event.url === "string") { + event.url = targetUrl; + } + if (event.request && typeof event.request === "object") { + if (typeof Request !== "undefined" && event.request instanceof Request) { + event.request = new Request(targetUrl, { + method: transformed.init.method, + headers: transformed.init.headers, + body: transformed.init.body + }); + } else if (typeof event.request.url === "string") { + try { + event.request.url = targetUrl; + } catch { + } + } + } + const headerEntries = transformed.init.headers instanceof Headers ? Array.from(transformed.init.headers.entries()) : Object.entries(transformed.init.headers || {}); + for (const [k, v] of headerEntries) { + const lowerK = k.toLowerCase(); + if (lowerK === "user-agent") { + if (preExistingUserAgent) { + continue; + } + if (typeof event.headers?.set === "function") { + event.headers.set("User-Agent", v); + } else if (event.headers) { + event.headers["User-Agent"] = v; + } + if (event.request?.headers && typeof event.request.headers.set === "function") { + event.request.headers.set("User-Agent", v); + } else if (event.request?.headers && typeof event.request.headers === "object") { + event.request.headers["User-Agent"] = v; + } + continue; + } + if (typeof event.headers?.set === "function") { + event.headers.set(k, v); + } else if (event.headers) { + event.headers[k] = v; + } + if (event.request?.headers && typeof event.request.headers.set === "function") { + event.request.headers.set(k, v); + } else if (event.request?.headers && typeof event.request.headers === "object") { + event.request.headers[k] = v; + } + } + if (transformed.init.body !== void 0) { + event.body = transformed.init.body; + if (event.request && typeof event.request === "object" && !(event.request instanceof Request)) { + event.request.body = transformed.init.body; + } + } + } else { + if (typeof event.url === "string") { + event.url = cleanUrl; + } + if (event.request && typeof event.request === "object") { + if (typeof Request !== "undefined" && event.request instanceof Request) { + event.request = new Request(cleanUrl, event.request); + } else if (typeof event.request.url === "string") { + try { + event.request.url = cleanUrl; + } catch { + } + } + } + } + } + if (authRecord && authRecord.access) { + if (typeof event.headers?.set === "function") { + event.headers.set("Authorization", `Bearer ${authRecord.access}`); + } else if (event.headers) { + event.headers["Authorization"] = `Bearer ${authRecord.access}`; + } + if (event.request?.headers && typeof event.request.headers.set === "function") { + event.request.headers.set("Authorization", `Bearer ${authRecord.access}`); + } else if (event.request?.headers && typeof event.request.headers === "object") { + event.request.headers["Authorization"] = `Bearer ${authRecord.access}`; + } + } + const userAgent = buildAgyCliUserAgent(modelName); + if (typeof event.headers?.set === "function") { + if (!event.headers.get("User-Agent") && !event.headers.get("user-agent")) { + event.headers.set("User-Agent", userAgent); + } + } else if (event.headers) { + if (!event.headers["User-Agent"] && !event.headers["user-agent"]) { + event.headers["User-Agent"] = userAgent; + } + } + if (event.request?.headers && typeof event.request.headers.set === "function") { + if (!event.request.headers.get("User-Agent") && !event.request.headers.get("user-agent")) { + event.request.headers.set("User-Agent", userAgent); + } + } else if (event.request?.headers && typeof event.request.headers === "object") { + if (!event.request.headers["User-Agent"] && !event.request.headers["user-agent"]) { + event.request.headers["User-Agent"] = userAgent; + } + } + }; +} +function createV2HttpResponseHook(state = {}) { + return async (event) => { + if (!event || !event.response) return; + const rawUrl = typeof event.url === "string" ? event.url : event.request?.url || ""; + const isInternal = rawUrl.includes("cloudcode-pa.googleapis.com"); + if (isInternal && event.response instanceof Response) { + const isStreaming = rawUrl.includes(":streamGenerateCode") || event.response.headers.get("content-type")?.includes("text/event-stream"); + const parsedModel = parseGenerativeLanguageRequest(rawUrl)?.effectiveModel || state.lastInterceptedModel; + const transformed = await transformAgyResponse( + event.response, + !!isStreaming, + null, + parsedModel, + void 0, + void 0 + ); + event.response = transformed; + } + }; +} +function createV2RetryHook() { + return async (event) => { + if (!event) return; + const response = event.response; + if (response instanceof Response) { + if (response.status === 429 || response.status === 503) { + const quota = await classifyQuotaResponse(response); + if (quota?.retryDelayMs) { + event.retryDelayMs = quota.retryDelayMs; + } else { + event.retryDelayMs = await resolveRetryDelayMs(response, event.attempt || 1); + } + } + } else if (event.details && Array.isArray(event.details)) { + const retryInfo = event.details.find( + (d) => d && d["@type"] === "type.googleapis.com/google.rpc.RetryInfo" + ); + if (retryInfo?.retryDelay) { + const delayMs = retryInternals.parseRetryDelayValue(retryInfo.retryDelay); + if (delayMs !== null) { + event.retryDelayMs = delayMs; + } + } + } + }; +} +function registerV2SessionHooks(ctx) { + const state = {}; + ctx.session.hook("http.request", createV2HttpRequestHook(state), { providerID: AGY_PROVIDER_ID }); + ctx.session.hook("http.response", createV2HttpResponseHook(state), { providerID: AGY_PROVIDER_ID }); + ctx.session.hook("retry", createV2RetryHook(), { providerID: AGY_PROVIDER_ID }); +} + +// src/plugin/v2.ts +async function setupOpenCodeV2(ctx) { + const resolveVariants = (modelId) => { + const mapping = TIER_MAPPING[modelId]; + if (!mapping) return void 0; + const variants = []; + if (mapping.minimal !== void 0) variants.push({ id: "minimal" }); + if (mapping.low !== void 0) variants.push({ id: "low" }); + if (mapping.medium !== void 0) variants.push({ id: "medium" }); + if (mapping.high !== void 0) variants.push({ id: "high" }); + return variants.length > 0 ? variants : void 0; + }; + const updateProviderRecord = (p) => { + p.name = "Antigravity CLI"; + p.activation = "enabled"; + p.package = "aisdk:@ai-sdk/google"; + p.description = "Google Gemini Antigravity Code Assist OAuth provider"; + p.settings = { ...p.settings || {}, apiKey: "dummy" }; + p.integrationID = AGY_PROVIDER_ID; + }; + const buildModelRecord = (modelId, simple, existingModel) => { + const isClaude = modelId.startsWith("claude-"); + const isGpt = modelId.startsWith("gpt-"); + const variants = resolveVariants(modelId); + return { + id: modelId, + providerID: AGY_PROVIDER_ID, + name: simple.name, + description: simple.description, + family: modelId.includes("gemini") ? "gemini" : isClaude ? "claude" : isGpt ? "gpt" : "unknown", + reasoning: simple.reasoning, + attachment: simple.attachment, + tool_call: simple.toolCall, + limit: { + context: simple.maxTokens, + output: simple.maxOutputTokens + }, + cost: simple.cost || { input: 0, output: 0 }, + ...variants ? { variants } : {}, + ...existingModel || {} + }; + }; + const updateModelRecord = (m, modelId, simple) => { + m.name = simple.name; + m.capabilities = { + tools: true, + input: ["text", "image"], + output: ["text"] + }; + m.limit = { + context: simple.maxTokens, + output: simple.maxOutputTokens + }; + m.family = "gemini"; + if (simple.cost) { + m.cost = simple.cost; + } + const variants = resolveVariants(modelId); + if (variants) { + m.variants = variants; + } + }; + if (ctx.provider?.transform) { + ctx.provider.transform(async (editor) => { + if (!editor) return; + if (typeof editor.update === "function") { + editor.update(AGY_PROVIDER_ID, updateProviderRecord); + } else if (typeof editor.set === "function") { + const p = { id: AGY_PROVIDER_ID }; + updateProviderRecord(p); + editor.set(AGY_PROVIDER_ID, p); + } else if (typeof editor === "object") { + const p = editor[AGY_PROVIDER_ID] || { id: AGY_PROVIDER_ID }; + updateProviderRecord(p); + editor[AGY_PROVIDER_ID] = p; + } + }); + } + if (ctx.model?.transform) { + ctx.model.transform(async (editor) => { + if (!editor) return; + if (typeof editor.update === "function") { + for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) { + editor.update(AGY_PROVIDER_ID, modelId, (m) => updateModelRecord(m, modelId, simple)); + } + } else if (typeof editor.set === "function") { + for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) { + const m = buildModelRecord(modelId, simple); + editor.set(`${AGY_PROVIDER_ID}:${modelId}`, m); + } + } else if (typeof editor === "object") { + for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) { + const key = `${AGY_PROVIDER_ID}:${modelId}`; + editor[key] = buildModelRecord(modelId, simple, editor[key]); + } + } + }); + } + if (ctx.catalog?.transform) { + ctx.catalog.transform(async (editor) => { + if (!editor) return; + if (typeof editor?.provider?.update === "function") { + editor.provider.update(AGY_PROVIDER_ID, updateProviderRecord); + if (typeof editor?.model?.update === "function") { + for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) { + editor.model.update(AGY_PROVIDER_ID, modelId, (m) => updateModelRecord(m, modelId, simple)); + } + } + return; + } + const catalog = editor; + catalog.providers = catalog.providers || {}; + catalog.providers[AGY_PROVIDER_ID] = { + id: AGY_PROVIDER_ID, + name: "Antigravity CLI", + npm: "aisdk:@ai-sdk/google", + settings: { apiKey: "dummy" }, + integrationID: AGY_PROVIDER_ID, + models: { ...catalog.providers[AGY_PROVIDER_ID]?.models || {} }, + ...catalog.providers[AGY_PROVIDER_ID] + }; + if (catalog.providers[AGY_PROVIDER_ID].settings) { + catalog.providers[AGY_PROVIDER_ID].settings = { + ...catalog.providers[AGY_PROVIDER_ID].settings, + apiKey: "dummy" + }; + } else { + catalog.providers[AGY_PROVIDER_ID].settings = { apiKey: "dummy" }; + } + catalog.providers[AGY_PROVIDER_ID].integrationID = AGY_PROVIDER_ID; + const targetModels = catalog.providers[AGY_PROVIDER_ID].models; + for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) { + targetModels[modelId] = buildModelRecord(modelId, simple, targetModels[modelId]); + } + }); + } + ctx.tool.transform(async (editor) => { + if (!editor) return; + const quotaTool = createAgyQuotaTool({ + client: void 0, + getAuthResolver: () => void 0, + getConfiguredProjectId: () => void 0, + getUserAgentModel: () => void 0 + }); + const quotaSummaryTool = createAgyQuotaSummaryTool({ + client: void 0, + getAuthResolver: () => void 0, + getConfiguredProjectId: () => void 0, + getUserAgentModel: () => void 0 + }); + if (typeof editor.add === "function") { + editor.add({ + name: AGY_QUOTA_TOOL_NAME, + description: quotaTool.description, + input: {}, + execute: async (args, context) => quotaTool.execute(args, context) + }); + editor.add({ + name: AGY_QUOTA_SUMMARY_TOOL_NAME, + description: quotaSummaryTool.description, + input: {}, + execute: async (args, context) => quotaSummaryTool.execute(args, context) + }); + } else if (typeof editor.set === "function") { + editor.set(AGY_QUOTA_TOOL_NAME, quotaTool); + editor.set(AGY_QUOTA_SUMMARY_TOOL_NAME, quotaSummaryTool); + } else if (editor.tools && typeof editor.tools === "object") { + editor.tools[AGY_QUOTA_TOOL_NAME] = quotaTool; + editor.tools[AGY_QUOTA_SUMMARY_TOOL_NAME] = quotaSummaryTool; + } else if (typeof editor === "object") { + editor[AGY_QUOTA_TOOL_NAME] = quotaTool; + editor[AGY_QUOTA_SUMMARY_TOOL_NAME] = quotaSummaryTool; + } + }); + ctx.command.transform(async (editor) => { + if (!editor) return; + const commands = { + [AGY_V2_QUOTA_COMMAND]: { + description: "Show Agy Code Assist quota usage", + template: AGY_V2_QUOTA_COMMAND_TEMPLATE + }, + [AGY_V2_QUOTA_SUMMARY_COMMAND]: { + description: "Show Agy Code Assist quota summary with weekly and 5-hour limits", + template: AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE + } + }; + if (typeof editor.add === "function") { + editor.add({ + name: AGY_V2_QUOTA_COMMAND, + description: "Display current Antigravity quota usage", + template: AGY_V2_QUOTA_COMMAND_TEMPLATE + }); + editor.add({ + name: AGY_V2_QUOTA_SUMMARY_COMMAND, + description: "Display Antigravity quota summary grouped by model family", + template: AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE + }); + } else if (typeof editor.set === "function") { + editor.set(AGY_V2_QUOTA_COMMAND, commands[AGY_V2_QUOTA_COMMAND]); + editor.set(AGY_V2_QUOTA_SUMMARY_COMMAND, commands[AGY_V2_QUOTA_SUMMARY_COMMAND]); + } else if (editor.commands && typeof editor.commands === "object") { + editor.commands[AGY_V2_QUOTA_COMMAND] = commands[AGY_V2_QUOTA_COMMAND]; + editor.commands[AGY_V2_QUOTA_SUMMARY_COMMAND] = commands[AGY_V2_QUOTA_SUMMARY_COMMAND]; + } else if (typeof editor === "object") { + editor[AGY_V2_QUOTA_COMMAND] = commands[AGY_V2_QUOTA_COMMAND]; + editor[AGY_V2_QUOTA_SUMMARY_COMMAND] = commands[AGY_V2_QUOTA_SUMMARY_COMMAND]; + } + }); + ctx.integration?.transform?.((editor) => { + if (!editor) return; + editor.update?.(AGY_PROVIDER_ID, (integration) => { + integration.name = "Antigravity CLI (plugin)"; + }); + editor.method?.update?.({ + integrationID: AGY_PROVIDER_ID, + method: { + id: "oauth", + type: "oauth", + label: "Antigravity CLI (OAuth)" + }, + authorize: async () => { + const authorization = await authorizeAgy(); + return { + mode: "code", + url: authorization.url, + instructions: "Please complete Google account authorization in your browser. After authorization, copy the full redirect URL or code and paste it below:", + callback: async (code) => { + const trimmed = code.trim(); + const parsed = parseOAuthCallbackInput(trimmed); + const authCode = parsed.code || trimmed; + if (!authCode) { + return { type: "failed", error: "Missing authorization code in callback input" }; + } + if (parsed.state && parsed.state !== authorization.state) { + return { type: "failed", error: "State mismatch in callback input (possible CSRF attempt)" }; + } + const exchangeResult = await exchangeAgyWithVerifier(authCode, authorization.verifier); + if (exchangeResult.type !== "success") { + return exchangeResult; + } + const initialRefresh = formatRefreshParts({ + refreshToken: exchangeResult.refresh + }); + let authRecord = { + type: "oauth", + refresh: initialRefresh, + access: exchangeResult.access, + expires: exchangeResult.expires + }; + try { + const projectContext = await ensureProjectContext( + authRecord, + { auth: { set: async () => { + } } } + ); + authRecord = { + ...authRecord, + refresh: projectContext.auth.refresh + }; + } catch { + } + try { + saveStoredAuthToJson(authRecord); + } catch { + return { + type: "failed", + error: "Google authentication succeeded but AGY credentials could not be stored. Check filesystem access and retry." + }; + } + return { + type: "oauth", + methodID: "oauth", + refresh: authRecord.refresh, + access: authRecord.access, + expires: authRecord.expires + }; + } + }; + } + }); + }); + ctx.aisdk?.hook("sdk", async (event) => { + if (!event) return; + const isMatchingProvider = event.providerID === AGY_PROVIDER_ID || event.model?.providerID === AGY_PROVIDER_ID; + if (!isMatchingProvider) return; + event.options = event.options || {}; + event.options.apiKey = "dummy"; + event.options.fetch = createV2FetchInterceptor(); + }); + registerV2SessionHooks(ctx); +} +var v2PluginDefinition = defineOpenCodeV2Plugin({ + id: AGY_PROVIDER_ID, + setup: setupOpenCodeV2 +}); + // index.ts -var index_default = { - id: "@anthonyhaussman/opencode-agy-auth", +var dualPlugin = { + ...v2PluginDefinition, server: AgyCLIOAuthPlugin }; +var index_default = dualPlugin; export { AgyCLIOAuthPlugin, GoogleOAuthPlugin, - index_default as default + index_default as default, + defineOpenCodeV2Plugin, + setupOpenCodeV2, + v2PluginDefinition }; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/references/upstream-plugins/opencode-agy-auth/dist/index.js.map b/references/upstream-plugins/opencode-agy-auth/dist/index.js.map index 8767c44b..d9d29b9f 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/index.js.map +++ b/references/upstream-plugins/opencode-agy-auth/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/plugin/pricing.ts","../src/constants.ts","../src/fetch.ts","../src/plugin/oauth-authorize.ts","../node_modules/jose/dist/node/esm/runtime/base64url.js","../node_modules/jose/dist/node/esm/lib/buffer_utils.js","../node_modules/jose/dist/node/esm/util/base64url.js","../node_modules/@openauthjs/openauth/dist/esm/pkce.js","../src/sdk/oauth.ts","../src/sdk/activity-request-id.ts","../src/sdk/user-agent.ts","../src/sdk/agy-cli-version.ts","../src/sdk/fetch_quota.ts","../src/plugin/auth.ts","../src/sdk/retry/quota.ts","../src/sdk/retry/helpers.ts","../src/sdk/retry/cooldown-store.ts","../src/sdk/retry/index.ts","../src/sdk/terminal-hyperlink.ts","../src/plugin/project/types.ts","../src/plugin/project/utils.ts","../src/sdk/fetch_project.ts","../src/plugin/project/context.ts","../src/plugin/provider.ts","../src/plugin/cache.ts","../src/sdk/cache/signature-cache.ts","../src/sdk/request/turn-state-tracker.ts","../src/sdk/request/thinking.ts","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/index.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/core.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/util.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/errors.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/parse.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/regexes.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/checks.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/doc.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/versions.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/schemas.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/index.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ar.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/az.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/be.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ca.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/cs.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/da.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/de.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/en.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/eo.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/es.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/fa.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/fi.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/fr.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/fr-CA.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/he.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/hu.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/id.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/is.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/it.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ja.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ka.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/km.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/kh.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ko.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/lt.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/mk.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ms.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/nl.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/no.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ota.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ps.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/pl.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/pt.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ru.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/sl.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/sv.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ta.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/th.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/tr.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/uk.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ua.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ur.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/vi.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/zh-CN.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/zh-TW.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/yo.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/registries.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/api.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/to-json-schema.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/json-schema.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/iso.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/errors.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/parse.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/schemas.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/compat.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/coerce.js","../node_modules/@opencode-ai/plugin/dist/tool.js","../src/plugin/token.ts","../src/plugin/quota-utils.ts","../src/plugin/quota.ts","../src/plugin/quota-summary.ts","../src/plugin/notify.ts","../src/plugin/traffic.ts","../src/sdk/request/prepare.ts","../models.json","../src/sdk/request-helpers/types.ts","../src/sdk/request-helpers/thinking.ts","../src/sdk/request-helpers/parsing.ts","../src/sdk/request-helpers/errors.ts","../src/sdk/request/identifiers.ts","../src/sdk/request/shared.ts","../src/sdk/request/openai.ts","../src/sdk/request/tool-mapper.ts","../src/sdk/request/response.ts","../src/sdk/chat-logger.ts","../src/plugin.ts","../index.ts"],"sourcesContent":["import { tmpdir, userInfo } from 'node:os';\nimport { join } from 'node:path';\nimport { existsSync, readFileSync, writeFileSync, statSync } from 'node:fs';\nimport type { ProviderModel } from './types';\n\n// Using fetch API (available in Node.js 18+)\nconst PRICING_API_URL = 'https://models.dev/api.json';\nconst CACHE_FILE_NAME = 'agy-pricing-cache.json';\nconst CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nlet memoryCache: ModelsDevResponse | null = null;\n\ninterface ModelsDevCost {\n input?: number;\n output?: number;\n cache_read?: number;\n cache_write?: number;\n [key: string]: any;\n}\n\ninterface ModelsDevModel {\n cost?: ModelsDevCost;\n [key: string]: any;\n}\n\ninterface ModelsDevProvider {\n models: Record;\n [key: string]: any;\n}\n\ntype ModelsDevResponse = Record;\n\nfunction getCachePath(): string {\n try {\n return join(tmpdir(), `${CACHE_FILE_NAME}-${userInfo().uid}`);\n } catch (err) {\n return join(tmpdir(), CACHE_FILE_NAME);\n }\n}\n\nfunction loadCachedPricing(): ModelsDevResponse | null {\n try {\n const cachePath = getCachePath();\n if (!existsSync(cachePath)) return null;\n\n const stats = statSync(cachePath);\n if (Date.now() - stats.mtimeMs > CACHE_TTL_MS) {\n return null; // Cache expired\n }\n\n const data = readFileSync(cachePath, 'utf-8');\n return JSON.parse(data) as ModelsDevResponse;\n } catch (err) {\n return null;\n }\n}\n\nfunction savePricingCache(data: ModelsDevResponse): void {\n try {\n const cachePath = getCachePath();\n writeFileSync(cachePath, JSON.stringify(data), 'utf-8');\n } catch (err) {\n // Ignore cache write errors\n }\n}\n\nasync function fetchPricing(): Promise {\n if (memoryCache) {\n return memoryCache;\n }\n\n const cached = loadCachedPricing();\n if (cached) {\n memoryCache = cached;\n return cached;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 2000);\n\n try {\n const response = await fetch(PRICING_API_URL, { signal: controller.signal });\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return null;\n }\n const data = (await response.json()) as ModelsDevResponse;\n if (data) {\n savePricingCache(data);\n memoryCache = data;\n return data;\n }\n } catch (err) {\n clearTimeout(timeoutId);\n // Silent fail, will fallback to 0 or existing costs\n }\n\n return null;\n}\n\nfunction determineProvider(modelId: string): string | null {\n const lower = modelId.toLowerCase();\n if (lower.startsWith('gemini-') || lower.startsWith('gemma-')) return 'google';\n if (lower.startsWith('claude-')) return 'anthropic';\n if (lower.startsWith('gpt-') || lower.startsWith('o1') || lower.startsWith('o3')) return 'openai';\n return null;\n}\n\n/**\n * Mutates the STATIC_MODELS object by injecting dynamic costs from models.dev\n */\nexport function updateStaticModelsWithPricing(\n staticModels: Record\n): void {\n fetchPricing().then((pricingData) => {\n if (!pricingData) return;\n\n for (const [modelId, modelObj] of Object.entries(staticModels)) {\n const provider = determineProvider(modelId);\n if (!provider || !pricingData[provider]?.models) continue;\n\n // Remove the \"-thinking\" suffix which is custom for opencode if the base model exists\n let lookupId = modelId;\n if (lookupId.endsWith('-thinking') && !pricingData[provider].models[lookupId]) {\n lookupId = lookupId.replace('-thinking', '');\n }\n\n const apiModel = pricingData[provider].models[lookupId];\n if (apiModel && apiModel.cost) {\n const apiCost = apiModel.cost;\n\n modelObj.cost = {\n input: apiCost.input ?? modelObj.cost?.input ?? 0,\n output: apiCost.output ?? modelObj.cost?.output ?? 0,\n cache: {\n read: apiCost.cache_read ?? modelObj.cost?.cache?.read ?? 0,\n write: apiCost.cache_write ?? modelObj.cost?.cache?.write ?? 0\n }\n };\n }\n }\n }).catch(() => {\n // Ignore errors to prevent unhandled rejections\n });\n}\n","// Provider identifier shared between the plugin loader and credential storage.\nexport const AGY_PROVIDER_ID = 'google-agy';\n\n// Antigravity CLI (Daily) constant definitions.\nexport const AGY_CLIENT_ID = 'REDACTED_GOOGLE_OAUTH_CLIENT_ID.apps.googleusercontent.com';\nexport const AGY_CLIENT_SECRET = 'REDACTED_GOOGLE_OAUTH_CLIENT_SECRET';\nexport const AGY_SCOPES: readonly string[] = [\n 'openid',\n 'https://www.googleapis.com/auth/cloud-platform',\n 'https://www.googleapis.com/auth/userinfo.email',\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'https://www.googleapis.com/auth/cclog',\n 'https://www.googleapis.com/auth/experimentsandconfigs'\n];\nexport const AGY_REDIRECT_URI = 'https://antigravity.google/oauth-callback';\nexport const AGY_CODE_ASSIST_ENDPOINT = process.env.OPENCODE_AGY_ENDPOINT || 'https://daily-cloudcode-pa.googleapis.com';\nexport const AGY_GENERATIVE_LANGUAGE_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta';\n","type BunRequestInit = RequestInit & { proxy?: string };\n\nexport function agyFetch(input: RequestInfo | URL, init?: RequestInit): Promise {\n const proxy = process.env.OPENCODE_AGY_AUTH_PROXY;\n if (!proxy) {\n return fetch(input, init);\n }\n\n return fetch(input, {\n ...(init ?? {}),\n proxy\n } as BunRequestInit);\n}\n","import { spawn } from 'node:child_process';\n\nimport { authorizeAgy, exchangeAgyWithVerifier } from '../sdk/oauth';\nimport type { AgyTokenExchangeResult } from '../sdk/oauth';\nimport { resolveProjectContextFromAccessToken } from './project';\nimport { resolveConfiguredProjectId } from './provider';\nimport { formatRefreshParts } from './auth';\nimport type { OAuthAuthDetails, PluginClient } from './types';\n\n\n/**\n * Builds the OAuth authorization callback for the plugin authentication method.\n */\nexport function createOAuthAuthorizeMethod(options?: {\n client?: PluginClient;\n getConfiguredProjectId?: () => Promise | string | undefined;\n getUserAgentModel?: () => Promise | string | undefined;\n}): () => Promise<{\n url: string;\n instructions: string;\n method: 'code';\n callback: (callbackUrl: string) => Promise;\n}> {\n return async () => {\n const maybeHydrateProjectId = async (\n result: AgyTokenExchangeResult\n ): Promise => {\n if (result.type !== 'success' || !result.access) {\n return result;\n }\n\n const configuredProjectId = resolveConfiguredProjectId({\n configProjectId: await options?.getConfiguredProjectId?.()\n });\n\n try {\n const initialRefresh = formatRefreshParts({\n refreshToken: result.refresh\n });\n\n const authSnapshot = {\n type: 'oauth',\n refresh: initialRefresh,\n access: result.access,\n expires: result.expires\n } satisfies OAuthAuthDetails;\n\n const projectContext = await resolveProjectContextFromAccessToken(\n authSnapshot,\n result.access,\n configuredProjectId,\n undefined,\n await options?.getUserAgentModel?.()\n );\n\n return projectContext.auth.refresh !== initialRefresh\n ? { ...result, refresh: projectContext.auth.refresh }\n : { ...result, refresh: initialRefresh };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.warn(`[OAuth] Project resolution skipped: ${message}`);\n if (options?.client?.tui?.showToast) {\n const message = error instanceof Error ? error.message : String(error);\n options.client.tui.showToast({\n body: {\n title: \"Failed to bind project context\",\n message: `Authorized successfully but failed to bind project, real models will be unavailable: ${message}`,\n variant: \"warning\",\n duration: 15000\n }\n }).catch(() => {});\n }\n const initialRefresh = formatRefreshParts({\n refreshToken: result.refresh\n });\n return { ...result, refresh: initialRefresh };\n }\n };\n\n const isHeadless = !!(\n process.env.SSH_CONNECTION ||\n process.env.SSH_CLIENT ||\n process.env.SSH_TTY ||\n process.env.OPENCODE_HEADLESS\n );\n\n const authorization = await authorizeAgy();\n if (!isHeadless) {\n openBrowserUrl(authorization.url);\n }\n\n return {\n url: authorization.url,\n instructions: isHeadless\n ? 'Headless/SSH environment detected. Browser auto-open skipped. Please manually open the following URL in a browser on your local machine to authorize:\\n\\n' + authorization.url + '\\n\\nAfter authorization, the page will redirect to https://antigravity.google/oauth-callback?code=... . Copy the full redirect URL from your browser address bar, or just the code parameter value, and paste it into the input box below.\\n\\nNote: If you are not in a headless environment, unset OPENCODE_HEADLESS or run without SSH to enable browser auto-open.'\n : 'Please complete Google account authorization in your browser. After authorization, the page will redirect to https://antigravity.google/oauth-callback?code=... . Please copy the full redirect URL from your browser address bar, or just the code parameter value, and paste it into the input box below:',\n method: 'code',\n callback: async (callbackUrl: string): Promise => {\n try {\n const { code, state } = parseOAuthCallbackInput(callbackUrl);\n if (!code) {\n return { type: 'failed', error: 'Missing authorization code in callback input' };\n }\n if (state && state !== authorization.state) {\n return { type: 'failed', error: 'State mismatch in callback input (possible CSRF attempt)' };\n }\n const exchangeResult = await exchangeAgyWithVerifier(code, authorization.verifier);\n return await maybeHydrateProjectId(exchangeResult);\n } catch (error) {\n return {\n type: 'failed',\n error: error instanceof Error ? error.message : 'Unknown error'\n };\n }\n }\n };\n };\n}\n\nfunction parseOAuthCallbackInput(input: string): { code?: string; state?: string } {\n const trimmed = input.trim();\n if (!trimmed) {\n return {};\n }\n\n if (/^https?:\\/\\//i.test(trimmed)) {\n try {\n const url = new URL(trimmed);\n return {\n code: url.searchParams.get('code') || undefined,\n state: url.searchParams.get('state') || undefined\n };\n } catch {\n return {};\n }\n }\n\n const candidate = trimmed.startsWith('?') ? trimmed.slice(1) : trimmed;\n if (candidate.includes('=')) {\n const params = new URLSearchParams(candidate);\n const code = params.get('code') || undefined;\n const state = params.get('state') || undefined;\n if (code || state) {\n return { code, state };\n }\n }\n\n return { code: trimmed };\n}\n\nfunction openBrowserUrl(url: string): void {\n try {\n const platform = process.platform;\n const command =\n platform === 'darwin' ? 'open' : platform === 'win32' ? 'rundll32' : 'xdg-open';\n const args = platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];\n const child = spawn(command, args, {\n stdio: 'ignore',\n detached: true\n });\n child.unref?.();\n } catch {}\n}\n","import { Buffer } from 'node:buffer';\nimport { decoder } from '../lib/buffer_utils.js';\nfunction normalize(input) {\n let encoded = input;\n if (encoded instanceof Uint8Array) {\n encoded = decoder.decode(encoded);\n }\n return encoded;\n}\nconst encode = (input) => Buffer.from(input).toString('base64url');\nexport const decodeBase64 = (input) => new Uint8Array(Buffer.from(input, 'base64'));\nexport const encodeBase64 = (input) => Buffer.from(input).toString('base64');\nexport { encode };\nexport const decode = (input) => new Uint8Array(Buffer.from(normalize(input), 'base64url'));\n","import digest from '../runtime/digest.js';\nexport const encoder = new TextEncoder();\nexport const decoder = new TextDecoder();\nconst MAX_INT32 = 2 ** 32;\nexport function concat(...buffers) {\n const size = buffers.reduce((acc, { length }) => acc + length, 0);\n const buf = new Uint8Array(size);\n let i = 0;\n for (const buffer of buffers) {\n buf.set(buffer, i);\n i += buffer.length;\n }\n return buf;\n}\nexport function p2s(alg, p2sInput) {\n return concat(encoder.encode(alg), new Uint8Array([0]), p2sInput);\n}\nfunction writeUInt32BE(buf, value, offset) {\n if (value < 0 || value >= MAX_INT32) {\n throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);\n }\n buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);\n}\nexport function uint64be(value) {\n const high = Math.floor(value / MAX_INT32);\n const low = value % MAX_INT32;\n const buf = new Uint8Array(8);\n writeUInt32BE(buf, high, 0);\n writeUInt32BE(buf, low, 4);\n return buf;\n}\nexport function uint32be(value) {\n const buf = new Uint8Array(4);\n writeUInt32BE(buf, value);\n return buf;\n}\nexport function lengthAndInput(input) {\n return concat(uint32be(input.length), input);\n}\nexport async function concatKdf(secret, bits, value) {\n const iterations = Math.ceil((bits >> 3) / 32);\n const res = new Uint8Array(iterations * 32);\n for (let iter = 0; iter < iterations; iter++) {\n const buf = new Uint8Array(4 + secret.length + value.length);\n buf.set(uint32be(iter + 1));\n buf.set(secret, 4);\n buf.set(value, 4 + secret.length);\n res.set(await digest('sha256', buf), iter * 32);\n }\n return res.slice(0, bits >> 3);\n}\n","import * as base64url from '../runtime/base64url.js';\nexport const encode = base64url.encode;\nexport const decode = base64url.decode;\n","// src/pkce.ts\nimport { base64url } from \"jose\";\nfunction generateVerifier(length) {\n const buffer = new Uint8Array(length);\n crypto.getRandomValues(buffer);\n return base64url.encode(buffer);\n}\nasync function generateChallenge(verifier, method) {\n if (method === \"plain\")\n return verifier;\n const encoder = new TextEncoder;\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\n return base64url.encode(new Uint8Array(hash));\n}\nasync function generatePKCE(length = 64) {\n if (length < 43 || length > 128) {\n throw new Error(\"Code verifier length must be between 43 and 128 characters\");\n }\n const verifier = generateVerifier(length);\n const challenge = await generateChallenge(verifier, \"S256\");\n return {\n verifier,\n challenge,\n method: \"S256\"\n };\n}\nasync function validatePKCE(verifier, challenge, method = \"S256\") {\n const generatedChallenge = await generateChallenge(verifier, method);\n return generatedChallenge === challenge;\n}\nexport {\n validatePKCE,\n generatePKCE\n};\n","import { generatePKCE } from '@openauthjs/openauth/pkce';\nimport { randomBytes } from 'node:crypto';\n\nimport { AGY_CLIENT_ID, AGY_CLIENT_SECRET, AGY_REDIRECT_URI, AGY_SCOPES } from '../constants';\nimport { agyFetch } from '../fetch';\n\ninterface PkcePair {\n challenge: string;\n verifier: string;\n}\n\nexport interface AgyAuthorization {\n url: string;\n verifier: string;\n state: string;\n}\n\ninterface AgyTokenExchangeSuccess {\n type: 'success';\n refresh: string;\n access: string;\n expires: number;\n email?: string;\n}\n\ninterface AgyTokenExchangeFailure {\n type: 'failed';\n error: string;\n}\n\nexport type AgyTokenExchangeResult = AgyTokenExchangeSuccess | AgyTokenExchangeFailure;\n\ninterface AgyTokenResponse {\n access_token: string;\n expires_in: number;\n refresh_token: string;\n}\n\ninterface AgyUserInfo {\n email?: string;\n}\n\n/**\n * Builds the Agy OAuth authorization URL with PKCE.\n */\nexport async function authorizeAgy(): Promise {\n const pkce = (await generatePKCE()) as PkcePair;\n const state = randomBytes(32).toString('hex');\n\n const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');\n url.searchParams.set('client_id', AGY_CLIENT_ID);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('redirect_uri', AGY_REDIRECT_URI);\n url.searchParams.set('scope', AGY_SCOPES.join(' '));\n url.searchParams.set('code_challenge', pkce.challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n url.searchParams.set('access_type', 'offline');\n url.searchParams.set('prompt', 'consent');\n url.hash = 'opencode';\n\n return {\n url: url.toString(),\n verifier: pkce.verifier,\n state\n };\n}\n\n/**\n * Exchanges the authorization code for Agy using a known PKCE verifier.\n */\nexport async function exchangeAgyWithVerifier(code: string, verifier: string): Promise {\n try {\n return await exchangeAgyWithVerifierInternal(code, verifier);\n } catch (error) {\n return {\n type: 'failed',\n error: error instanceof Error ? error.message : 'Unknown error'\n };\n }\n}\n\nasync function exchangeAgyWithVerifierInternal(code: string, verifier: string): Promise {\n const tokenResponse = await agyFetch('https://oauth2.googleapis.com/token', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams({\n client_id: AGY_CLIENT_ID,\n client_secret: AGY_CLIENT_SECRET,\n code,\n grant_type: 'authorization_code',\n redirect_uri: AGY_REDIRECT_URI,\n code_verifier: verifier\n })\n });\n\n if (!tokenResponse.ok) {\n const errorText = await tokenResponse.text();\n return { type: 'failed', error: errorText };\n }\n\n const tokenPayload = (await tokenResponse.json()) as AgyTokenResponse;\n\n const userInfoResponse = await agyFetch('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', {\n headers: {\n Authorization: `Bearer ${tokenPayload.access_token}`\n }\n });\n\n const userInfo = userInfoResponse.ok ? ((await userInfoResponse.json()) as AgyUserInfo) : {};\n\n const refreshToken = tokenPayload.refresh_token;\n if (!refreshToken) {\n return { type: 'failed', error: 'Missing refresh token in response' };\n }\n\n return {\n type: 'success',\n refresh: refreshToken,\n access: tokenPayload.access_token,\n expires: Date.now() + tokenPayload.expires_in * 1000,\n email: userInfo.email\n };\n}\n","/**\n * Agy/Gemini CLI attaches a short activity ID to each request via its network logger.\n * We mirror the same format so backend/debug traces look like CLI traffic.\n */\nexport function createAgyActivityRequestId(): string {\n return Math.random().toString(36).substring(7);\n}\n","import os from 'node:os';\nimport { AGY_CLI_VERSION } from './agy-cli-version';\n\nconst AGY_CLI_DEFAULT_SURFACE = 'terminal';\n\nlet cachedUserAgent: string | null = null;\n\nexport function getAgyCliVersion(): string {\n const explicitVersion = process.env.OPENCODE_AGY_CLI_VERSION?.trim();\n if (explicitVersion) {\n return explicitVersion;\n }\n return AGY_CLI_VERSION;\n}\n\nexport function buildAgyCliUserAgent(model?: string): string {\n const version = getAgyCliVersion();\n if (cachedUserAgent && cachedUserAgent.startsWith('antigravity/cli/' + version + ' ')) {\n return cachedUserAgent;\n }\n const rawPlatform = os.platform();\n const platform = rawPlatform === 'win32' ? 'windows' : rawPlatform;\n const rawArch = os.arch();\n const arch = rawArch === 'x64' ? 'amd64' : rawArch;\n cachedUserAgent = 'antigravity/cli/' + version + ' ' + platform + '/' + arch;\n return cachedUserAgent;\n}\n\nfunction getAgyCliSurface(): string {\n return (\n process.env.AGY_CLI_SURFACE?.trim() ||\n process.env.SURFACE?.trim() ||\n AGY_CLI_DEFAULT_SURFACE\n );\n}\n\nexport const userAgentInternals = {\n resetCache() {\n cachedUserAgent = null;\n }\n};\n","export const AGY_CLI_VERSION = '1.2.10';\n","import { AGY_CODE_ASSIST_ENDPOINT } from '../constants';\nimport { agyFetch } from '../fetch';\nimport { createAgyActivityRequestId } from './activity-request-id';\nimport { buildAgyCliUserAgent } from './user-agent';\nimport type { RetrieveUserQuotaResponse, RetrieveUserQuotaSummaryResponse } from '../plugin/project/types';\n\n/**\n * Fetches the Code Assist quota bucket information, which contains the model IDs visible to the current account/project.\n */\nexport async function retrieveUserQuota(\n accessToken: string,\n projectId: string,\n userAgentModel?: string\n): Promise {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:retrieveUserQuota`;\n const headers = buildCodeAssistHeaders(accessToken, userAgentModel);\n\n try {\n const response = await agyFetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({ project: projectId })\n });\n\n if (!response.ok) {\n return null;\n }\n return (await response.json()) as RetrieveUserQuotaResponse;\n } catch {\n return null;\n }\n}\n\n/**\n * Fetches the Code Assist quota summary, grouped by model family with window-based buckets.\n */\nexport async function retrieveUserQuotaSummary(\n accessToken: string,\n projectId: string,\n userAgentModel?: string\n): Promise {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:retrieveUserQuotaSummary`;\n const headers = buildCodeAssistHeaders(accessToken, userAgentModel);\n\n try {\n const response = await agyFetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({ project: projectId })\n });\n\n if (!response.ok) {\n return null;\n }\n return (await response.json()) as RetrieveUserQuotaSummaryResponse;\n } catch {\n return null;\n }\n}\n\nfunction buildCodeAssistHeaders(\n accessToken: string,\n userAgentModel?: string\n): Record {\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${accessToken}`,\n 'User-Agent': userAgent,\n 'x-activity-request-id': createAgyActivityRequestId()\n };\n}\n","import type { AuthDetails, OAuthAuthDetails, RefreshParts } from './types';\n\nconst ACCESS_TOKEN_EXPIRY_BUFFER_MS = 60 * 1000;\n\nexport function isOAuthAuth(auth: AuthDetails): auth is OAuthAuthDetails {\n return auth.type === 'oauth';\n}\n\n/**\n * Splits the packed refresh string into the corresponding refresh token and project ID.\n */\nexport function parseRefreshParts(refresh: string): RefreshParts {\n const [refreshToken = '', projectId = '', managedProjectId = ''] = (refresh ?? '').split('|');\n return {\n refreshToken,\n projectId: projectId || undefined,\n managedProjectId: managedProjectId || undefined\n };\n}\n\n/**\n * Serializes the parts of a refresh token into the stored string format.\n */\nexport function formatRefreshParts(parts: RefreshParts): string {\n if (!parts.refreshToken) {\n return '';\n }\n\n if (!parts.projectId && !parts.managedProjectId) {\n return parts.refreshToken;\n }\n\n const projectSegment = parts.projectId ?? '';\n const managedSegment = parts.managedProjectId ?? '';\n return `${parts.refreshToken}|${projectSegment}|${managedSegment}`;\n}\n\n/**\n * Determines whether the access token has expired or is missing, with a buffer for clock skew.\n */\nexport function accessTokenExpired(auth: OAuthAuthDetails): boolean {\n if (!auth.access || typeof auth.expires !== 'number') {\n return true;\n }\n return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;\n}\n","import { retrieveUserQuotaSummary } from \"../fetch_quota\";\nimport type { QuotaSummaryBucket, RetrieveUserQuotaSummaryResponse } from \"../../plugin/project/types\";\n\ninterface GoogleRpcErrorInfo {\n \"@type\"?: string;\n reason?: string;\n domain?: string;\n metadata?: Record;\n}\n\ninterface GoogleRpcQuotaViolation {\n quotaId?: string;\n description?: string;\n}\n\ninterface GoogleRpcQuotaFailure {\n \"@type\"?: string;\n violations?: GoogleRpcQuotaViolation[];\n}\n\ninterface GoogleRpcRetryInfo {\n \"@type\"?: string;\n retryDelay?: string | { seconds?: number; nanos?: number };\n}\n\nexport interface QuotaContext {\n terminal: boolean;\n retryDelayMs?: number;\n reason?: string;\n}\n\nconst CLOUDCODE_DOMAINS = new Set([\n \"cloudcode-pa.googleapis.com\",\n \"staging-cloudcode-pa.googleapis.com\",\n \"autopush-cloudcode-pa.googleapis.com\",\n \"cloudaicompanion.googleapis.com\",\n \"daily-cloudcode-pa.googleapis.com\",\n]);\n\n/**\n * NOTE: Special Design - Granular 429 Error Classification and Retry Strategy\n * Traditional network retry modules usually treat 429 errors uniformly as rate-limiting for retries or throwing errors.\n * Here we parse it granularly:\n * 1. Differentiate between \"Account physical quota exhausted\" and \"Model instantaneous capacity overloaded (MODEL_CAPACITY_EXHAUSTED)\".\n * 2. If it's physical quota exhaustion, it's considered an unretriable terminal state to avoid meaningless network requests;\n * If it's a momentary overload of Google's backend model capacity, it's considered retriable, parses RetryInfo delay from response, and notifies the upper layer (showing a Toast in TUI, backing off, and retrying).\n */\nexport async function classifyQuotaResponse(response: Response): Promise {\n const payload = await parseErrorBody(response);\n if (!payload) {\n return null;\n }\n\n const details = Array.isArray(payload.details) ? payload.details : [];\n const retryInfo = details.find(\n (detail): detail is GoogleRpcRetryInfo =>\n isObject(detail) &&\n detail[\"@type\"] === \"type.googleapis.com/google.rpc.RetryInfo\",\n );\n const retryDelayMs =\n (retryInfo?.retryDelay ? parseRetryDelayValue(retryInfo.retryDelay) : null) ??\n parseRetryDelayFromMessage(payload.message ?? \"\") ??\n undefined;\n\n const errorInfo = details.find(\n (detail): detail is GoogleRpcErrorInfo =>\n isObject(detail) &&\n detail[\"@type\"] === \"type.googleapis.com/google.rpc.ErrorInfo\",\n );\n\n if (errorInfo?.domain && !CLOUDCODE_DOMAINS.has(errorInfo.domain)) {\n return null;\n }\n if (errorInfo?.reason === \"QUOTA_EXHAUSTED\") {\n return { terminal: true, retryDelayMs, reason: errorInfo.reason };\n }\n if (errorInfo?.reason === \"RATE_LIMIT_EXCEEDED\") {\n return { terminal: false, retryDelayMs: retryDelayMs ?? 10_000, reason: errorInfo.reason };\n }\n if (errorInfo?.reason === \"MODEL_CAPACITY_EXHAUSTED\") {\n return {\n terminal: retryDelayMs === undefined,\n retryDelayMs,\n reason: errorInfo.reason,\n };\n }\n\n const quotaFailure = details.find(\n (detail): detail is GoogleRpcQuotaFailure =>\n isObject(detail) &&\n detail[\"@type\"] === \"type.googleapis.com/google.rpc.QuotaFailure\",\n );\n if (quotaFailure?.violations?.length) {\n const allTexts = quotaFailure.violations\n .flatMap((violation) => [violation.quotaId ?? \"\", violation.description ?? \"\"])\n .join(\" \")\n .toLowerCase();\n\n if (allTexts.includes(\"perday\") || allTexts.includes(\"daily\") || allTexts.includes(\"per day\")) {\n return { terminal: true, retryDelayMs, reason: errorInfo?.reason };\n }\n if (allTexts.includes(\"perminute\") || allTexts.includes(\"per minute\")) {\n return { terminal: false, retryDelayMs: retryDelayMs ?? 60_000, reason: errorInfo?.reason };\n }\n return { terminal: false, retryDelayMs, reason: errorInfo?.reason };\n }\n\n const quotaLimit = errorInfo?.metadata?.quota_limit?.toLowerCase() ?? \"\";\n if (quotaLimit.includes(\"perminute\") || quotaLimit.includes(\"per minute\")) {\n return { terminal: false, retryDelayMs: retryDelayMs ?? 60_000, reason: errorInfo?.reason };\n }\n\n return { terminal: false, retryDelayMs, reason: errorInfo?.reason };\n}\n\n/**\n * Extracts the RetryInfo delay hint directly from the error payload.\n */\nexport async function parseRetryDelayFromBody(response: Response): Promise {\n const payload = await parseErrorBody(response);\n if (!payload) {\n return null;\n }\n\n const details = Array.isArray(payload.details) ? payload.details : [];\n const retryInfo = details.find(\n (detail): detail is GoogleRpcRetryInfo =>\n isObject(detail) &&\n detail[\"@type\"] === \"type.googleapis.com/google.rpc.RetryInfo\",\n );\n if (retryInfo?.retryDelay) {\n const delayMs = parseRetryDelayValue(retryInfo.retryDelay);\n if (delayMs !== null) {\n return delayMs;\n }\n }\n\n if (typeof payload.message === \"string\") {\n return parseRetryDelayFromMessage(payload.message);\n }\n return null;\n}\n\nfunction parseRetryDelayValue(value: string | { seconds?: number; nanos?: number }): number | null {\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed) {\n return null;\n }\n if (trimmed.endsWith(\"ms\")) {\n const milliseconds = Number(trimmed.slice(0, -2));\n return Number.isFinite(milliseconds) && milliseconds > 0 ? Math.round(milliseconds) : null;\n }\n const match = trimmed.match(/^([\\d.]+)s$/);\n if (!match?.[1]) {\n return null;\n }\n const seconds = Number(match[1]);\n return Number.isFinite(seconds) && seconds > 0 ? Math.round(seconds * 1000) : null;\n }\n\n const seconds = typeof value.seconds === \"number\" ? value.seconds : 0;\n const nanos = typeof value.nanos === \"number\" ? value.nanos : 0;\n if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {\n return null;\n }\n const totalMs = Math.round(seconds * 1000 + nanos / 1e6);\n return totalMs > 0 ? totalMs : null;\n}\n\nfunction parseRetryDelayFromMessage(message: string): number | null {\n const retryMatch = message.match(/Please retry in ([0-9.]+(?:ms|s))/i);\n if (retryMatch?.[1]) {\n return parseRetryDelayValue(retryMatch[1]);\n }\n\n const afterMatch = message.match(/after\\s+([0-9.]+(?:ms|s))/i);\n if (afterMatch?.[1]) {\n return parseRetryDelayValue(afterMatch[1]);\n }\n\n return null;\n}\n\nasync function parseErrorBody(\n response: Response,\n): Promise<{ message?: string; details?: unknown[] } | null> {\n let text = \"\";\n try {\n text = await response.clone().text();\n } catch {\n return null;\n }\n if (!text) {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return null;\n }\n\n const normalized = normalizeErrorEnvelope(parsed);\n if (!normalized || !isObject(normalized.error)) {\n return null;\n }\n\n const error = normalized.error as Record;\n return {\n message: typeof error.message === \"string\" ? error.message : undefined,\n details: Array.isArray(error.details) ? error.details : undefined,\n };\n}\n\nfunction isObject(value: unknown): value is Record {\n return !!value && typeof value === \"object\";\n}\n\nfunction normalizeErrorEnvelope(parsed: unknown): Record | null {\n if (Array.isArray(parsed)) {\n const first = parsed[0];\n return isObject(first) ? first : null;\n }\n return isObject(parsed) ? parsed : null;\n}\n\nexport const MAX_QUOTA_RESET_WAIT_MS = 7_200_000; // 7200 seconds (2 hours)\n\nexport function findResetTimeForModel(\n summary: RetrieveUserQuotaSummaryResponse | null | undefined,\n model?: string,\n): string | null {\n if (!summary) return null;\n\n const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const modelNorm = model ? normalize(model) : \"\";\n\n let targetGroups = summary.groups ?? [];\n if (modelNorm && targetGroups.length > 0) {\n const matched = targetGroups.filter((g) => {\n const gName = normalize(g.displayName ?? \"\");\n return gName.includes(modelNorm) || modelNorm.includes(gName);\n });\n if (matched.length > 0) {\n targetGroups = matched;\n }\n }\n\n const allBuckets: QuotaSummaryBucket[] = [];\n for (const group of targetGroups) {\n if (group.buckets) {\n allBuckets.push(...group.buckets);\n }\n }\n if (allBuckets.length === 0 && summary.buckets) {\n allBuckets.push(...summary.buckets);\n }\n\n const now = Date.now();\n const validResetBuckets = allBuckets.filter((b) => {\n if (!b.resetTime) return false;\n const t = new Date(b.resetTime).getTime();\n return !Number.isNaN(t) && t > now;\n });\n\n if (validResetBuckets.length === 0) {\n return null;\n }\n\n const exhaustedFiveHour = validResetBuckets.find(\n (b) => (b.remainingFraction === 0 || b.disabled) && b.window?.toUpperCase() === \"FIVE_HOUR\",\n );\n if (exhaustedFiveHour?.resetTime) return exhaustedFiveHour.resetTime;\n\n const exhaustedAny = validResetBuckets.find((b) => b.remainingFraction === 0 || b.disabled);\n if (exhaustedAny?.resetTime) return exhaustedAny.resetTime;\n\n const fiveHour = validResetBuckets.find((b) => b.window?.toUpperCase() === \"FIVE_HOUR\");\n if (fiveHour?.resetTime) return fiveHour.resetTime;\n\n validResetBuckets.sort(\n (a, b) => new Date(a.resetTime!).getTime() - new Date(b.resetTime!).getTime(),\n );\n return validResetBuckets[0]?.resetTime ?? null;\n}\n\nexport async function resolveQuotaResetDelay(\n accessToken: string,\n projectId: string,\n model?: string,\n userAgentModel?: string,\n): Promise<{ waitMs: number; resetTime: string } | null> {\n try {\n const summary = await retrieveUserQuotaSummary(accessToken, projectId, userAgentModel);\n if (!summary) return null;\n\n const resetTime = findResetTimeForModel(summary, model);\n if (!resetTime) return null;\n\n const resetTimestamp = new Date(resetTime).getTime();\n if (Number.isNaN(resetTimestamp)) return null;\n\n const waitMs = resetTimestamp - Date.now() + 1000;\n if (waitMs <= 0 || waitMs > MAX_QUOTA_RESET_WAIT_MS) {\n return null;\n }\n\n return { waitMs, resetTime };\n } catch {\n return null;\n }\n}\n\nexport const retryInternals = {\n parseRetryDelayValue,\n parseRetryDelayFromMessage,\n findResetTimeForModel,\n resolveQuotaResetDelay,\n};\n","import { parseRetryDelayFromBody } from \"./quota\";\n\nexport const DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 5000;\nconst DEFAULT_MAX_DELAY_MS = 30000;\n\nconst RETRYABLE_NETWORK_CODES = new Set([\n \"ECONNRESET\",\n \"ETIMEDOUT\",\n \"EPIPE\",\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"ECONNREFUSED\",\n \"ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC\",\n \"ERR_SSL_WRONG_VERSION_NUMBER\",\n \"ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC\",\n \"ERR_SSL_BAD_RECORD_MAC\",\n \"EPROTO\",\n]);\n\n/**\n * Ensures the request body is replayable before attempting a retry.\n */\nexport function canRetryRequest(init: RequestInit | undefined): boolean {\n if (!init?.body) {\n return true;\n }\n\n const body = init.body;\n if (typeof body === \"string\") {\n return true;\n }\n if (typeof URLSearchParams !== \"undefined\" && body instanceof URLSearchParams) {\n return true;\n }\n if (typeof ArrayBuffer !== \"undefined\" && body instanceof ArrayBuffer) {\n return true;\n }\n if (typeof ArrayBuffer !== \"undefined\" && ArrayBuffer.isView(body)) {\n return true;\n }\n if (typeof Blob !== \"undefined\" && body instanceof Blob) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Status code-based retry strategy, consistent with Gemini/Agy CLI.\n */\nexport function isRetryableStatus(status: number): boolean {\n return status === 429 || (status >= 500 && status < 600);\n}\n\n/**\n * Handles transient network failures (including error codes nested in `cause.code`).\n */\nexport function isRetryableNetworkError(error: unknown): boolean {\n const code = getNetworkErrorCode(error);\n if (code && RETRYABLE_NETWORK_CODES.has(code)) {\n return true;\n }\n\n return error instanceof Error && error.message.toLowerCase().includes(\"fetch failed\");\n}\n\n/**\n * Prioritizes parsing retry delay milliseconds via Retry-After header, quota info in response body, or fallback exponential backoff.\n */\nexport async function resolveRetryDelayMs(\n response: Response,\n attempt: number,\n quotaDelayMs?: number,\n): Promise {\n const retryAfterMsHeader = parseRetryAfterMs(response.headers.get(\"retry-after-ms\"));\n if (retryAfterMsHeader !== null) {\n return clampDelay(retryAfterMsHeader);\n }\n\n const retryAfterHeader = parseRetryAfter(response.headers.get(\"retry-after\"));\n if (retryAfterHeader !== null) {\n return clampDelay(retryAfterHeader);\n }\n\n if (quotaDelayMs !== undefined) {\n return clampDelay(quotaDelayMs);\n }\n\n const bodyDelay = await parseRetryDelayFromBody(response);\n if (bodyDelay !== null) {\n return clampDelay(bodyDelay);\n }\n\n return getExponentialDelayWithJitter(attempt);\n}\n\nexport function getExponentialDelayWithJitter(attempt: number): number {\n const base = Math.min(DEFAULT_MAX_DELAY_MS, DEFAULT_INITIAL_DELAY_MS * Math.pow(2, attempt - 1));\n const jitter = base * 0.3 * (Math.random() * 2 - 1);\n return clampDelay(base + jitter);\n}\n\nexport function wait(ms: number): Promise {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\nfunction getNetworkErrorCode(error: unknown): string | undefined {\n const readCode = (value: unknown): string | undefined => {\n if (!value || typeof value !== \"object\") {\n return undefined;\n }\n if (\"code\" in value && typeof (value as { code?: unknown }).code === \"string\") {\n return (value as { code: string }).code;\n }\n return undefined;\n };\n\n const direct = readCode(error);\n if (direct) {\n return direct;\n }\n\n let cursor: unknown = error;\n for (let depth = 0; depth < 5; depth += 1) {\n if (!cursor || typeof cursor !== \"object\" || !(\"cause\" in cursor)) {\n break;\n }\n cursor = (cursor as { cause?: unknown }).cause;\n const code = readCode(cursor);\n if (code) {\n return code;\n }\n }\n return undefined;\n}\n\nfunction parseRetryAfterMs(value: string | null): number | null {\n if (!value) {\n return null;\n }\n const parsed = Number(value.trim());\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return Math.round(parsed);\n}\n\nfunction parseRetryAfter(value: string | null): number | null {\n if (!value) {\n return null;\n }\n const trimmed = value.trim();\n if (!trimmed) {\n return null;\n }\n const seconds = Number(trimmed);\n if (Number.isFinite(seconds)) {\n return Math.max(0, Math.round(seconds * 1000));\n }\n const parsedDate = Date.parse(trimmed);\n if (!Number.isNaN(parsedDate)) {\n return Math.max(0, parsedDate - Date.now());\n }\n return null;\n}\n\nfunction clampDelay(delayMs: number): number {\n if (!Number.isFinite(delayMs)) {\n return DEFAULT_MAX_DELAY_MS;\n }\n return Math.min(Math.max(0, Math.round(delayMs)), DEFAULT_MAX_DELAY_MS);\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\n\ninterface CooldownData {\n version: \"1.0\";\n entries: Record;\n updatedAt: number;\n}\n\nconst WRITE_THROTTLE_MS = 5000;\n\nfunction getConfigDir(): string {\n const platform = process.platform;\n if (platform === \"win32\") {\n return join(process.env.APPDATA || join(homedir(), \"AppData\", \"Roaming\"), \"opencode\");\n }\n const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), \".config\");\n return join(xdgConfig, \"opencode\");\n}\n\nfunction getCooldownFilePath(): string {\n return join(getConfigDir(), \"antigravity-retry-cooldowns.json\");\n}\n\nexport function loadCooldowns(): Map {\n const result = new Map();\n try {\n const filePath = getCooldownFilePath();\n if (!existsSync(filePath)) {\n return result;\n }\n\n const content = readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(content) as CooldownData;\n if (data.version !== \"1.0\") {\n return result;\n }\n\n const now = Date.now();\n for (const [key, expiresAt] of Object.entries(data.entries)) {\n if (typeof expiresAt === \"number\" && expiresAt > now) {\n result.set(key, expiresAt);\n }\n }\n } catch {\n // Fault tolerance: start fresh on disk load failure\n }\n return result;\n}\n\nexport function saveCooldowns(entries: Map): boolean {\n try {\n const filePath = getCooldownFilePath();\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const now = Date.now();\n const serializable: Record = {};\n for (const [key, expiresAt] of entries.entries()) {\n if (expiresAt > now) {\n serializable[key] = expiresAt;\n }\n }\n\n const data: CooldownData = {\n version: \"1.0\",\n entries: serializable,\n updatedAt: now,\n };\n\n const tmpPath = join(tmpdir(), `antigravity-cooldowns-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);\n writeFileSync(tmpPath, JSON.stringify(data), \"utf-8\");\n\n try {\n renameSync(tmpPath, filePath);\n } catch {\n writeFileSync(filePath, readFileSync(tmpPath));\n try {\n unlinkSync(tmpPath);\n } catch {\n // Ignore temp file deletion failure\n }\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport class CooldownStore {\n private dirty = false;\n private lastWriteTime = 0;\n private writeTimer: ReturnType | null = null;\n private entries: Map = new Map();\n\n bind(entries: Map): void {\n this.entries = entries;\n }\n\n markDirty(): void {\n this.dirty = true;\n this.scheduleThrottledWrite();\n }\n\n flush(): boolean {\n this.dirty = false;\n this.clearWriteTimer();\n this.lastWriteTime = Date.now();\n return saveCooldowns(this.entries);\n }\n\n shutdown(): void {\n this.clearWriteTimer();\n if (this.dirty) {\n saveCooldowns(this.entries);\n this.dirty = false;\n }\n }\n\n private scheduleThrottledWrite(): void {\n if (this.writeTimer) {\n return;\n }\n\n const elapsed = Date.now() - this.lastWriteTime;\n const remaining = Math.max(0, WRITE_THROTTLE_MS - elapsed);\n\n this.writeTimer = setTimeout(() => {\n this.writeTimer = null;\n this.lastWriteTime = Date.now();\n if (this.dirty) {\n this.dirty = false;\n saveCooldowns(this.entries);\n }\n }, remaining);\n\n if (this.writeTimer && typeof this.writeTimer === \"object\" && \"unref\" in this.writeTimer) {\n this.writeTimer.unref();\n }\n }\n\n private clearWriteTimer(): void {\n if (this.writeTimer) {\n clearTimeout(this.writeTimer);\n this.writeTimer = null;\n }\n }\n}\n","import {\n canRetryRequest,\n DEFAULT_MAX_ATTEMPTS,\n getExponentialDelayWithJitter,\n isRetryableNetworkError,\n isRetryableStatus,\n resolveRetryDelayMs,\n wait,\n} from \"./helpers\";\nimport {\n classifyQuotaResponse,\n MAX_QUOTA_RESET_WAIT_MS,\n resolveQuotaResetDelay,\n retryInternals,\n} from \"./quota\";\nimport { agyFetch } from \"../../fetch\";\nimport { CooldownStore, loadCooldowns } from \"./cooldown-store\";\n\nconst retryCooldownByKey = new Map();\nconst cooldownStore = new CooldownStore();\nlet cooldownPersistenceInitialized = false;\n\nfunction initCooldownPersistence(): void {\n if (cooldownPersistenceInitialized) return;\n cooldownPersistenceInitialized = true;\n try {\n const persisted = loadCooldowns();\n for (const [key, expiresAt] of persisted.entries()) {\n retryCooldownByKey.set(key, expiresAt);\n }\n cooldownStore.bind(retryCooldownByKey);\n if (typeof process !== \"undefined\") {\n process.on(\"exit\", () => {\n cooldownStore.shutdown();\n });\n }\n } catch {\n cooldownStore.bind(retryCooldownByKey);\n }\n}\n\nexport { initCooldownPersistence };\nconst MODEL_CAPACITY_COOLDOWN_MS = 8000;\n\n/**\n * Sends a request with retry/exponential backoff semantics, consistent with Gemini/Agy CLI.\n */\nexport async function fetchWithRetry(\n input: RequestInfo,\n init: RequestInit | undefined,\n): Promise {\n if (!cooldownPersistenceInitialized) initCooldownPersistence();\n if (!canRetryRequest(init)) {\n return agyFetch(input, init);\n }\n\n const retryInit = cloneRetryableInit(init);\n const throttleKey = buildRetryThrottleKey(input, retryInit);\n await waitForRetryCooldown(throttleKey, retryInit.signal);\n let attempt = 1;\n let hasRetriedQuotaReset = false;\n const url = readRequestUrl(input);\n\n while (attempt <= DEFAULT_MAX_ATTEMPTS) {\n let response: Response;\n try {\n response = await agyFetch(input, retryInit);\n } catch (error) {\n if (attempt >= DEFAULT_MAX_ATTEMPTS || !isRetryableNetworkError(error)) {\n throw error;\n }\n if (retryInit.signal?.aborted) {\n throw error;\n }\n\n const delayMs = getExponentialDelayWithJitter(attempt);\n await wait(delayMs);\n attempt += 1;\n continue;\n }\n\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const quotaContext = response.status === 429 ? await classifyQuotaResponse(response) : null;\n if (response.status === 429 && quotaContext?.terminal) {\n if (quotaContext.reason === \"MODEL_CAPACITY_EXHAUSTED\") {\n const cooldownMs = quotaContext.retryDelayMs ?? MODEL_CAPACITY_COOLDOWN_MS;\n setRetryCooldown(throttleKey, cooldownMs);\n return response;\n }\n\n if (quotaContext.reason === \"QUOTA_EXHAUSTED\" && !hasRetriedQuotaReset && !retryInit.signal?.aborted) {\n const body = typeof retryInit.body === \"string\" ? safeParseBody(retryInit.body) : null;\n const project = readString(body?.project);\n const model = readString(body?.model);\n const token = extractAuthToken(retryInit.headers);\n\n if (token && project) {\n const resetInfo = await resolveQuotaResetDelay(token, project, model);\n if (resetInfo && resetInfo.waitMs > 0 && resetInfo.waitMs <= MAX_QUOTA_RESET_WAIT_MS) {\n hasRetriedQuotaReset = true;\n setRetryCooldown(throttleKey, resetInfo.waitMs);\n await wait(resetInfo.waitMs);\n if (retryInit.signal?.aborted) {\n return response;\n }\n continue;\n }\n }\n }\n\n return response;\n }\n\n if (attempt >= DEFAULT_MAX_ATTEMPTS || retryInit.signal?.aborted) {\n return response;\n }\n\n const delayMs = await resolveRetryDelayMs(response, attempt, quotaContext?.retryDelayMs);\n if (delayMs > 0 && response.status === 429) {\n setRetryCooldown(throttleKey, delayMs);\n }\n if (delayMs > 0) {\n await wait(delayMs);\n }\n attempt += 1;\n }\n\n return agyFetch(input, retryInit);\n}\n\nfunction cloneRetryableInit(init: RequestInit | undefined): RequestInit {\n if (!init) {\n return {};\n }\n return {\n ...init,\n headers: new Headers(init.headers ?? {}),\n };\n}\n\nfunction buildRetryThrottleKey(input: RequestInfo, init: RequestInit): string {\n const url = readRequestUrl(input);\n const body = typeof init.body === \"string\" ? safeParseBody(init.body) : null;\n const project = readString(body?.project);\n const model = readString(body?.model);\n return `${url}|${project ?? \"\"}|${model ?? \"\"}`;\n}\n\nasync function waitForRetryCooldown(key: string, signal?: AbortSignal | null): Promise {\n const until = retryCooldownByKey.get(key);\n if (!until) {\n return;\n }\n\n const remaining = until - Date.now();\n if (remaining <= 0) {\n retryCooldownByKey.delete(key);\n return;\n }\n if (signal?.aborted) {\n return;\n }\n\n await wait(remaining);\n retryCooldownByKey.delete(key);\n}\n\nfunction setRetryCooldown(key: string, delayMs: number): void {\n if (!cooldownPersistenceInitialized) initCooldownPersistence();\n const next = Date.now() + delayMs;\n const current = retryCooldownByKey.get(key) ?? 0;\n retryCooldownByKey.set(key, Math.max(current, next));\n cooldownStore.markDirty();\n}\n\nexport function shutdownRetryCooldowns(): void {\n if (cooldownPersistenceInitialized) {\n cooldownStore.shutdown();\n }\n}\n\nfunction readRequestUrl(input: RequestInfo): string {\n if (typeof input === \"string\") {\n return input;\n }\n if (input instanceof URL) {\n return input.toString();\n }\n\n const request = input as Request;\n if (request.url) {\n return request.url;\n }\n return input.toString();\n}\n\nfunction safeParseBody(body: string): Record | null {\n if (!body) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(body);\n if (parsed && typeof parsed === \"object\") {\n return parsed as Record;\n }\n } catch {}\n return null;\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\nfunction extractAuthToken(headers?: HeadersInit): string | undefined {\n if (!headers) return undefined;\n const h = new Headers(headers);\n const auth = h.get(\"authorization\") || h.get(\"Authorization\");\n if (!auth) return undefined;\n const match = auth.match(/^Bearer\\s+(.+)$/i);\n return match?.[1]?.trim() || auth.trim();\n}\n\nexport { retryInternals };\n","const OSC8_OPEN = '\\x1b]8;;';\nconst OSC8_CLOSE = '\\x07';\n\nexport function supportsOsc8Hyperlinks(): boolean {\n if (process.stdout && !process.stdout.isTTY) {\n return false;\n }\n\n if (process.env.OPENCODE_HEADLESS) {\n return false;\n }\n\n const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? '';\n if (termProgram === 'iterm.app' || termProgram === 'wezterm' || termProgram === 'ghostty') {\n return true;\n }\n\n if (process.env.KITTY_WINDOW_ID) {\n return true;\n }\n\n const vteVersion = parseInt(process.env.VTE_VERSION ?? '0', 10);\n if (vteVersion >= 5000) {\n return true;\n }\n\n const colorTerm = process.env.COLORTERM?.toLowerCase() ?? '';\n if (colorTerm === 'truecolor' || colorTerm === '24bit') {\n const term = process.env.TERM?.toLowerCase() ?? '';\n if (term.startsWith('xterm') || term.startsWith('alacritty') || term === 'termion') {\n return true;\n }\n }\n\n return false;\n}\n\nexport function formatHyperlink(url: string, text?: string): string {\n if (!supportsOsc8Hyperlinks()) {\n if (text) {\n return text === url ? text : `${text} (${url})`;\n }\n return url;\n }\n const displayText = text ?? url;\n return `${OSC8_OPEN}${url}${OSC8_CLOSE}${displayText}${OSC8_OPEN}${OSC8_CLOSE}`;\n}\n\nexport function stripOsc8(text: string): string {\n return text.replace(/\\x1b\\]8;[^;]*;[^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)/g, '');\n}\n","export const FREE_TIER_ID = \"free-tier\";\nexport const LEGACY_TIER_ID = \"legacy-tier\";\n\nexport const CODE_ASSIST_METADATA = {\n ideType: \"ANTIGRAVITY\",\n} as const;\n\nexport interface AgyUserTier {\n id?: string;\n isDefault?: boolean;\n userDefinedCloudaicompanionProject?: boolean;\n name?: string;\n description?: string;\n}\n\nexport interface CloudAiCompanionProject {\n id?: string;\n}\n\nexport interface AgyIneligibleTier {\n reasonCode?: string;\n reasonMessage?: string;\n validationUrl?: string;\n validationLearnMoreUrl?: string;\n}\n\nexport interface LoadCodeAssistPayload {\n cloudaicompanionProject?: string | CloudAiCompanionProject;\n currentTier?: {\n id?: string;\n name?: string;\n };\n allowedTiers?: AgyUserTier[];\n ineligibleTiers?: AgyIneligibleTier[];\n}\n\nexport interface OnboardUserPayload {\n name?: string;\n done?: boolean;\n response?: {\n cloudaicompanionProject?: {\n id?: string;\n };\n };\n}\n\nexport interface RetrieveUserQuotaBucket {\n remainingAmount?: string;\n remainingFraction?: number;\n resetTime?: string;\n tokenType?: string;\n modelId?: string;\n}\n\nexport interface RetrieveUserQuotaResponse {\n buckets?: RetrieveUserQuotaBucket[];\n}\n\nexport interface QuotaSummaryBucket {\n bucketId?: string;\n displayName?: string;\n description?: string;\n window?: string;\n remaining?: string;\n remainingFraction?: number;\n remainingAmount?: string;\n disabled?: boolean;\n resetTime?: string;\n}\n\nexport interface QuotaSummaryGroup {\n displayName?: string;\n description?: string;\n buckets?: QuotaSummaryBucket[];\n}\n\nexport interface RetrieveUserQuotaSummaryResponse {\n groups?: QuotaSummaryGroup[];\n buckets?: QuotaSummaryBucket[];\n description?: string;\n}\n\n/**\n * Thrown during Gemini enablement if the required Google Cloud project is missing.\n */\nexport class ProjectIdRequiredError extends Error {\n constructor() {\n super(\n \"Google Gemini/Agy requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, then set `provider.google.options.projectId` in your Opencode config (or set OPENCODE_AGY_PROJECT_ID / GOOGLE_CLOUD_PROJECT).\",\n );\n }\n}\n\nexport class ProjectAccessDeniedError extends Error {\n constructor(projectId: string | undefined, backendMessage: string | undefined) {\n const projectStr = projectId ? `project '${projectId}'` : 'the requested project';\n const msg = backendMessage ? `\\nBackend response: ${backendMessage}` : '';\n super(`Access denied to ${projectStr}. Ensure the Gemini for Google Cloud API is enabled and you have the correct IAM permissions.${msg}`);\n this.name = 'ProjectAccessDeniedError';\n }\n}\n\nexport class AccountValidationRequiredError extends Error {\n validationUrl?: string;\n validationLearnMoreUrl?: string;\n\n constructor(\n message: string,\n validationUrl?: string,\n validationLearnMoreUrl?: string,\n ) {\n const parts = [message.trim()];\n if (validationUrl) {\n parts.push(`Complete validation: ${validationUrl}`);\n }\n if (validationLearnMoreUrl) {\n parts.push(`Learn more: ${validationLearnMoreUrl}`);\n }\n\n super(parts.join(\"\\n\"));\n this.name = \"AccountValidationRequiredError\";\n this.validationUrl = validationUrl;\n this.validationLearnMoreUrl = validationLearnMoreUrl;\n }\n}\n","import type { OAuthAuthDetails } from \"../types\";\nimport {\n AccountValidationRequiredError,\n CODE_ASSIST_METADATA,\n LEGACY_TIER_ID,\n type CloudAiCompanionProject,\n type AgyIneligibleTier,\n type AgyUserTier,\n} from \"./types\";\n\n/**\n * Builds the metadata headers required for the Code Assist API.\n */\nexport function buildMetadata(projectId?: string, includeDuetProject = true): Record {\n const metadata: Record = {\n ...CODE_ASSIST_METADATA,\n };\n if (projectId && includeDuetProject) {\n metadata.duetProject = projectId;\n }\n return metadata;\n}\n\n/**\n * Normalizes project identifiers from API payloads or configuration.\n */\nexport function normalizeProjectId(value?: string | CloudAiCompanionProject): string | undefined {\n if (!value) {\n return undefined;\n }\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n return trimmed ? trimmed : undefined;\n }\n if (typeof value === \"object\" && typeof value.id === \"string\") {\n const trimmed = value.id.trim();\n return trimmed ? trimmed : undefined;\n }\n return undefined;\n}\n\n/**\n * Selects the default hierarchy ID from the allowed hierarchy list.\n */\nexport function pickOnboardTier(allowedTiers?: AgyUserTier[]): AgyUserTier {\n if (allowedTiers && allowedTiers.length > 0) {\n for (const tier of allowedTiers) {\n if (tier?.isDefault) {\n return tier;\n }\n }\n return allowedTiers[0] ?? { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };\n }\n return { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };\n}\n\n/**\n * Builds a concise error message for non-compliant hierarchy payloads.\n */\nexport function buildIneligibleTierMessage(tiers?: AgyIneligibleTier[]): string | undefined {\n if (!tiers || tiers.length === 0) {\n return undefined;\n }\n const reasons = tiers\n .map((tier) => tier?.reasonMessage?.trim())\n .filter((message): message is string => !!message);\n return reasons.length > 0 ? reasons.join(\", \") : undefined;\n}\n\nexport function throwIfValidationRequired(tiers?: AgyIneligibleTier[]): void {\n if (!tiers || tiers.length === 0) {\n return;\n }\n\n const validationTier = tiers.find((tier) => {\n const reasonCode = tier?.reasonCode?.trim().toUpperCase();\n return reasonCode === \"VALIDATION_REQUIRED\" && !!tier.validationUrl?.trim();\n });\n if (!validationTier) {\n return;\n }\n\n throw new AccountValidationRequiredError(\n validationTier.reasonMessage?.trim() || \"Verify your account to continue.\",\n validationTier.validationUrl?.trim(),\n validationTier.validationLearnMoreUrl?.trim(),\n );\n}\n\n/**\n * Detects VPC-SC errors from Cloud Code responses.\n */\nexport function isVpcScError(payload: unknown): boolean {\n if (!payload || typeof payload !== \"object\") {\n return false;\n }\n const error = (payload as { error?: unknown }).error;\n if (!error || typeof error !== \"object\") {\n return false;\n }\n const details = (error as { details?: unknown }).details;\n if (!Array.isArray(details)) {\n return false;\n }\n return details.some((detail) => {\n if (!detail || typeof detail !== \"object\") {\n return false;\n }\n return (detail as { reason?: unknown }).reason === \"SECURITY_POLICY_VIOLATED\";\n });\n}\n\n/**\n * Safely parses JSON, returning null on failure.\n */\nexport function parseJsonSafe(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\n/**\n * Promise-based delay utility.\n */\nexport function wait(ms: number): Promise {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n/**\n * Generates a cache key for the project context based on the refresh token.\n */\nexport function getCacheKey(auth: OAuthAuthDetails): string | undefined {\n const refresh = auth.refresh?.trim();\n if (!refresh) {\n return undefined;\n }\n const [baseRefreshToken = ''] = refresh.split('|');\n return baseRefreshToken ? baseRefreshToken : undefined;\n}\n","import { AGY_CODE_ASSIST_ENDPOINT } from '../constants';\nimport { agyFetch } from '../fetch';\nimport { fetchWithRetry } from './retry';\nimport { createAgyActivityRequestId } from './activity-request-id';\nimport { buildAgyCliUserAgent } from './user-agent';\nimport { formatHyperlink } from './terminal-hyperlink';\nimport {\n FREE_TIER_ID,\n type LoadCodeAssistPayload,\n type OnboardUserPayload,\n ProjectIdRequiredError,\n ProjectAccessDeniedError\n} from '../plugin/project/types';\nimport { buildMetadata, isVpcScError, parseJsonSafe, wait } from '../plugin/project/utils';\n\n/**\n * Loads hosted project information for a given access token and optional project.\n */\nexport async function loadManagedProject(\n accessToken: string,\n projectId?: string,\n userAgentModel?: string\n): Promise {\n try {\n const metadata = buildMetadata(projectId);\n const requestBody: Record = { metadata };\n if (projectId) {\n requestBody.cloudaicompanionProject = projectId;\n }\n\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`;\n if (process.env.OPENCODE_AGY_VERBOSE_LOGS === \"1\") {\n console.warn(`[Agy Auth] loadManagedProject calling URL: ${formatHyperlink(url)} with project: ${projectId || 'none'}`);\n }\n const headers = buildCodeAssistHeaders(accessToken, userAgentModel);\n\n const response = await fetchWithRetry(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(requestBody)\n });\n\n if (!response.ok) {\n if (response.status === 403 || response.status === 404) {\n console.warn(`[Agy Auth] loadManagedProject failed with ${response.status} (possible Cloud API mismatch/unauthorized). URL: ${formatHyperlink(url)}, Project: ${projectId}`);\n const responseText = await readResponseTextIfNeeded(response, true);\n if (responseText && isVpcScError(responseText)) {\n console.warn(`[Agy Auth] loadManagedProject: Detected VPC Service Controls block`);\n }\n throw new ProjectAccessDeniedError(projectId, responseText);\n } else {\n const cleanStatusText = response.statusText.replace(/[\\r\\n]+/g, ' ').trim();\n if (response.status === 429) {\n console.warn(`[Agy Auth] loadManagedProject failed with 429 ${cleanStatusText} (rate limited after retries; see Retry-After / quota exhaustion). URL: ${formatHyperlink(url)}, Project: ${projectId}`);\n } else {\n console.warn(`[Agy Auth] loadManagedProject failed with ${response.status} ${cleanStatusText}`);\n }\n }\n return null;\n }\n\n const responseJson = await response.json();\n return responseJson as LoadCodeAssistPayload;\n } catch (error) {\n if (error instanceof ProjectAccessDeniedError) {\n throw error;\n }\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to load code assist project: ${errStr}`);\n return null;\n }\n}\n\n/**\n * Enables a hosted project for the user, optionally retrying until complete.\n */\nexport async function onboardManagedProject(\n accessToken: string,\n tierId: string,\n projectId?: string,\n userAgentModel?: string,\n attempts = 10,\n delayMs = 5000\n): Promise {\n const isFreeTier = tierId === FREE_TIER_ID;\n const metadata = buildMetadata(projectId, !isFreeTier);\n const requestBody: Record = { tierId, metadata };\n\n if (!isFreeTier) {\n if (!projectId) {\n throw new ProjectIdRequiredError();\n }\n requestBody.cloudaicompanionProject = projectId;\n }\n\n const baseUrl = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal`;\n const onboardUrl = `${baseUrl}:onboardUser`;\n if (process.env.OPENCODE_AGY_VERBOSE_LOGS === \"1\") {\n console.warn(`[Agy Auth] onboardManagedProject calling URL: ${formatHyperlink(onboardUrl)} with project: ${projectId || 'none'}`);\n }\n\n try {\n const response = await fetchWithDebug(\n onboardUrl,\n 'POST',\n buildCodeAssistHeaders(accessToken, userAgentModel),\n requestBody,\n projectId\n );\n if (!response.ok) {\n const cleanStatusText = response.statusText.replace(/[\\r\\n]+/g, ' ').trim();\n console.warn(`[Agy Auth] onboardManagedProject response not ok: status ${response.status} ${cleanStatusText}`);\n return undefined;\n }\n\n let payload = (await response.json()) as OnboardUserPayload;\n if (!payload.done && payload.name) {\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n await wait(delayMs);\n const operationUrl = `${baseUrl}/${payload.name}`;\n const opResponse = await fetchWithDebug(\n operationUrl,\n 'GET',\n buildCodeAssistHeaders(accessToken, userAgentModel),\n undefined,\n projectId\n );\n if (!opResponse.ok) {\n return undefined;\n }\n payload = (await opResponse.json()) as OnboardUserPayload;\n if (payload.done) {\n break;\n }\n }\n }\n\n const managedProjectId = payload.response?.cloudaicompanionProject?.id;\n if (payload.done && managedProjectId) {\n return managedProjectId;\n }\n if (payload.done && projectId) {\n return projectId;\n }\n } catch (error) {\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to onboard Antigravity managed project: ${errStr}`);\n return undefined;\n }\n\n return undefined;\n}\n\nfunction buildCodeAssistHeaders(\n accessToken: string,\n userAgentModel?: string\n): Record {\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${accessToken}`,\n 'User-Agent': userAgent\n };\n}\n\nasync function fetchWithDebug(\n url: string,\n method: 'GET' | 'POST',\n headers: Record,\n body: Record | undefined,\n projectId?: string\n): Promise {\n const response = await agyFetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined\n });\n return response;\n}\n\nasync function readResponseTextIfNeeded(response: Response, needed: boolean): Promise {\n if (!needed && response.ok) {\n return undefined;\n }\n try {\n return await response.clone().text();\n } catch {\n return undefined;\n }\n}\n","import { AGY_PROVIDER_ID } from '../../constants';\nimport { formatRefreshParts, parseRefreshParts } from '../auth';\nimport type { OAuthAuthDetails, PluginClient, ProjectContextResult } from '../types';\nimport { loadManagedProject, onboardManagedProject } from '../../sdk/fetch_project';\nimport { FREE_TIER_ID, LEGACY_TIER_ID, ProjectIdRequiredError, ProjectAccessDeniedError, type LoadCodeAssistPayload } from './types';\nimport {\n buildIneligibleTierMessage,\n getCacheKey,\n normalizeProjectId,\n pickOnboardTier,\n throwIfValidationRequired\n} from './utils';\n\nconst projectContextResultCache = new Map();\nconst projectContextPendingCache = new Map>();\n\n/**\n * Clears cached project context results and pending Promises.\n */\nexport function invalidateProjectContextCache(refresh?: string): void {\n if (!refresh) {\n projectContextPendingCache.clear();\n projectContextResultCache.clear();\n return;\n }\n\n projectContextPendingCache.delete(refresh);\n projectContextResultCache.delete(refresh);\n\n const prefix = `${refresh}|cfg:`;\n for (const key of projectContextPendingCache.keys()) {\n if (key.startsWith(prefix)) {\n projectContextPendingCache.delete(key);\n }\n }\n for (const key of projectContextResultCache.keys()) {\n if (key.startsWith(prefix)) {\n projectContextResultCache.delete(key);\n }\n }\n}\n\n/**\n * Resolves the project context corresponding to the access token, optionally persisting updated auth details.\n */\nexport async function resolveProjectContextFromAccessToken(\n auth: OAuthAuthDetails,\n accessToken: string,\n configuredProjectId?: string,\n persistAuth?: (auth: OAuthAuthDetails) => Promise,\n userAgentModel?: string\n): Promise {\n const parts = parseRefreshParts(auth.refresh);\n const configuredProject = configuredProjectId?.trim();\n const projectId = configuredProject || parts.projectId;\n\n if (!configuredProject && (projectId || parts.managedProjectId)) {\n return {\n auth,\n effectiveProjectId: projectId || parts.managedProjectId || ''\n };\n }\n\n let loadPayload: LoadCodeAssistPayload | null = null;\n try {\n loadPayload = await loadManagedProject(accessToken, projectId, userAgentModel);\n } catch (error) {\n if (error instanceof ProjectAccessDeniedError) {\n throw error;\n }\n console.warn(`[Agy Auth] loadManagedProject returned an error for project: ${projectId || 'none'}, ${error}`);\n }\n\n if (!loadPayload) {\n console.warn(`[Agy Auth] loadManagedProject returned null for project: ${projectId || 'none'} (possible 429 rate limit / quota exhaustion / transient backend error - not necessarily a missing project config)`);\n if (projectId) {\n throw new Error(\n `Failed to load project context for '${projectId}'. This may be due to a rate limit, quota exhaustion, or transient backend error. Please try again later.`\n );\n }\n throw new ProjectIdRequiredError();\n }\n\n const managedProjectId = normalizeProjectId(loadPayload.cloudaicompanionProject);\n if (managedProjectId) {\n const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, managedProjectId);\n if (persistAuth) {\n await persistAuth(updatedAuth);\n }\n return { auth: updatedAuth, effectiveProjectId: managedProjectId };\n }\n\n const currentTierId = loadPayload.currentTier?.id;\n if (!currentTierId) {\n throwIfValidationRequired(loadPayload.ineligibleTiers);\n }\n\n if (currentTierId) {\n if (projectId) {\n return { auth, effectiveProjectId: projectId };\n }\n const ineligibleMessage = buildIneligibleTierMessage(loadPayload.ineligibleTiers);\n if (ineligibleMessage) {\n throw new Error(ineligibleMessage);\n }\n throw new ProjectIdRequiredError();\n }\n\n const tier = pickOnboardTier(loadPayload.allowedTiers);\n const tierId = tier.id ?? LEGACY_TIER_ID;\n if (tierId !== FREE_TIER_ID && !projectId) {\n throw new ProjectIdRequiredError();\n }\n\n const onboardedProjectId = await onboardManagedProject(\n accessToken,\n tierId,\n projectId,\n userAgentModel\n );\n if (onboardedProjectId) {\n const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, onboardedProjectId);\n if (persistAuth) {\n await persistAuth(updatedAuth);\n }\n return { auth: updatedAuth, effectiveProjectId: onboardedProjectId };\n }\n\n if (projectId) {\n return { auth, effectiveProjectId: projectId };\n }\n console.warn(`[Agy Auth] onboardManagedProject failed to resolve a project ID for tier: ${tierId}, configured project: ${projectId || 'none'}`);\n throw new ProjectIdRequiredError();\n}\n\n/**\n * Resolves the effective project ID for the current auth state and caches the result by refresh token.\n */\nexport async function ensureProjectContext(\n auth: OAuthAuthDetails,\n client: PluginClient,\n configuredProjectId?: string,\n userAgentModel?: string\n): Promise {\n const accessToken = auth.access;\n if (!accessToken) {\n return { auth, effectiveProjectId: '' };\n }\n\n const cacheKey = buildProjectCacheKey(auth, configuredProjectId);\n if (cacheKey) {\n const cached = projectContextResultCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n const pending = projectContextPendingCache.get(cacheKey);\n if (pending) {\n return pending;\n }\n }\n\n const resolveContext = async (): Promise =>\n resolveProjectContextFromAccessToken(\n auth,\n accessToken,\n configuredProjectId,\n async (updatedAuth) => {\n await client.auth.set({\n path: { id: AGY_PROVIDER_ID },\n body: updatedAuth\n });\n },\n userAgentModel\n );\n\n if (!cacheKey) {\n return resolveContext();\n }\n\n const promise = resolveContext()\n .then((result) => {\n const nextKey = getCacheKey(result.auth) ?? cacheKey;\n projectContextPendingCache.delete(cacheKey);\n projectContextResultCache.set(nextKey, result);\n if (nextKey !== cacheKey) {\n projectContextResultCache.delete(cacheKey);\n }\n return result;\n })\n .catch((error) => {\n projectContextPendingCache.delete(cacheKey);\n throw error;\n });\n\n projectContextPendingCache.set(cacheKey, promise);\n return promise;\n}\n\nfunction withProjectAuth(\n auth: OAuthAuthDetails,\n refreshToken: string,\n projectId: string | undefined,\n managedProjectId: string\n): OAuthAuthDetails {\n return {\n ...auth,\n refresh: formatRefreshParts({\n refreshToken,\n projectId,\n managedProjectId\n })\n };\n}\n\nfunction buildProjectCacheKey(auth: OAuthAuthDetails, configuredProjectId?: string): string | undefined {\n const base = getCacheKey(auth);\n if (!base) {\n return undefined;\n }\n const project = configuredProjectId?.trim() ?? '';\n return project ? `${base}|cfg:${project}` : base;\n}\n","import type { Config } from \"./types\";\n\nimport { AGY_PROVIDER_ID } from \"../constants\";\nimport type { PluginClient, Provider } from \"./types\";\n\ninterface ResolveConfiguredProjectIdInput {\n provider?: Provider | null;\n config?: Config | null;\n configProjectId?: string;\n env?: NodeJS.ProcessEnv;\n}\n\nexport function resolveConfiguredProjectId(\n input: ResolveConfiguredProjectIdInput = {},\n): string | undefined {\n const env = input.env ?? process.env;\n\n return (\n normalizeProjectId(env.OPENCODE_AGY_PROJECT_ID) ??\n resolveConfiguredProjectIdFromProvider(input.provider) ??\n normalizeProjectId(input.configProjectId) ??\n resolveConfiguredProjectIdFromConfig(input.config) ??\n normalizeProjectId(env.GOOGLE_CLOUD_PROJECT) ??\n normalizeProjectId(env.GOOGLE_CLOUD_PROJECT_ID)\n );\n}\n\nfunction resolveConfiguredProjectIdFromProvider(\n provider: Provider | null | undefined,\n): string | undefined {\n if (!provider || typeof provider !== \"object\") {\n return undefined;\n }\n return normalizeProjectId(provider.options?.projectId);\n}\n\nexport function resolveConfiguredProjectIdFromConfig(\n config: Config | null | undefined,\n): string | undefined {\n if (!config?.provider || typeof config.provider !== \"object\") {\n return undefined;\n }\n\n const providerConfig = config.provider[AGY_PROVIDER_ID];\n return normalizeProjectId(providerConfig?.options?.projectId);\n}\n\nexport async function resolveConfiguredProjectIdFromClient(\n client: PluginClient | null | undefined,\n): Promise {\n if (!client?.config?.get) {\n return undefined;\n }\n\n try {\n const result = await client.config.get();\n return resolveConfiguredProjectIdFromConfig(result?.data);\n } catch {\n return undefined;\n }\n}\n\nfunction normalizeProjectId(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed || undefined;\n}\n","import { createHash } from \"node:crypto\";\nimport { accessTokenExpired } from \"./auth\";\nimport type { OAuthAuthDetails } from \"./types\";\nimport { SignatureCache, createSignatureCache, type SignatureCacheConfig } from \"../sdk/cache/signature-cache\";\n\n// In-memory cache of authorized account details.\nconst authCache = new Map();\n\n/**\n * Normalizes and trims whitespace from the refresh token string.\n */\nfunction normalizeRefreshKey(refresh?: string): string | undefined {\n const key = refresh?.trim();\n return key ? key : undefined;\n}\n\n/**\n * Extracts valid OAuthAuthDetails from cache. Reuses an available and unexpired Token if present, otherwise prioritizes the latest provided value.\n */\nexport function resolveCachedAuth(auth: OAuthAuthDetails): OAuthAuthDetails {\n const key = normalizeRefreshKey(auth.refresh);\n if (!key) {\n return auth;\n }\n\n const cached = authCache.get(key);\n if (!cached) {\n authCache.set(key, auth);\n return auth;\n }\n\n if (!accessTokenExpired(auth)) {\n authCache.set(key, auth);\n return auth;\n }\n\n if (!accessTokenExpired(cached)) {\n return cached;\n }\n\n authCache.set(key, auth);\n return auth;\n}\n\n/**\n * Explicitly updates or saves authorized token details to the cache.\n */\nexport function storeCachedAuth(auth: OAuthAuthDetails): void {\n const key = normalizeRefreshKey(auth.refresh);\n if (!key) {\n return;\n }\n authCache.set(key, auth);\n}\n\n/**\n * Clears cached login authorization details. If no refresh token is provided, clears the global cache.\n */\nexport function clearCachedAuth(refresh?: string): void {\n if (!refresh) {\n authCache.clear();\n return;\n }\n const key = normalizeRefreshKey(refresh);\n if (key) {\n authCache.delete(key);\n }\n}\n\n// ============================================================================\n// Thinking signature cache layer (supports self-healing alignment of Gemini and Claude signature states in multi-turn dialogues).\n// ============================================================================\n\ninterface SignatureEntry {\n signature: string;\n timestamp: number;\n}\n\n// In-memory cache layer: sessionId -> Map\nconst signatureCache = new Map>();\n\n// Cache validity period set to 1 hour.\nconst SIGNATURE_CACHE_TTL_MS = 60 * 60 * 1000;\n\n// Maximum cache size per session to prevent memory leaks when left open for a long time.\nconst MAX_ENTRIES_PER_SESSION = 100;\n\n// Use the first 16 hex characters of the sha256 result as the textHash key width.\nconst SIGNATURE_TEXT_HASH_HEX_LEN = 16;\n\n// Disk-level persistent cache instance.\nlet diskCache: SignatureCache | null = null;\n\n/**\n * Initializes the disk-level signature storage manager.\n */\nexport function initDiskSignatureCache(config: SignatureCacheConfig | undefined): SignatureCache | null {\n diskCache = createSignatureCache(config);\n return diskCache;\n}\n\n/**\n * Calculates a stable sha256 hash of the thought chain content, taking the first 16 characters as the unique key.\n */\nfunction hashText(text: string): string {\n return createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\").slice(0, SIGNATURE_TEXT_HASH_HEX_LEN);\n}\n\n/**\n * Builds the composite primary key sessionId:textHash on disk storage.\n */\nfunction makeDiskKey(sessionId: string, textHash: string): string {\n return `${sessionId}:${textHash}`;\n}\n\n// Latest signature mapping: sessionId -> most recent signature string.\nconst latestSignatureMap = new Map();\n\n/**\n * Caches a thought chain fragment and its corresponding service signature, synchronously saving it to disk.\n */\nexport function cacheSignature(sessionId: string, text: string, signature: string): void {\n if (!sessionId || !text || !signature) return;\n\n const textHash = hashText(text);\n\n let sessionMemCache = signatureCache.get(sessionId);\n if (!sessionMemCache) {\n sessionMemCache = new Map();\n signatureCache.set(sessionId, sessionMemCache);\n }\n\n // When capacity limit is exceeded, trigger LRU cleanup of expired entries.\n if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {\n const now = Date.now();\n for (const [key, entry] of sessionMemCache.entries()) {\n if (now - entry.timestamp > SIGNATURE_CACHE_TTL_MS) {\n sessionMemCache.delete(key);\n }\n }\n // If still over the limit, discard the oldest 25% of entries by timestamp.\n if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {\n const entries = Array.from(sessionMemCache.entries())\n .sort((a, b) => a[1].timestamp - b[1].timestamp);\n const toRemove = entries.slice(0, Math.floor(MAX_ENTRIES_PER_SESSION / 4));\n for (const [key] of toRemove) {\n sessionMemCache.delete(key);\n }\n }\n }\n\n sessionMemCache.set(textHash, { signature, timestamp: Date.now() });\n latestSignatureMap.set(sessionId, signature);\n\n // If disk persistence is enabled, write to disk synchronously.\n if (diskCache) {\n const diskKey = makeDiskKey(sessionId, textHash);\n diskCache.store(diskKey, signature);\n // Also store the latest signature value directly using sessionId as the key, for global signature recovery without textHash.\n diskCache.store(sessionId, signature);\n }\n}\n\n/**\n * Recovers and retrieves the most recently cached signature for a session (supports signature recovery).\n */\nexport function getLatestSignature(sessionId: string): string | undefined {\n if (!sessionId) return undefined;\n\n // Prioritize retrieval from in-memory cache.\n const memValue = latestSignatureMap.get(sessionId);\n if (memValue) return memValue;\n\n // In-memory miss, fallback to disk query.\n if (diskCache) {\n const diskValue = diskCache.retrieve(sessionId);\n if (diskValue) {\n // Automatically promote to in-memory Map after read, accelerating subsequent queries.\n latestSignatureMap.set(sessionId, diskValue);\n return diskValue;\n }\n }\n\n return undefined;\n}\n\nexport type { SignatureCache } from \"../sdk/cache/signature-cache\";\nexport type { SignatureCacheConfig } from \"../sdk/cache/signature-cache\";\n","import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\n\n// =============================================================================\n/**\n * NOTE: Special Design - Cross-turn signature disk-level persistent cache\n * Google Agy / Gemini 2.5/3 thinking models introduce strict \"Thought Signature Validation\" restrictions:\n * In multi-turn dialogues (especially with Tool calls), the next request must carry a context signature (thoughtSignature) exactly matching the previous API response.\n * To avoid conversation crashes caused by the following:\n * 1. IDE-side session lifecycle rebuilds, causing loss of in-memory signature states.\n * 2. Concurrent packets from multi-turn Tool interactions disrupting the memory cache of signatures.\n * We implement a disk cache layer here with a background thread that periodically flushes to disk. Using a combination of session ID (sessionId) and historical thought chain hash digest as the Key,\n * it persists the signatures and thought chains. Even if the IDE restarts or turns split, it can pull back the latest matching signature to fulfill official validation constraints.\n */\n// Types & Interfaces\n// =============================================================================\n\n/**\n * Signature cache configuration options\n */\nexport interface SignatureCacheConfig {\n /** Whether to enable caching */\n enabled: boolean;\n /** In-memory cache time-to-live (seconds) */\n memory_ttl_seconds: number;\n /** Disk cache time-to-live (seconds) */\n disk_ttl_seconds: number;\n /** Auto-save interval to disk (seconds) */\n write_interval_seconds: number;\n}\n\n/**\n * Single cached data entry\n */\ninterface CacheEntry {\n /** Cached value (e.g., signature) */\n value: string;\n /** Timestamp (milliseconds) */\n timestamp: number;\n /** Full thought chain text content (optional, for self-healing recovery after compression) */\n thinkingText?: string;\n /** Preview of thought chain text (for debug logs only) */\n textPreview?: string;\n /** List of tool call IDs associated with this thought chain block */\n toolIds?: string[];\n}\n\n/**\n * Complete cache structure saved to disk\n */\ninterface CacheData {\n /** Version number */\n version: \"1.0\";\n /** Memory TTL config */\n memory_ttl_seconds: number;\n /** Disk TTL config */\n disk_ttl_seconds: number;\n /** Cache entry mapping table */\n entries: Record;\n /** Statistics */\n statistics: {\n /** Memory hit count */\n memory_hits: number;\n /** Disk hit count */\n disk_hits: number;\n /** Miss count */\n misses: number;\n /** Disk write count */\n writes: number;\n /** Last disk write timestamp */\n last_write: number;\n };\n}\n\n/**\n * Cache runtime state and statistics\n */\ninterface CacheStats {\n /** Memory hit count */\n memoryHits: number;\n /** Disk hit count */\n diskHits: number;\n /** Miss count */\n misses: number;\n /** Disk write count */\n writes: number;\n /** Total number of entries currently in memory */\n memoryEntries: number;\n /** Whether the cache is dirty (has unsaved data) */\n dirty: boolean;\n /** Whether disk storage is enabled */\n diskEnabled: boolean;\n}\n\n/**\n * Retrieve full thought chain cache data structure\n */\nexport interface ThinkingCacheData {\n /** Thought chain text */\n text: string;\n /** Signature */\n signature: string;\n /** Associated tool ID list */\n toolIds?: string[];\n}\n\n// =============================================================================\n// Path & Storage Utilities\n// =============================================================================\n\n/**\n * Get plugin config save directory\n */\nfunction getConfigDir(): string {\n const platform = process.platform;\n if (platform === \"win32\") {\n return join(process.env.APPDATA || join(homedir(), \"AppData\", \"Roaming\"), \"opencode\");\n }\n const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), \".config\");\n return join(xdgConfig, \"opencode\");\n}\n\n/**\n * Get absolute path for saving cache file\n */\nfunction getCacheFilePath(): string {\n return join(getConfigDir(), \"antigravity-signature-cache.json\");\n}\n\n/**\n * Synchronously ensure the config file is added to .gitignore to avoid leaking signatures and cache\n */\nfunction ensureGitignoreSync(configDir: string): void {\n const gitignorePath = join(configDir, \".gitignore\");\n const entries = [\".gitignore\", \"antigravity-signature-cache.json\"];\n try {\n let content = \"\";\n if (existsSync(gitignorePath)) {\n content = readFileSync(gitignorePath, \"utf-8\");\n }\n const existingLines = content.split(\"\\n\").map((line) => line.trim());\n const missing = entries.filter((e) => !existingLines.includes(e));\n if (missing.length === 0) return;\n if (content === \"\") {\n writeFileSync(gitignorePath, missing.join(\"\\n\") + \"\\n\", \"utf-8\");\n } else {\n const suffix = content.endsWith(\"\\n\") ? \"\" : \"\\n\";\n appendFileSync(gitignorePath, suffix + missing.join(\"\\n\") + \"\\n\", \"utf-8\");\n }\n } catch {\n // Ignore exception: anti-leakage is a non-blocking secondary feature\n }\n}\n\n// =============================================================================\n// Signature Cache Manager\n// =============================================================================\n\nexport class SignatureCache {\n // Memory cache map\n private cache: Map = new Map();\n\n // Configuration options\n private memoryTtlMs: number;\n private diskTtlMs: number;\n private writeIntervalMs: number;\n private cacheFilePath: string;\n private enabled: boolean;\n\n // State variables\n private dirty: boolean = false;\n private writeTimer: ReturnType | null = null;\n private cleanupTimer: ReturnType | null = null;\n\n // Statistical metrics\n private stats = {\n memoryHits: 0,\n diskHits: 0,\n misses: 0,\n writes: 0,\n };\n\n constructor(config: SignatureCacheConfig) {\n this.enabled = config.enabled;\n this.memoryTtlMs = config.memory_ttl_seconds * 1000;\n this.diskTtlMs = config.disk_ttl_seconds * 1000;\n this.writeIntervalMs = config.write_interval_seconds * 1000;\n this.cacheFilePath = getCacheFilePath();\n\n if (this.enabled) {\n this.loadFromDisk();\n this.startBackgroundTasks();\n }\n }\n\n // ===========================================================================\n // Public Signature API\n // ===========================================================================\n\n /**\n * Generates a unique cache key based on session ID and model ID\n */\n static makeKey(sessionId: string, modelId: string): string {\n return `${sessionId}:${modelId}`;\n }\n\n /**\n * Stores a signature in cache (marks as dirty, awaits background disk write)\n */\n store(key: string, signature: string): void {\n if (!this.enabled) return;\n\n this.cache.set(key, {\n value: signature,\n timestamp: Date.now(),\n });\n this.dirty = true;\n }\n\n /**\n * Retrieves a signature from cache and updates hit stats\n * Returns null if expired or missing\n */\n retrieve(key: string): string | null {\n if (!this.enabled) return null;\n\n const entry = this.cache.get(key);\n if (entry) {\n const age = Date.now() - entry.timestamp;\n if (age <= this.memoryTtlMs) {\n this.stats.memoryHits++;\n return entry.value;\n }\n // Expired in memory, delete\n this.cache.delete(key);\n }\n\n this.stats.misses++;\n return null;\n }\n\n /**\n * Checks if a key is valid and unexpired in cache (without affecting stats)\n */\n has(key: string): boolean {\n if (!this.enabled) return false;\n\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n const age = Date.now() - entry.timestamp;\n return age <= this.memoryTtlMs;\n }\n\n // ===========================================================================\n // Full Thinking Cache API\n // ===========================================================================\n\n /**\n * Caches the full thought chain text content and signature\n * Allows self-healing and recovery of historical thought blocks even if the context is subsequently compressed.\n */\n storeThinking(\n key: string,\n thinkingText: string,\n signature: string,\n toolIds?: string[],\n ): void {\n if (!this.enabled || !thinkingText || !signature) return;\n\n this.cache.set(key, {\n value: signature,\n timestamp: Date.now(),\n thinkingText,\n textPreview: thinkingText.slice(0, 100),\n toolIds,\n });\n this.dirty = true;\n }\n\n /**\n * Extracts full thought chain info from cache\n */\n retrieveThinking(key: string): ThinkingCacheData | null {\n if (!this.enabled) return null;\n\n const entry = this.cache.get(key);\n if (!entry || !entry.thinkingText) return null;\n\n const age = Date.now() - entry.timestamp;\n if (age > this.memoryTtlMs) {\n this.cache.delete(key);\n return null;\n }\n\n this.stats.memoryHits++;\n return {\n text: entry.thinkingText,\n signature: entry.value,\n toolIds: entry.toolIds,\n };\n }\n\n /**\n * Checks if full thought chain content exists for a key\n */\n hasThinking(key: string): boolean {\n if (!this.enabled) return false;\n\n const entry = this.cache.get(key);\n if (!entry || !entry.thinkingText) return false;\n\n const age = Date.now() - entry.timestamp;\n return age <= this.memoryTtlMs;\n }\n\n /**\n * Gets current cache stats and memory footprint\n */\n getStats(): CacheStats {\n return {\n ...this.stats,\n memoryEntries: this.cache.size,\n dirty: this.dirty,\n diskEnabled: this.enabled,\n };\n }\n\n /**\n * Manually triggers immediate save to disk\n */\n async flush(): Promise {\n if (!this.enabled) return true;\n return this.saveToDisk();\n }\n\n /**\n * Graceful shutdown: stops all timers and flushes unsaved data to disk\n */\n shutdown(): void {\n if (this.writeTimer) {\n clearInterval(this.writeTimer);\n this.writeTimer = null;\n }\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n\n if (this.dirty && this.enabled) {\n this.saveToDisk();\n }\n }\n\n // ===========================================================================\n // Disk Operations\n // ===========================================================================\n\n /**\n * Loads signature cache from disk and validates TTL state\n */\n private loadFromDisk(): void {\n try {\n if (!existsSync(this.cacheFilePath)) {\n return;\n }\n\n const content = readFileSync(this.cacheFilePath, \"utf-8\");\n const data = JSON.parse(content) as CacheData;\n\n if (data.version !== \"1.0\") {\n // On version mismatch, silently ignore and start a fresh cache\n return;\n }\n\n const now = Date.now();\n for (const [key, entry] of Object.entries(data.entries)) {\n const age = now - entry.timestamp;\n if (age <= this.diskTtlMs) {\n this.cache.set(key, {\n value: entry.value,\n timestamp: entry.timestamp,\n thinkingText: entry.thinkingText,\n textPreview: entry.textPreview,\n toolIds: entry.toolIds,\n });\n }\n }\n } catch {\n // Fault tolerance: silently start fresh memory cache on disk load failure\n }\n }\n\n /**\n * Synchronously saves memory cache to disk (using atomic write: temp file then rename)\n * Merges with existing unexpired entries on disk during write\n */\n private saveToDisk(): boolean {\n try {\n const dir = dirname(this.cacheFilePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n ensureGitignoreSync(dir);\n\n const now = Date.now();\n\n // Step 1: Read existing disk entries\n let existingEntries: Record = {};\n if (existsSync(this.cacheFilePath)) {\n try {\n const content = readFileSync(this.cacheFilePath, \"utf-8\");\n const data = JSON.parse(content) as CacheData;\n existingEntries = data.entries || {};\n } catch {\n // Fault tolerance: overwrite if malformed or corrupted\n }\n }\n\n // Step 2: Filter expired disk entries\n const validDiskEntries: Record = {};\n for (const [key, entry] of Object.entries(existingEntries)) {\n const age = now - entry.timestamp;\n if (age <= this.diskTtlMs) {\n validDiskEntries[key] = entry;\n }\n }\n\n // Step 3: Overwrite or merge memory cache into valid disk entries (memory data is newest, highest priority)\n const mergedEntries: Record = { ...validDiskEntries };\n for (const [key, entry] of this.cache.entries()) {\n mergedEntries[key] = {\n value: entry.value,\n timestamp: entry.timestamp,\n thinkingText: entry.thinkingText,\n textPreview: entry.textPreview,\n toolIds: entry.toolIds,\n };\n }\n\n // Step 4: Build persistent structure\n const cacheData: CacheData = {\n version: \"1.0\",\n memory_ttl_seconds: this.memoryTtlMs / 1000,\n disk_ttl_seconds: this.diskTtlMs / 1000,\n entries: mergedEntries,\n statistics: {\n memory_hits: this.stats.memoryHits,\n disk_hits: this.stats.diskHits,\n misses: this.stats.misses,\n writes: this.stats.writes + 1,\n last_write: now,\n },\n };\n\n // Step 5: Atomic write (write to temp file, rename to finalize, preventing data corruption)\n const tmpPath = join(tmpdir(), `antigravity-cache-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);\n writeFileSync(tmpPath, JSON.stringify(cacheData, null, 2), \"utf-8\");\n\n try {\n renameSync(tmpPath, this.cacheFilePath);\n } catch {\n // Cross-volume rename on Windows might fail, fallback to copy + delete\n writeFileSync(this.cacheFilePath, readFileSync(tmpPath));\n try {\n unlinkSync(tmpPath);\n } catch {\n // Ignore temp file deletion failure\n }\n }\n\n this.stats.writes++;\n this.dirty = false;\n return true;\n } catch {\n // Disk writing is non-core, fail silently\n return false;\n }\n }\n\n // ===========================================================================\n // Background Tasks\n // ===========================================================================\n\n /**\n * Starts timers for auto-saving and auto-cleaning expired memory entries\n */\n private startBackgroundTasks(): void {\n // Periodic disk write (if modified)\n this.writeTimer = setInterval(() => {\n if (this.dirty) {\n this.saveToDisk();\n }\n }, this.writeIntervalMs);\n\n // Perform memory garbage collection every 30 mins to free unused space\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired();\n }, 30 * 60 * 1000);\n }\n\n /**\n * Removes memory cache entries exceeding their TTL\n */\n private cleanupExpired(): void {\n const now = Date.now();\n for (const [key, entry] of this.cache.entries()) {\n const age = now - entry.timestamp;\n if (age > this.memoryTtlMs) {\n this.cache.delete(key);\n }\n }\n }\n}\n\n// =============================================================================\n// Factory function\n// =============================================================================\n\n/**\n * Instantiates signature cache object based on config. Returns null if disabled.\n */\nexport function createSignatureCache(config: SignatureCacheConfig | undefined): SignatureCache | null {\n if (!config || !config.enabled) {\n return null;\n }\n return new SignatureCache(config);\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { analyzeConversationState, type ConversationState } from \"./thinking\";\n\nexport type TurnState = Pick;\n\ninterface TurnStateRecord {\n state: TurnState;\n updatedAt: number;\n}\n\ninterface TurnStateData {\n version: \"1.0\";\n entries: Record;\n updatedAt: number;\n}\n\nconst WRITE_THROTTLE_MS = 5000;\n\nfunction getConfigDir(): string {\n const platform = process.platform;\n if (platform === \"win32\") {\n return join(process.env.APPDATA || join(homedir(), \"AppData\", \"Roaming\"), \"opencode\");\n }\n const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), \".config\");\n return join(xdgConfig, \"opencode\");\n}\n\nfunction getTurnStateFilePath(): string {\n return join(getConfigDir(), \"antigravity-turn-states.json\");\n}\n\nfunction loadTurnStatesFromDisk(): Map {\n const result = new Map();\n try {\n const filePath = getTurnStateFilePath();\n if (!existsSync(filePath)) {\n return result;\n }\n\n const content = readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(content) as TurnStateData;\n if (data.version !== \"1.0\") {\n return result;\n }\n\n const now = Date.now();\n const maxAge = 24 * 60 * 60 * 1000;\n for (const [key, record] of Object.entries(data.entries)) {\n if (record.state && typeof record.state === \"object\" && now - record.updatedAt < maxAge) {\n result.set(key, record);\n }\n }\n } catch {\n }\n return result;\n}\n\nfunction saveTurnStatesToDisk(entries: Map): boolean {\n try {\n const filePath = getTurnStateFilePath();\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const now = Date.now();\n const maxAge = 24 * 60 * 60 * 1000;\n const serializable: Record = {};\n for (const [key, record] of entries.entries()) {\n if (now - record.updatedAt < maxAge) {\n serializable[key] = record;\n }\n }\n\n const data: TurnStateData = {\n version: \"1.0\",\n entries: serializable,\n updatedAt: now,\n };\n\n const tmpPath = join(tmpdir(), `antigravity-turn-states-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);\n writeFileSync(tmpPath, JSON.stringify(data), \"utf-8\");\n\n try {\n renameSync(tmpPath, filePath);\n } catch {\n writeFileSync(filePath, readFileSync(tmpPath));\n try {\n unlinkSync(tmpPath);\n } catch {\n }\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport class TurnStateTracker {\n private entries = new Map();\n private dirty = false;\n private lastWriteTime = 0;\n private writeTimer: ReturnType | null = null;\n private readonly diskEnabled: boolean;\n\n constructor(diskEnabled = true) {\n this.diskEnabled = diskEnabled;\n if (diskEnabled) {\n this.entries = loadTurnStatesFromDisk();\n }\n }\n\n getState(sessionId: string): TurnState | undefined {\n const record = this.entries.get(sessionId);\n if (!record) return undefined;\n return record.state;\n }\n\n needsThinkingRecovery(sessionId: string): boolean {\n const state = this.entries.get(sessionId);\n if (!state) return false;\n return state.state.inToolLoop && !state.state.turnHasThinking;\n }\n\n updateAfterResponse(sessionId: string, newState: TurnState): void {\n this.entries.set(sessionId, { state: newState, updatedAt: Date.now() });\n this.dirty = true;\n this.scheduleThrottledWrite();\n }\n\n recoverFromContents(sessionId: string, contents: any[]): TurnState {\n const fullState = analyzeConversationState(contents);\n const turnState: TurnState = {\n inToolLoop: fullState.inToolLoop,\n turnHasThinking: fullState.turnHasThinking,\n lastModelHasThinking: fullState.lastModelHasThinking,\n lastModelHasToolCalls: fullState.lastModelHasToolCalls,\n };\n this.entries.set(sessionId, { state: turnState, updatedAt: Date.now() });\n this.dirty = true;\n this.scheduleThrottledWrite();\n return turnState;\n }\n\n clear(sessionId: string): void {\n this.entries.delete(sessionId);\n this.dirty = true;\n this.scheduleThrottledWrite();\n }\n\n shutdown(): void {\n this.clearWriteTimer();\n if (this.dirty && this.diskEnabled) {\n saveTurnStatesToDisk(this.entries);\n this.dirty = false;\n }\n }\n\n private scheduleThrottledWrite(): void {\n if (!this.diskEnabled) return;\n if (this.writeTimer) {\n return;\n }\n\n const elapsed = Date.now() - this.lastWriteTime;\n const remaining = Math.max(0, WRITE_THROTTLE_MS - elapsed);\n\n this.writeTimer = setTimeout(() => {\n this.writeTimer = null;\n this.lastWriteTime = Date.now();\n if (this.dirty) {\n this.dirty = false;\n saveTurnStatesToDisk(this.entries);\n }\n }, remaining);\n\n if (this.writeTimer && typeof this.writeTimer === \"object\" && \"unref\" in this.writeTimer) {\n this.writeTimer.unref();\n }\n }\n\n private clearWriteTimer(): void {\n if (this.writeTimer) {\n clearTimeout(this.writeTimer);\n this.writeTimer = null;\n }\n }\n}\n\nlet trackerInstance: TurnStateTracker | null = null;\n\nexport function initTurnStateTracker(): TurnStateTracker {\n if (!trackerInstance) {\n try {\n trackerInstance = new TurnStateTracker(true);\n if (typeof process !== \"undefined\") {\n process.on(\"exit\", () => {\n trackerInstance?.shutdown();\n });\n }\n } catch {\n trackerInstance = new TurnStateTracker(false);\n }\n }\n return trackerInstance;\n}\n\nexport function getTurnStateTracker(): TurnStateTracker | null {\n return trackerInstance;\n}\n\nexport function shutdownTurnStateTracker(): void {\n if (trackerInstance) {\n trackerInstance.shutdown();\n trackerInstance = null;\n }\n}\n","import { createHash } from \"node:crypto\";\n\n/**\n * NOTE: Special Design - Streaming deduplication and signature state self-healing\n * Agy/Gemini official API has the following non-standard behaviors and strict constraints that must be specially handled here:\n * 1. [Streaming Deduplication]: In the data packets returned by the official API during streaming, thought chain (Thinking) content is output cumulatively.\n * We must perform hash comparison and truncation on each returned delta to prevent the IDE from receiving duplicate text.\n * 2. [Signature State Self-healing]: In multi-turn dialogues, if the thoughtSignature is lost due to tool calls or client state breakage,\n * the official API throws a signature mismatch error. An auto-detection mechanism (e.g., needsThinkingRecovery) is designed here to, upon signature breakage,\n * automatically backfill/align fallback thought chain fragments and signatures in context messages, allowing the dialogue chain to self-heal and continue.\n */\n\n// ============================================================================\n// Types & Interfaces\n// ============================================================================\n\n/**\n * Cached signed thought chain data structure\n */\nexport interface SignedThinking {\n /** Full thought chain text content */\n text: string;\n /** Corresponding server signature */\n signature: string;\n}\n\n/**\n * External contract interface for signature storage manager\n */\nexport interface SignatureStore {\n get(sessionKey: string): SignedThinking | undefined;\n set(sessionKey: string, value: SignedThinking): void;\n has(sessionKey: string): boolean;\n delete(sessionKey: string): void;\n}\n\n/**\n * Custom callback functions for the streaming phase\n */\nexport interface StreamingCallbacks {\n onCacheSignature?: (sessionKey: string, text: string, signature: string) => void;\n onInjectDebug?: (response: unknown, debugText: string) => unknown;\n transformThinkingParts?: (parts: unknown) => unknown;\n onTurnStateUpdate?: (sessionKey: string, state: { turnHasThinking: boolean; lastModelHasToolCalls: boolean }) => void;\n}\n\n/**\n * Configuration parameters for the streaming phase\n */\nexport interface StreamingOptions {\n /** Unique identifier for the signature session, used for cross-turn signature recovery */\n signatureSessionKey?: string;\n /** Debugging text to inject into the stream (optional) */\n debugText?: string;\n /** Whether to cache the latest generated signature in this stream */\n cacheSignatures?: boolean;\n /** Set of already rendered thought chain hashes, used to avoid duplicate output in tool call loops */\n displayedThinkingHashes?: Set;\n}\n\n/**\n * Text buffer for caching thought chains of a specific index or type (handles streaming chunk cumulative output)\n */\nexport interface ThoughtBuffer {\n get(index: number): string | undefined;\n set(index: number, text: string): void;\n clear(): void;\n}\n\n/**\n * Agent and tool interaction state record during multi-turn dialogue runtime\n */\nexport interface ConversationState {\n /** Whether inside an incomplete tool call loop (i.e., last turn ended with functionResponse, continuing this turn) */\n inToolLoop: boolean;\n /** Array index of the first model reply message in the current dialogue turn */\n turnStartIdx: number;\n /** Whether the start of the current dialogue turn contains a thought chain (thought) */\n turnHasThinking: boolean;\n /** Array index of the last model reply message */\n lastModelIdx: number;\n /** Whether the last model message contains a thought chain */\n lastModelHasThinking: boolean;\n /** Whether the last model message contains a tool call (tool_use) */\n lastModelHasToolCalls: boolean;\n}\n\n// ============================================================================\n// Stores & Buffers Factories\n// ============================================================================\n\n/**\n * Creates a memory Map-based signature storage manager\n */\nexport function createSignatureStore(): SignatureStore {\n const store = new Map();\n\n return {\n get: (key: string) => store.get(key),\n set: (key: string, value: SignedThinking) => {\n store.set(key, value);\n },\n has: (key: string) => store.has(key),\n delete: (key: string) => {\n store.delete(key);\n },\n };\n}\n\n/**\n * Creates a thought chain text accumulation buffer for temporarily storing streaming chunks\n */\nexport function createThoughtBuffer(): ThoughtBuffer {\n const buffer = new Map();\n\n return {\n get: (index: number) => buffer.get(index),\n set: (index: number, text: string) => {\n buffer.set(index, text);\n },\n clear: () => buffer.clear(),\n };\n}\n\n/**\n * Default global memory signature storage\n */\nexport const defaultSignatureStore = createSignatureStore();\n\n// ============================================================================\n// Hashing Helper\n// ============================================================================\n\nconst THINKING_HASH_HEX_LEN = 16;\n\n/**\n * Generates a SHA-256 hash of the thought chain content, truncated to 16 hex characters.\n * Consistent with the signature cache hash width in cache.ts.\n */\nfunction hashString(str: string): string {\n return createHash(\"sha256\").update(str, \"utf8\").digest(\"hex\").slice(0, THINKING_HASH_HEX_LEN);\n}\n\n// ============================================================================\n// Thinking State Detection & Historical Self-healing Helpers\n// ============================================================================\n\n/**\n * Checks if a message part belongs to the thought chain type\n * Compatible with Gemini (thought field) and Claude (type: \"thinking\", etc.)\n */\nfunction isThinkingPart(part: any): boolean {\n if (!part || typeof part !== \"object\") return false;\n return (\n part.thought === true ||\n part.type === \"thinking\" ||\n part.type === \"redacted_thinking\"\n );\n}\n\n/**\n * Checks if a message part is a tool execution result response\n */\nfunction isFunctionResponsePart(part: any): boolean {\n return part && typeof part === \"object\" && \"functionResponse\" in part;\n}\n\n/**\n * Checks if a message part is a model-initiated tool call request\n */\nfunction isFunctionCallPart(part: any): boolean {\n return part && typeof part === \"object\" && \"functionCall\" in part;\n}\n\n/**\n * Checks if a message is exclusively a tool execution return body (role = user but entirely functionResponse)\n */\nfunction isToolResultMessage(msg: any): boolean {\n if (!msg || msg.role !== \"user\") return false;\n const parts = msg.parts || [];\n return parts.some(isFunctionResponsePart);\n}\n\n/**\n * Checks if a message contains any thought chain segment (including Gemini array format or Claude content format)\n */\nfunction messageHasThinking(msg: any): boolean {\n if (!msg || typeof msg !== \"object\") return false;\n\n if (Array.isArray(msg.parts)) {\n return msg.parts.some(isThinkingPart);\n }\n\n if (Array.isArray(msg.content)) {\n return msg.content.some(\n (block: any) =>\n block?.type === \"thinking\" || block?.type === \"redacted_thinking\",\n );\n }\n\n return false;\n}\n\n/**\n * Checks if a message contains a tool call (supports Gemini and Claude tool_use)\n */\nfunction messageHasToolCalls(msg: any): boolean {\n if (!msg || typeof msg !== \"object\") return false;\n\n if (Array.isArray(msg.parts)) {\n return msg.parts.some(isFunctionCallPart);\n }\n\n if (Array.isArray(msg.content)) {\n return msg.content.some((block: any) => block?.type === \"tool_use\");\n }\n\n return false;\n}\n\n/**\n * Analyzes multi-turn historical dialogue arrays to extract context metrics like agent interaction loop state, model message positions, whether the turn has thoughts, etc.\n */\nexport function analyzeConversationState(contents: any[]): ConversationState {\n const state: ConversationState = {\n inToolLoop: false,\n turnStartIdx: -1,\n turnHasThinking: false,\n lastModelIdx: -1,\n lastModelHasThinking: false,\n lastModelHasToolCalls: false,\n };\n\n if (!Array.isArray(contents) || contents.length === 0) {\n return state;\n }\n\n // Find the position of the last real human user question\n let lastRealUserIdx = -1;\n for (let i = 0; i < contents.length; i++) {\n const msg = contents[i];\n if (msg?.role === \"user\" && !isToolResultMessage(msg)) {\n lastRealUserIdx = i;\n }\n }\n\n // Scan all model messages to extract features of the most recent reply round\n for (let i = 0; i < contents.length; i++) {\n const msg = contents[i];\n const role = msg?.role;\n\n if (role === \"model\" || role === \"assistant\") {\n const hasThinking = messageHasThinking(msg);\n const hasToolCalls = messageHasToolCalls(msg);\n\n // If this is the first model message after the most recent user question, treat as the start of the current turn\n if (i > lastRealUserIdx && state.turnStartIdx === -1) {\n state.turnStartIdx = i;\n state.turnHasThinking = hasThinking;\n }\n\n state.lastModelIdx = i;\n state.lastModelHasToolCalls = hasToolCalls;\n state.lastModelHasThinking = hasThinking;\n }\n }\n\n // Determine if in the tail of an incomplete tool loop (i.e., user just returned tool call results, waiting for model's next step)\n if (contents.length > 0) {\n const lastMsg = contents[contents.length - 1];\n if (lastMsg?.role === \"user\" && isToolResultMessage(lastMsg)) {\n state.inToolLoop = true;\n }\n }\n\n return state;\n}\n\n/**\n * Calculate the number of uncompleted tool responses at the tail\n */\nfunction countTrailingToolResults(contents: any[]): number {\n let count = 0;\n\n for (let i = contents.length - 1; i >= 0; i--) {\n const msg = contents[i];\n\n if (msg?.role === \"user\") {\n const parts = msg.parts || [];\n const functionResponses = parts.filter(isFunctionResponsePart);\n\n if (functionResponses.length > 0) {\n count += functionResponses.length;\n } else {\n break;\n }\n } else if (msg?.role === \"model\" || msg?.role === \"assistant\") {\n break;\n }\n }\n\n return count;\n}\n\n/**\n * Closes the tool execution loop and injects transition content to smoothly recover the dialogue without providing the old thought chain\n */\nexport function closeToolLoopForThinking(contents: any[]): any[] {\n const strippedContents = contents;\n const toolResultCount = countTrailingToolResults(strippedContents);\n let syntheticModelContent: string;\n\n if (toolResultCount === 0) {\n syntheticModelContent = \"[Processing prev ctx.]\";\n } else if (toolResultCount === 1) {\n syntheticModelContent = \"[Tool exec completed.]\";\n } else {\n syntheticModelContent = `[${toolResultCount} tool executions completed.]`;\n }\n\n const syntheticModel = {\n role: \"model\",\n parts: [{ text: syntheticModelContent }],\n };\n\n const syntheticUser = {\n role: \"user\",\n parts: [{ text: \"[Continue]\" }],\n };\n\n return [...strippedContents, syntheticModel, syntheticUser];\n}\n\n/**\n * Checks if the current state meets conditions to trigger historical self-healing\n */\nexport function needsThinkingRecovery(state: ConversationState): boolean {\n return state.inToolLoop && !state.turnHasThinking;\n}\n\n/**\n * Determines if the current model reply message had its thought chain pruned (has only tool calls but lost its preceding thought chain description)\n */\nexport function looksLikeCompactedThinkingTurn(msg: any): boolean {\n if (!msg || typeof msg !== \"object\") return false;\n\n const parts = msg.parts || [];\n if (parts.length === 0) return false;\n\n const hasFunctionCall = parts.some(\n (p: any) => p && typeof p === \"object\" && p.functionCall,\n );\n\n if (!hasFunctionCall) return false;\n\n const hasThinking = parts.some(\n (p: any) =>\n p &&\n typeof p === \"object\" &&\n (p.thought === true || p.type === \"thinking\" || p.type === \"redacted_thinking\"),\n );\n\n if (hasThinking) return false;\n\n const hasTextBeforeFunctionCall = parts.some((p: any, idx: number) => {\n if (!p || typeof p !== \"object\") return false;\n const firstFuncIdx = parts.findIndex(\n (fp: any) => fp && typeof fp === \"object\" && fp.functionCall,\n );\n if (idx >= firstFuncIdx) return false;\n return (\n \"text\" in p &&\n typeof p.text === \"string\" &&\n p.text.trim().length > 0 &&\n !p.thought\n );\n });\n\n return !hasTextBeforeFunctionCall;\n}\n\n/**\n * Deeply determines if the start of this Turn contains historical rounds whose thought chains might have been pruned/compressed by the system\n */\nexport function hasPossibleCompactedThinking(\n contents: any[],\n turnStartIdx: number,\n): boolean {\n if (!Array.isArray(contents) || turnStartIdx < 0) return false;\n\n for (let i = turnStartIdx; i < contents.length; i++) {\n const msg = contents[i];\n if (msg?.role === \"model\" && looksLikeCompactedThinkingTurn(msg)) {\n return true;\n }\n }\n\n return false;\n}\n\n// ============================================================================\n// SSE Deduplication Filter Processors (Streaming Transformers & Deduplicators)\n// ============================================================================\n\n/**\n * For streaming SSE data packets, calculates and locally strips duplicated thought chain text\n * Simultaneously supports Gemini's exclusive candidates.content structure and Claude's exclusive content[type=thinking] structure\n */\nexport function deduplicateThinkingText(\n response: unknown,\n sentBuffer: ThoughtBuffer,\n displayedThinkingHashes?: Set,\n): unknown {\n if (!response || typeof response !== \"object\") return response;\n\n const resp = response as Record;\n\n // Branch 1: Handle Gemini structure type (candidates)\n if (Array.isArray(resp.candidates)) {\n const newCandidates = resp.candidates.map((candidate: unknown, index: number) => {\n const cand = candidate as Record | null;\n if (!cand?.content) return candidate;\n\n const content = cand.content as Record;\n if (!Array.isArray(content.parts)) return candidate;\n\n const newParts = content.parts.map((part: unknown) => {\n const p = part as Record;\n\n if (p.thought === true || p.type === \"thinking\") {\n const fullText = (p.text || p.thinking || \"\") as string;\n\n // If this thought chain was already rendered, block it directly from the stream to prevent long overlapping rendering in tool call branches\n if (displayedThinkingHashes) {\n const hash = hashString(fullText);\n if (displayedThinkingHashes.has(hash)) {\n sentBuffer.set(index, fullText);\n return null;\n }\n displayedThinkingHashes.add(hash);\n }\n\n // Calculate incremental output (Delta)\n const sentText = sentBuffer.get(index) ?? \"\";\n\n if (fullText.startsWith(sentText)) {\n const delta = fullText.slice(sentText.length);\n sentBuffer.set(index, fullText);\n\n if (delta) {\n return { ...p, text: delta, thinking: delta };\n }\n return null;\n }\n\n sentBuffer.set(index, fullText);\n return part;\n }\n return part;\n });\n\n const filteredParts = newParts.filter((p) => p !== null);\n\n return {\n ...cand,\n content: { ...content, parts: filteredParts },\n };\n });\n\n return { ...resp, candidates: newCandidates };\n }\n\n // Branch 2: Handle Claude structure type (content blocks) because Agy backend forwarding includes Claude\n if (Array.isArray(resp.content)) {\n let thinkingIndex = 0;\n const newContent = resp.content.map((block: unknown) => {\n const b = block as Record | null;\n if (b?.type === \"thinking\") {\n const fullText = (b.thinking || b.text || \"\") as string;\n\n if (displayedThinkingHashes) {\n const hash = hashString(fullText);\n if (displayedThinkingHashes.has(hash)) {\n sentBuffer.set(thinkingIndex, fullText);\n thinkingIndex++;\n return null;\n }\n displayedThinkingHashes.add(hash);\n }\n\n const sentText = sentBuffer.get(thinkingIndex) ?? \"\";\n\n if (fullText.startsWith(sentText)) {\n const delta = fullText.slice(sentText.length);\n sentBuffer.set(thinkingIndex, fullText);\n thinkingIndex++;\n\n if (delta) {\n return { ...b, thinking: delta, text: delta };\n }\n return null;\n }\n\n sentBuffer.set(thinkingIndex, fullText);\n thinkingIndex++;\n return block;\n }\n return block;\n });\n\n const filteredContent = newContent.filter((b) => b !== null);\n if (filteredContent.length === 0) {\n return { ...resp, content: [] };\n }\n return { ...resp, content: filteredContent };\n }\n\n return response;\n}\n\n/**\n * Caches thought chain content and its validation signature from the returned message body for signature alignment in the next interaction round\n * Also supports Gemini signature mechanism (candidates[].thoughtSignature) and Claude signature mechanism (content[].signature)\n */\nexport function cacheThinkingSignaturesFromResponse(\n response: unknown,\n signatureSessionKey: string,\n signatureStore: SignatureStore,\n thoughtBuffer: ThoughtBuffer,\n onCacheSignature?: (sessionKey: string, text: string, signature: string) => void,\n): void {\n if (!response || typeof response !== \"object\") return;\n\n const resp = response as Record;\n\n // Branch 1: Collect and cache Gemini type signatures\n if (Array.isArray(resp.candidates)) {\n resp.candidates.forEach((candidate: unknown, index: number) => {\n const cand = candidate as Record | null;\n if (!cand?.content) return;\n const content = cand.content as Record;\n if (!Array.isArray(content.parts)) return;\n\n content.parts.forEach((part: unknown) => {\n const p = part as Record;\n if (p.thought === true || p.type === \"thinking\") {\n const text = (p.text || p.thinking || \"\") as string;\n if (text) {\n const current = thoughtBuffer.get(index) ?? \"\";\n thoughtBuffer.set(index, current + text);\n }\n }\n\n if (p.thoughtSignature) {\n const fullText = thoughtBuffer.get(index) ?? \"\";\n if (fullText) {\n const signature = p.thoughtSignature as string;\n onCacheSignature?.(signatureSessionKey, fullText, signature);\n signatureStore.set(signatureSessionKey, { text: fullText, signature });\n }\n }\n });\n });\n }\n\n // Branch 2: Collect and cache Claude type signatures\n if (Array.isArray(resp.content)) {\n const CLAUDE_BUFFER_KEY = 0;\n resp.content.forEach((block: unknown) => {\n const b = block as Record | null;\n if (b?.type === \"thinking\") {\n const text = (b.thinking || b.text || \"\") as string;\n if (text) {\n const current = thoughtBuffer.get(CLAUDE_BUFFER_KEY) ?? \"\";\n thoughtBuffer.set(CLAUDE_BUFFER_KEY, current + text);\n }\n }\n if (b?.signature) {\n const fullText = thoughtBuffer.get(CLAUDE_BUFFER_KEY) ?? \"\";\n if (fullText) {\n const signature = b.signature as string;\n onCacheSignature?.(signatureSessionKey, fullText, signature);\n signatureStore.set(signatureSessionKey, { text: fullText, signature });\n }\n }\n });\n }\n}\n\n/**\n * Transforms a single complete SSE event, triggering thought chain caching and incremental deduplication here\n */\nexport function transformSseEvent(\n eventText: string,\n signatureStore: SignatureStore,\n thoughtBuffer: ThoughtBuffer,\n sentThinkingBuffer: ThoughtBuffer,\n callbacks: StreamingCallbacks,\n options: StreamingOptions,\n debugState: { injected: boolean },\n): string {\n // Extract all data from data: lines\n const dataLines: string[] = [];\n const lines = eventText.split(/\\r?\\n/);\n let isDataEvent = false;\n\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n isDataEvent = true;\n dataLines.push(line.slice(5).trim());\n }\n }\n\n if (!isDataEvent) {\n return eventText;\n }\n\n const jsonString = dataLines.join(\"\\n\").trim();\n if (!jsonString) {\n return eventText;\n }\n\n try {\n const parsed = JSON.parse(jsonString) as Record | null;\n if (parsed && typeof parsed === \"object\" && parsed.response !== undefined) {\n // Extract and write to cache\n if (options.cacheSignatures && options.signatureSessionKey) {\n cacheThinkingSignaturesFromResponse(\n parsed.response,\n options.signatureSessionKey,\n signatureStore,\n thoughtBuffer,\n callbacks.onCacheSignature,\n );\n }\n\n // Calculate deduplication\n let response: unknown = deduplicateThinkingText(\n parsed.response,\n sentThinkingBuffer,\n options.displayedThinkingHashes\n );\n\n // Debug text injection\n if (options.debugText && callbacks.onInjectDebug && !debugState.injected) {\n response = callbacks.onInjectDebug(response, options.debugText);\n debugState.injected = true;\n }\n\n const transformed = callbacks.transformThinkingParts\n ? callbacks.transformThinkingParts(response)\n : response;\n return `data: ${JSON.stringify(transformed)}`;\n }\n } catch (_) {}\n return eventText;\n}\n\n/**\n * Creates a TransformStream processor to split, deduplicate, and recombine the output stream\n */\nexport function createStreamingTransformer(\n signatureStore: SignatureStore,\n callbacks: StreamingCallbacks,\n options: StreamingOptions = {},\n): TransformStream {\n const decoder = new TextDecoder();\n const encoder = new TextEncoder();\n let buffer = \"\";\n const thoughtBuffer = createThoughtBuffer();\n const sentThinkingBuffer = createThoughtBuffer();\n const debugState = { injected: false };\n let hasSeenUsageMetadata = false;\n let streamHasThinking = false;\n let streamHasToolCalls = false;\n\n const displayedThinkingHashes = options.displayedThinkingHashes ?? new Set();\n const mergedOptions = { ...options, displayedThinkingHashes };\n\n return new TransformStream({\n transform(chunk, controller) {\n buffer += decoder.decode(chunk, { stream: true });\n\n const events = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = events.pop() || \"\";\n\n for (const event of events) {\n if (!event.trim()) continue; // Skip empty events if any\n if (event.includes(\"usageMetadata\")) {\n hasSeenUsageMetadata = true;\n }\n if (!streamHasThinking) {\n streamHasThinking = event.includes('\"thought\":true') || event.includes('\"type\":\"thinking\"');\n }\n if (!streamHasToolCalls) {\n streamHasToolCalls = event.includes('\"functionCall\"');\n }\n\n const transformedEvent = transformSseEvent(\n event,\n signatureStore,\n thoughtBuffer,\n sentThinkingBuffer,\n callbacks,\n mergedOptions,\n debugState,\n );\n controller.enqueue(encoder.encode(transformedEvent + \"\\n\\n\"));\n }\n },\n flush(controller) {\n buffer += decoder.decode();\n\n if (buffer.trim()) {\n if (buffer.includes(\"usageMetadata\")) {\n hasSeenUsageMetadata = true;\n }\n if (!streamHasThinking) {\n streamHasThinking = buffer.includes('\"thought\":true') || buffer.includes('\"type\":\"thinking\"');\n }\n if (!streamHasToolCalls) {\n streamHasToolCalls = buffer.includes('\"functionCall\"');\n }\n const transformedEvent = transformSseEvent(\n buffer,\n signatureStore,\n thoughtBuffer,\n sentThinkingBuffer,\n callbacks,\n mergedOptions,\n debugState,\n );\n controller.enqueue(encoder.encode(transformedEvent + \"\\n\\n\"));\n }\n\n // Fallback strategy: If no token usage metadata was generated at the end, forcibly inject a 0-count fallback to ensure VS Code statistics compatibility\n if (!hasSeenUsageMetadata) {\n const syntheticUsage = {\n candidates: [\n {\n finishReason: \"STOP\",\n },\n ],\n usageMetadata: {\n promptTokenCount: 0,\n candidatesTokenCount: 0,\n totalTokenCount: 0,\n },\n };\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(syntheticUsage)}\\n\\n`));\n }\n\n if (callbacks.onTurnStateUpdate && options.signatureSessionKey) {\n callbacks.onTurnStateUpdate(options.signatureSessionKey, {\n turnHasThinking: streamHasThinking,\n lastModelHasToolCalls: streamHasToolCalls,\n });\n }\n },\n });\n}\n","export * as core from \"../core/index.js\";\nexport * from \"./schemas.js\";\nexport * from \"./checks.js\";\nexport * from \"./errors.js\";\nexport * from \"./parse.js\";\nexport * from \"./compat.js\";\n// zod-specified\nimport { config } from \"../core/index.js\";\nimport en from \"../locales/en.js\";\nconfig(en());\nexport { globalRegistry, registry, config, $output, $input, $brand, clone, regexes, treeifyError, prettifyError, formatError, flattenError, toJSONSchema, TimePrecision, util, NEVER, } from \"../core/index.js\";\nexport * as locales from \"../locales/index.js\";\n// iso\n// must be exported from top-level\n// https://github.com/colinhacks/zod/issues/4491\nexport { ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration } from \"./iso.js\";\nexport * as iso from \"./iso.js\";\nexport * as coerce from \"./coerce.js\";\n","export * from \"./core.js\";\nexport * from \"./parse.js\";\nexport * from \"./errors.js\";\nexport * from \"./schemas.js\";\nexport * from \"./checks.js\";\nexport * from \"./versions.js\";\nexport * as util from \"./util.js\";\nexport * as regexes from \"./regexes.js\";\nexport * as locales from \"../locales/index.js\";\nexport * from \"./registries.js\";\nexport * from \"./doc.js\";\nexport * from \"./api.js\";\nexport * from \"./to-json-schema.js\";\nexport * as JSONSchema from \"./json-schema.js\";\n","/** A special constant with type `never` */\nexport const NEVER = Object.freeze({\n status: \"aborted\",\n});\nexport /*@__NO_SIDE_EFFECTS__*/ function $constructor(name, initializer, params) {\n function init(inst, def) {\n var _a;\n Object.defineProperty(inst, \"_zod\", {\n value: inst._zod ?? {},\n enumerable: false,\n });\n (_a = inst._zod).traits ?? (_a.traits = new Set());\n inst._zod.traits.add(name);\n initializer(inst, def);\n // support prototype modifications\n for (const k in _.prototype) {\n if (!(k in inst))\n Object.defineProperty(inst, k, { value: _.prototype[k].bind(inst) });\n }\n inst._zod.constr = _;\n inst._zod.def = def;\n }\n // doesn't work if Parent has a constructor with arguments\n const Parent = params?.Parent ?? Object;\n class Definition extends Parent {\n }\n Object.defineProperty(Definition, \"name\", { value: name });\n function _(def) {\n var _a;\n const inst = params?.Parent ? new Definition() : this;\n init(inst, def);\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n for (const fn of inst._zod.deferred) {\n fn();\n }\n return inst;\n }\n Object.defineProperty(_, \"init\", { value: init });\n Object.defineProperty(_, Symbol.hasInstance, {\n value: (inst) => {\n if (params?.Parent && inst instanceof params.Parent)\n return true;\n return inst?._zod?.traits?.has(name);\n },\n });\n Object.defineProperty(_, \"name\", { value: name });\n return _;\n}\n////////////////////////////// UTILITIES ///////////////////////////////////////\nexport const $brand = Symbol(\"zod_brand\");\nexport class $ZodAsyncError extends Error {\n constructor() {\n super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);\n }\n}\nexport class $ZodEncodeError extends Error {\n constructor(name) {\n super(`Encountered unidirectional transform during encode: ${name}`);\n this.name = \"ZodEncodeError\";\n }\n}\nexport const globalConfig = {};\nexport function config(newConfig) {\n if (newConfig)\n Object.assign(globalConfig, newConfig);\n return globalConfig;\n}\n","// functions\nexport function assertEqual(val) {\n return val;\n}\nexport function assertNotEqual(val) {\n return val;\n}\nexport function assertIs(_arg) { }\nexport function assertNever(_x) {\n throw new Error();\n}\nexport function assert(_) { }\nexport function getEnumValues(entries) {\n const numericValues = Object.values(entries).filter((v) => typeof v === \"number\");\n const values = Object.entries(entries)\n .filter(([k, _]) => numericValues.indexOf(+k) === -1)\n .map(([_, v]) => v);\n return values;\n}\nexport function joinValues(array, separator = \"|\") {\n return array.map((val) => stringifyPrimitive(val)).join(separator);\n}\nexport function jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\")\n return value.toString();\n return value;\n}\nexport function cached(getter) {\n const set = false;\n return {\n get value() {\n if (!set) {\n const value = getter();\n Object.defineProperty(this, \"value\", { value });\n return value;\n }\n throw new Error(\"cached value already set\");\n },\n };\n}\nexport function nullish(input) {\n return input === null || input === undefined;\n}\nexport function cleanRegex(source) {\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n return source.slice(start, end);\n}\nexport function floatSafeRemainder(val, step) {\n const valDecCount = (val.toString().split(\".\")[1] || \"\").length;\n const stepString = step.toString();\n let stepDecCount = (stepString.split(\".\")[1] || \"\").length;\n if (stepDecCount === 0 && /\\d?e-\\d?/.test(stepString)) {\n const match = stepString.match(/\\d?e-(\\d?)/);\n if (match?.[1]) {\n stepDecCount = Number.parseInt(match[1]);\n }\n }\n const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount;\n const valInt = Number.parseInt(val.toFixed(decCount).replace(\".\", \"\"));\n const stepInt = Number.parseInt(step.toFixed(decCount).replace(\".\", \"\"));\n return (valInt % stepInt) / 10 ** decCount;\n}\nconst EVALUATING = Symbol(\"evaluating\");\nexport function defineLazy(object, key, getter) {\n let value = undefined;\n Object.defineProperty(object, key, {\n get() {\n if (value === EVALUATING) {\n // Circular reference detected, return undefined to break the cycle\n return undefined;\n }\n if (value === undefined) {\n value = EVALUATING;\n value = getter();\n }\n return value;\n },\n set(v) {\n Object.defineProperty(object, key, {\n value: v,\n // configurable: true,\n });\n // object[key] = v;\n },\n configurable: true,\n });\n}\nexport function objectClone(obj) {\n return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));\n}\nexport function assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true,\n });\n}\nexport function mergeDefs(...defs) {\n const mergedDescriptors = {};\n for (const def of defs) {\n const descriptors = Object.getOwnPropertyDescriptors(def);\n Object.assign(mergedDescriptors, descriptors);\n }\n return Object.defineProperties({}, mergedDescriptors);\n}\nexport function cloneDef(schema) {\n return mergeDefs(schema._zod.def);\n}\nexport function getElementAtPath(obj, path) {\n if (!path)\n return obj;\n return path.reduce((acc, key) => acc?.[key], obj);\n}\nexport function promiseAllObject(promisesObj) {\n const keys = Object.keys(promisesObj);\n const promises = keys.map((key) => promisesObj[key]);\n return Promise.all(promises).then((results) => {\n const resolvedObj = {};\n for (let i = 0; i < keys.length; i++) {\n resolvedObj[keys[i]] = results[i];\n }\n return resolvedObj;\n });\n}\nexport function randomString(length = 10) {\n const chars = \"abcdefghijklmnopqrstuvwxyz\";\n let str = \"\";\n for (let i = 0; i < length; i++) {\n str += chars[Math.floor(Math.random() * chars.length)];\n }\n return str;\n}\nexport function esc(str) {\n return JSON.stringify(str);\n}\nexport const captureStackTrace = (\"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => { });\nexport function isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nexport const allowsEval = cached(() => {\n // @ts-ignore\n if (typeof navigator !== \"undefined\" && navigator?.userAgent?.includes(\"Cloudflare\")) {\n return false;\n }\n try {\n const F = Function;\n new F(\"\");\n return true;\n }\n catch (_) {\n return false;\n }\n});\nexport function isPlainObject(o) {\n if (isObject(o) === false)\n return false;\n // modified constructor\n const ctor = o.constructor;\n if (ctor === undefined)\n return true;\n // modified prototype\n const prot = ctor.prototype;\n if (isObject(prot) === false)\n return false;\n // ctor doesn't have static `isPrototypeOf`\n if (Object.prototype.hasOwnProperty.call(prot, \"isPrototypeOf\") === false) {\n return false;\n }\n return true;\n}\nexport function shallowClone(o) {\n if (isPlainObject(o))\n return { ...o };\n if (Array.isArray(o))\n return [...o];\n return o;\n}\nexport function numKeys(data) {\n let keyCount = 0;\n for (const key in data) {\n if (Object.prototype.hasOwnProperty.call(data, key)) {\n keyCount++;\n }\n }\n return keyCount;\n}\nexport const getParsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"undefined\":\n return \"undefined\";\n case \"string\":\n return \"string\";\n case \"number\":\n return Number.isNaN(data) ? \"nan\" : \"number\";\n case \"boolean\":\n return \"boolean\";\n case \"function\":\n return \"function\";\n case \"bigint\":\n return \"bigint\";\n case \"symbol\":\n return \"symbol\";\n case \"object\":\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (data.then && typeof data.then === \"function\" && data.catch && typeof data.catch === \"function\") {\n return \"promise\";\n }\n if (typeof Map !== \"undefined\" && data instanceof Map) {\n return \"map\";\n }\n if (typeof Set !== \"undefined\" && data instanceof Set) {\n return \"set\";\n }\n if (typeof Date !== \"undefined\" && data instanceof Date) {\n return \"date\";\n }\n // @ts-ignore\n if (typeof File !== \"undefined\" && data instanceof File) {\n return \"file\";\n }\n return \"object\";\n default:\n throw new Error(`Unknown data type: ${t}`);\n }\n};\nexport const propertyKeyTypes = new Set([\"string\", \"number\", \"symbol\"]);\nexport const primitiveTypes = new Set([\"string\", \"number\", \"bigint\", \"boolean\", \"symbol\", \"undefined\"]);\nexport function escapeRegex(str) {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n// zod-specific utils\nexport function clone(inst, def, params) {\n const cl = new inst._zod.constr(def ?? inst._zod.def);\n if (!def || params?.parent)\n cl._zod.parent = inst;\n return cl;\n}\nexport function normalizeParams(_params) {\n const params = _params;\n if (!params)\n return {};\n if (typeof params === \"string\")\n return { error: () => params };\n if (params?.message !== undefined) {\n if (params?.error !== undefined)\n throw new Error(\"Cannot specify both `message` and `error` params\");\n params.error = params.message;\n }\n delete params.message;\n if (typeof params.error === \"string\")\n return { ...params, error: () => params.error };\n return params;\n}\nexport function createTransparentProxy(getter) {\n let target;\n return new Proxy({}, {\n get(_, prop, receiver) {\n target ?? (target = getter());\n return Reflect.get(target, prop, receiver);\n },\n set(_, prop, value, receiver) {\n target ?? (target = getter());\n return Reflect.set(target, prop, value, receiver);\n },\n has(_, prop) {\n target ?? (target = getter());\n return Reflect.has(target, prop);\n },\n deleteProperty(_, prop) {\n target ?? (target = getter());\n return Reflect.deleteProperty(target, prop);\n },\n ownKeys(_) {\n target ?? (target = getter());\n return Reflect.ownKeys(target);\n },\n getOwnPropertyDescriptor(_, prop) {\n target ?? (target = getter());\n return Reflect.getOwnPropertyDescriptor(target, prop);\n },\n defineProperty(_, prop, descriptor) {\n target ?? (target = getter());\n return Reflect.defineProperty(target, prop, descriptor);\n },\n });\n}\nexport function stringifyPrimitive(value) {\n if (typeof value === \"bigint\")\n return value.toString() + \"n\";\n if (typeof value === \"string\")\n return `\"${value}\"`;\n return `${value}`;\n}\nexport function optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin === \"optional\" && shape[k]._zod.optout === \"optional\";\n });\n}\nexport const NUMBER_FORMAT_RANGES = {\n safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],\n int32: [-2147483648, 2147483647],\n uint32: [0, 4294967295],\n float32: [-3.4028234663852886e38, 3.4028234663852886e38],\n float64: [-Number.MAX_VALUE, Number.MAX_VALUE],\n};\nexport const BIGINT_FORMAT_RANGES = {\n int64: [/* @__PURE__*/ BigInt(\"-9223372036854775808\"), /* @__PURE__*/ BigInt(\"9223372036854775807\")],\n uint64: [/* @__PURE__*/ BigInt(0), /* @__PURE__*/ BigInt(\"18446744073709551615\")],\n};\nexport function pick(schema, mask) {\n const currDef = schema._zod.def;\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const newShape = {};\n for (const key in mask) {\n if (!(key in currDef.shape)) {\n throw new Error(`Unrecognized key: \"${key}\"`);\n }\n if (!mask[key])\n continue;\n newShape[key] = currDef.shape[key];\n }\n assignProp(this, \"shape\", newShape); // self-caching\n return newShape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\nexport function omit(schema, mask) {\n const currDef = schema._zod.def;\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const newShape = { ...schema._zod.def.shape };\n for (const key in mask) {\n if (!(key in currDef.shape)) {\n throw new Error(`Unrecognized key: \"${key}\"`);\n }\n if (!mask[key])\n continue;\n delete newShape[key];\n }\n assignProp(this, \"shape\", newShape); // self-caching\n return newShape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\nexport function extend(schema, shape) {\n if (!isPlainObject(shape)) {\n throw new Error(\"Invalid input to extend: expected a plain object\");\n }\n const checks = schema._zod.def.checks;\n const hasChecks = checks && checks.length > 0;\n if (hasChecks) {\n throw new Error(\"Object schemas containing refinements cannot be extended. Use `.safeExtend()` instead.\");\n }\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const _shape = { ...schema._zod.def.shape, ...shape };\n assignProp(this, \"shape\", _shape); // self-caching\n return _shape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\nexport function safeExtend(schema, shape) {\n if (!isPlainObject(shape)) {\n throw new Error(\"Invalid input to safeExtend: expected a plain object\");\n }\n const def = {\n ...schema._zod.def,\n get shape() {\n const _shape = { ...schema._zod.def.shape, ...shape };\n assignProp(this, \"shape\", _shape); // self-caching\n return _shape;\n },\n checks: schema._zod.def.checks,\n };\n return clone(schema, def);\n}\nexport function merge(a, b) {\n const def = mergeDefs(a._zod.def, {\n get shape() {\n const _shape = { ...a._zod.def.shape, ...b._zod.def.shape };\n assignProp(this, \"shape\", _shape); // self-caching\n return _shape;\n },\n get catchall() {\n return b._zod.def.catchall;\n },\n checks: [], // delete existing checks\n });\n return clone(a, def);\n}\nexport function partial(Class, schema, mask) {\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const oldShape = schema._zod.def.shape;\n const shape = { ...oldShape };\n if (mask) {\n for (const key in mask) {\n if (!(key in oldShape)) {\n throw new Error(`Unrecognized key: \"${key}\"`);\n }\n if (!mask[key])\n continue;\n // if (oldShape[key]!._zod.optin === \"optional\") continue;\n shape[key] = Class\n ? new Class({\n type: \"optional\",\n innerType: oldShape[key],\n })\n : oldShape[key];\n }\n }\n else {\n for (const key in oldShape) {\n // if (oldShape[key]!._zod.optin === \"optional\") continue;\n shape[key] = Class\n ? new Class({\n type: \"optional\",\n innerType: oldShape[key],\n })\n : oldShape[key];\n }\n }\n assignProp(this, \"shape\", shape); // self-caching\n return shape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\nexport function required(Class, schema, mask) {\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const oldShape = schema._zod.def.shape;\n const shape = { ...oldShape };\n if (mask) {\n for (const key in mask) {\n if (!(key in shape)) {\n throw new Error(`Unrecognized key: \"${key}\"`);\n }\n if (!mask[key])\n continue;\n // overwrite with non-optional\n shape[key] = new Class({\n type: \"nonoptional\",\n innerType: oldShape[key],\n });\n }\n }\n else {\n for (const key in oldShape) {\n // overwrite with non-optional\n shape[key] = new Class({\n type: \"nonoptional\",\n innerType: oldShape[key],\n });\n }\n }\n assignProp(this, \"shape\", shape); // self-caching\n return shape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\n// invalid_type | too_big | too_small | invalid_format | not_multiple_of | unrecognized_keys | invalid_union | invalid_key | invalid_element | invalid_value | custom\nexport function aborted(x, startIndex = 0) {\n if (x.aborted === true)\n return true;\n for (let i = startIndex; i < x.issues.length; i++) {\n if (x.issues[i]?.continue !== true) {\n return true;\n }\n }\n return false;\n}\nexport function prefixIssues(path, issues) {\n return issues.map((iss) => {\n var _a;\n (_a = iss).path ?? (_a.path = []);\n iss.path.unshift(path);\n return iss;\n });\n}\nexport function unwrapMessage(message) {\n return typeof message === \"string\" ? message : message?.message;\n}\nexport function finalizeIssue(iss, ctx, config) {\n const full = { ...iss, path: iss.path ?? [] };\n // for backwards compatibility\n if (!iss.message) {\n const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ??\n unwrapMessage(ctx?.error?.(iss)) ??\n unwrapMessage(config.customError?.(iss)) ??\n unwrapMessage(config.localeError?.(iss)) ??\n \"Invalid input\";\n full.message = message;\n }\n // delete (full as any).def;\n delete full.inst;\n delete full.continue;\n if (!ctx?.reportInput) {\n delete full.input;\n }\n return full;\n}\nexport function getSizableOrigin(input) {\n if (input instanceof Set)\n return \"set\";\n if (input instanceof Map)\n return \"map\";\n // @ts-ignore\n if (input instanceof File)\n return \"file\";\n return \"unknown\";\n}\nexport function getLengthableOrigin(input) {\n if (Array.isArray(input))\n return \"array\";\n if (typeof input === \"string\")\n return \"string\";\n return \"unknown\";\n}\nexport function issue(...args) {\n const [iss, input, inst] = args;\n if (typeof iss === \"string\") {\n return {\n message: iss,\n code: \"custom\",\n input,\n inst,\n };\n }\n return { ...iss };\n}\nexport function cleanEnum(obj) {\n return Object.entries(obj)\n .filter(([k, _]) => {\n // return true if NaN, meaning it's not a number, thus a string key\n return Number.isNaN(Number.parseInt(k, 10));\n })\n .map((el) => el[1]);\n}\n// Codec utility functions\nexport function base64ToUint8Array(base64) {\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes;\n}\nexport function uint8ArrayToBase64(bytes) {\n let binaryString = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binaryString += String.fromCharCode(bytes[i]);\n }\n return btoa(binaryString);\n}\nexport function base64urlToUint8Array(base64url) {\n const base64 = base64url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padding = \"=\".repeat((4 - (base64.length % 4)) % 4);\n return base64ToUint8Array(base64 + padding);\n}\nexport function uint8ArrayToBase64url(bytes) {\n return uint8ArrayToBase64(bytes).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n}\nexport function hexToUint8Array(hex) {\n const cleanHex = hex.replace(/^0x/, \"\");\n if (cleanHex.length % 2 !== 0) {\n throw new Error(\"Invalid hex string length\");\n }\n const bytes = new Uint8Array(cleanHex.length / 2);\n for (let i = 0; i < cleanHex.length; i += 2) {\n bytes[i / 2] = Number.parseInt(cleanHex.slice(i, i + 2), 16);\n }\n return bytes;\n}\nexport function uint8ArrayToHex(bytes) {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n// instanceof\nexport class Class {\n constructor(..._args) { }\n}\n","import { $constructor } from \"./core.js\";\nimport * as util from \"./util.js\";\nconst initializer = (inst, def) => {\n inst.name = \"$ZodError\";\n Object.defineProperty(inst, \"_zod\", {\n value: inst._zod,\n enumerable: false,\n });\n Object.defineProperty(inst, \"issues\", {\n value: def,\n enumerable: false,\n });\n inst.message = JSON.stringify(def, util.jsonStringifyReplacer, 2);\n Object.defineProperty(inst, \"toString\", {\n value: () => inst.message,\n enumerable: false,\n });\n};\nexport const $ZodError = $constructor(\"$ZodError\", initializer);\nexport const $ZodRealError = $constructor(\"$ZodError\", initializer, { Parent: Error });\nexport function flattenError(error, mapper = (issue) => issue.message) {\n const fieldErrors = {};\n const formErrors = [];\n for (const sub of error.issues) {\n if (sub.path.length > 0) {\n fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];\n fieldErrors[sub.path[0]].push(mapper(sub));\n }\n else {\n formErrors.push(mapper(sub));\n }\n }\n return { formErrors, fieldErrors };\n}\nexport function formatError(error, _mapper) {\n const mapper = _mapper ||\n function (issue) {\n return issue.message;\n };\n const fieldErrors = { _errors: [] };\n const processError = (error) => {\n for (const issue of error.issues) {\n if (issue.code === \"invalid_union\" && issue.errors.length) {\n issue.errors.map((issues) => processError({ issues }));\n }\n else if (issue.code === \"invalid_key\") {\n processError({ issues: issue.issues });\n }\n else if (issue.code === \"invalid_element\") {\n processError({ issues: issue.issues });\n }\n else if (issue.path.length === 0) {\n fieldErrors._errors.push(mapper(issue));\n }\n else {\n let curr = fieldErrors;\n let i = 0;\n while (i < issue.path.length) {\n const el = issue.path[i];\n const terminal = i === issue.path.length - 1;\n if (!terminal) {\n curr[el] = curr[el] || { _errors: [] };\n }\n else {\n curr[el] = curr[el] || { _errors: [] };\n curr[el]._errors.push(mapper(issue));\n }\n curr = curr[el];\n i++;\n }\n }\n }\n };\n processError(error);\n return fieldErrors;\n}\nexport function treeifyError(error, _mapper) {\n const mapper = _mapper ||\n function (issue) {\n return issue.message;\n };\n const result = { errors: [] };\n const processError = (error, path = []) => {\n var _a, _b;\n for (const issue of error.issues) {\n if (issue.code === \"invalid_union\" && issue.errors.length) {\n // regular union error\n issue.errors.map((issues) => processError({ issues }, issue.path));\n }\n else if (issue.code === \"invalid_key\") {\n processError({ issues: issue.issues }, issue.path);\n }\n else if (issue.code === \"invalid_element\") {\n processError({ issues: issue.issues }, issue.path);\n }\n else {\n const fullpath = [...path, ...issue.path];\n if (fullpath.length === 0) {\n result.errors.push(mapper(issue));\n continue;\n }\n let curr = result;\n let i = 0;\n while (i < fullpath.length) {\n const el = fullpath[i];\n const terminal = i === fullpath.length - 1;\n if (typeof el === \"string\") {\n curr.properties ?? (curr.properties = {});\n (_a = curr.properties)[el] ?? (_a[el] = { errors: [] });\n curr = curr.properties[el];\n }\n else {\n curr.items ?? (curr.items = []);\n (_b = curr.items)[el] ?? (_b[el] = { errors: [] });\n curr = curr.items[el];\n }\n if (terminal) {\n curr.errors.push(mapper(issue));\n }\n i++;\n }\n }\n }\n };\n processError(error);\n return result;\n}\n/** Format a ZodError as a human-readable string in the following form.\n *\n * From\n *\n * ```ts\n * ZodError {\n * issues: [\n * {\n * expected: 'string',\n * code: 'invalid_type',\n * path: [ 'username' ],\n * message: 'Invalid input: expected string'\n * },\n * {\n * expected: 'number',\n * code: 'invalid_type',\n * path: [ 'favoriteNumbers', 1 ],\n * message: 'Invalid input: expected number'\n * }\n * ];\n * }\n * ```\n *\n * to\n *\n * ```\n * username\n * ✖ Expected number, received string at \"username\n * favoriteNumbers[0]\n * ✖ Invalid input: expected number\n * ```\n */\nexport function toDotPath(_path) {\n const segs = [];\n const path = _path.map((seg) => (typeof seg === \"object\" ? seg.key : seg));\n for (const seg of path) {\n if (typeof seg === \"number\")\n segs.push(`[${seg}]`);\n else if (typeof seg === \"symbol\")\n segs.push(`[${JSON.stringify(String(seg))}]`);\n else if (/[^\\w$]/.test(seg))\n segs.push(`[${JSON.stringify(seg)}]`);\n else {\n if (segs.length)\n segs.push(\".\");\n segs.push(seg);\n }\n }\n return segs.join(\"\");\n}\nexport function prettifyError(error) {\n const lines = [];\n // sort by path length\n const issues = [...error.issues].sort((a, b) => (a.path ?? []).length - (b.path ?? []).length);\n // Process each issue\n for (const issue of issues) {\n lines.push(`✖ ${issue.message}`);\n if (issue.path?.length)\n lines.push(` → at ${toDotPath(issue.path)}`);\n }\n // Convert Map to formatted string\n return lines.join(\"\\n\");\n}\n","import * as core from \"./core.js\";\nimport * as errors from \"./errors.js\";\nimport * as util from \"./util.js\";\nexport const _parse = (_Err) => (schema, value, _ctx, _params) => {\n const ctx = _ctx ? Object.assign(_ctx, { async: false }) : { async: false };\n const result = schema._zod.run({ value, issues: [] }, ctx);\n if (result instanceof Promise) {\n throw new core.$ZodAsyncError();\n }\n if (result.issues.length) {\n const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));\n util.captureStackTrace(e, _params?.callee);\n throw e;\n }\n return result.value;\n};\nexport const parse = /* @__PURE__*/ _parse(errors.$ZodRealError);\nexport const _parseAsync = (_Err) => async (schema, value, _ctx, params) => {\n const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };\n let result = schema._zod.run({ value, issues: [] }, ctx);\n if (result instanceof Promise)\n result = await result;\n if (result.issues.length) {\n const e = new (params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));\n util.captureStackTrace(e, params?.callee);\n throw e;\n }\n return result.value;\n};\nexport const parseAsync = /* @__PURE__*/ _parseAsync(errors.$ZodRealError);\nexport const _safeParse = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? { ..._ctx, async: false } : { async: false };\n const result = schema._zod.run({ value, issues: [] }, ctx);\n if (result instanceof Promise) {\n throw new core.$ZodAsyncError();\n }\n return result.issues.length\n ? {\n success: false,\n error: new (_Err ?? errors.$ZodError)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))),\n }\n : { success: true, data: result.value };\n};\nexport const safeParse = /* @__PURE__*/ _safeParse(errors.$ZodRealError);\nexport const _safeParseAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };\n let result = schema._zod.run({ value, issues: [] }, ctx);\n if (result instanceof Promise)\n result = await result;\n return result.issues.length\n ? {\n success: false,\n error: new _Err(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))),\n }\n : { success: true, data: result.value };\n};\nexport const safeParseAsync = /* @__PURE__*/ _safeParseAsync(errors.$ZodRealError);\nexport const _encode = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { direction: \"backward\" }) : { direction: \"backward\" };\n return _parse(_Err)(schema, value, ctx);\n};\nexport const encode = /* @__PURE__*/ _encode(errors.$ZodRealError);\nexport const _decode = (_Err) => (schema, value, _ctx) => {\n return _parse(_Err)(schema, value, _ctx);\n};\nexport const decode = /* @__PURE__*/ _decode(errors.$ZodRealError);\nexport const _encodeAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { direction: \"backward\" }) : { direction: \"backward\" };\n return _parseAsync(_Err)(schema, value, ctx);\n};\nexport const encodeAsync = /* @__PURE__*/ _encodeAsync(errors.$ZodRealError);\nexport const _decodeAsync = (_Err) => async (schema, value, _ctx) => {\n return _parseAsync(_Err)(schema, value, _ctx);\n};\nexport const decodeAsync = /* @__PURE__*/ _decodeAsync(errors.$ZodRealError);\nexport const _safeEncode = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { direction: \"backward\" }) : { direction: \"backward\" };\n return _safeParse(_Err)(schema, value, ctx);\n};\nexport const safeEncode = /* @__PURE__*/ _safeEncode(errors.$ZodRealError);\nexport const _safeDecode = (_Err) => (schema, value, _ctx) => {\n return _safeParse(_Err)(schema, value, _ctx);\n};\nexport const safeDecode = /* @__PURE__*/ _safeDecode(errors.$ZodRealError);\nexport const _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { direction: \"backward\" }) : { direction: \"backward\" };\n return _safeParseAsync(_Err)(schema, value, ctx);\n};\nexport const safeEncodeAsync = /* @__PURE__*/ _safeEncodeAsync(errors.$ZodRealError);\nexport const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {\n return _safeParseAsync(_Err)(schema, value, _ctx);\n};\nexport const safeDecodeAsync = /* @__PURE__*/ _safeDecodeAsync(errors.$ZodRealError);\n","export const cuid = /^[cC][^\\s-]{8,}$/;\nexport const cuid2 = /^[0-9a-z]+$/;\nexport const ulid = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;\nexport const xid = /^[0-9a-vA-V]{20}$/;\nexport const ksuid = /^[A-Za-z0-9]{27}$/;\nexport const nanoid = /^[a-zA-Z0-9_-]{21}$/;\n/** ISO 8601-1 duration regex. Does not support the 8601-2 extensions like negative durations or fractional/negative components. */\nexport const duration = /^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$/;\n/** Implements ISO 8601-2 extensions like explicit +- prefixes, mixing weeks with other units, and fractional/negative components. */\nexport const extendedDuration = /^[-+]?P(?!$)(?:(?:[-+]?\\d+Y)|(?:[-+]?\\d+[.,]\\d+Y$))?(?:(?:[-+]?\\d+M)|(?:[-+]?\\d+[.,]\\d+M$))?(?:(?:[-+]?\\d+W)|(?:[-+]?\\d+[.,]\\d+W$))?(?:(?:[-+]?\\d+D)|(?:[-+]?\\d+[.,]\\d+D$))?(?:T(?=[\\d+-])(?:(?:[-+]?\\d+H)|(?:[-+]?\\d+[.,]\\d+H$))?(?:(?:[-+]?\\d+M)|(?:[-+]?\\d+[.,]\\d+M$))?(?:[-+]?\\d+(?:[.,]\\d+)?S)?)??$/;\n/** A regex for any UUID-like identifier: 8-4-4-4-12 hex pattern */\nexport const guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;\n/** Returns a regex for validating an RFC 9562/4122 UUID.\n *\n * @param version Optionally specify a version 1-8. If no version is specified, all versions are supported. */\nexport const uuid = (version) => {\n if (!version)\n return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;\n return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);\n};\nexport const uuid4 = /*@__PURE__*/ uuid(4);\nexport const uuid6 = /*@__PURE__*/ uuid(6);\nexport const uuid7 = /*@__PURE__*/ uuid(7);\n/** Practical email validation */\nexport const email = /^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$/;\n/** Equivalent to the HTML5 input[type=email] validation implemented by browsers. Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email */\nexport const html5Email = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;\n/** The classic emailregex.com regex for RFC 5322-compliant emails */\nexport const rfc5322Email = /^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;\n/** A loose regex that allows Unicode characters, enforces length limits, and that's about it. */\nexport const unicodeEmail = /^[^\\s@\"]{1,64}@[^\\s@]{1,255}$/u;\nexport const idnEmail = unicodeEmail;\nexport const browserEmail = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;\n// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression\nconst _emoji = `^(\\\\p{Extended_Pictographic}|\\\\p{Emoji_Component})+$`;\nexport function emoji() {\n return new RegExp(_emoji, \"u\");\n}\nexport const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;\nexport const ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;\nexport const cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\/([0-9]|[1-2][0-9]|3[0-2])$/;\nexport const cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;\n// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript\nexport const base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;\nexport const base64url = /^[A-Za-z0-9_-]*$/;\n// based on https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address\n// export const hostname: RegExp = /^([a-zA-Z0-9-]+\\.)*[a-zA-Z0-9-]+$/;\nexport const hostname = /^(?=.{1,253}\\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\\.?$/;\nexport const domain = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$/;\n// https://blog.stevenlevithan.com/archives/validate-phone-number#r4-3 (regex sans spaces)\nexport const e164 = /^\\+(?:[0-9]){6,14}[0-9]$/;\n// const dateSource = `((\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\\\d|30)|(02)-(0[1-9]|1\\\\d|2[0-8])))`;\nconst dateSource = `(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))`;\nexport const date = /*@__PURE__*/ new RegExp(`^${dateSource}$`);\nfunction timeSource(args) {\n const hhmm = `(?:[01]\\\\d|2[0-3]):[0-5]\\\\d`;\n const regex = typeof args.precision === \"number\"\n ? args.precision === -1\n ? `${hhmm}`\n : args.precision === 0\n ? `${hhmm}:[0-5]\\\\d`\n : `${hhmm}:[0-5]\\\\d\\\\.\\\\d{${args.precision}}`\n : `${hhmm}(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?`;\n return regex;\n}\nexport function time(args) {\n return new RegExp(`^${timeSource(args)}$`);\n}\n// Adapted from https://stackoverflow.com/a/3143231\nexport function datetime(args) {\n const time = timeSource({ precision: args.precision });\n const opts = [\"Z\"];\n if (args.local)\n opts.push(\"\");\n // if (args.offset) opts.push(`([+-]\\\\d{2}:\\\\d{2})`);\n if (args.offset)\n opts.push(`([+-](?:[01]\\\\d|2[0-3]):[0-5]\\\\d)`);\n const timeRegex = `${time}(?:${opts.join(\"|\")})`;\n return new RegExp(`^${dateSource}T(?:${timeRegex})$`);\n}\nexport const string = (params) => {\n const regex = params ? `[\\\\s\\\\S]{${params?.minimum ?? 0},${params?.maximum ?? \"\"}}` : `[\\\\s\\\\S]*`;\n return new RegExp(`^${regex}$`);\n};\nexport const bigint = /^-?\\d+n?$/;\nexport const integer = /^-?\\d+$/;\nexport const number = /^-?\\d+(?:\\.\\d+)?/;\nexport const boolean = /^(?:true|false)$/i;\nconst _null = /^null$/i;\nexport { _null as null };\nconst _undefined = /^undefined$/i;\nexport { _undefined as undefined };\n// regex for string with no uppercase letters\nexport const lowercase = /^[^A-Z]*$/;\n// regex for string with no lowercase letters\nexport const uppercase = /^[^a-z]*$/;\n// regex for hexadecimal strings (any length)\nexport const hex = /^[0-9a-fA-F]*$/;\n// Hash regexes for different algorithms and encodings\n// Helper function to create base64 regex with exact length and padding\nfunction fixedBase64(bodyLength, padding) {\n return new RegExp(`^[A-Za-z0-9+/]{${bodyLength}}${padding}$`);\n}\n// Helper function to create base64url regex with exact length (no padding)\nfunction fixedBase64url(length) {\n return new RegExp(`^[A-Za-z0-9_-]{${length}}$`);\n}\n// MD5 (16 bytes): base64 = 24 chars total (22 + \"==\")\nexport const md5_hex = /^[0-9a-fA-F]{32}$/;\nexport const md5_base64 = /*@__PURE__*/ fixedBase64(22, \"==\");\nexport const md5_base64url = /*@__PURE__*/ fixedBase64url(22);\n// SHA1 (20 bytes): base64 = 28 chars total (27 + \"=\")\nexport const sha1_hex = /^[0-9a-fA-F]{40}$/;\nexport const sha1_base64 = /*@__PURE__*/ fixedBase64(27, \"=\");\nexport const sha1_base64url = /*@__PURE__*/ fixedBase64url(27);\n// SHA256 (32 bytes): base64 = 44 chars total (43 + \"=\")\nexport const sha256_hex = /^[0-9a-fA-F]{64}$/;\nexport const sha256_base64 = /*@__PURE__*/ fixedBase64(43, \"=\");\nexport const sha256_base64url = /*@__PURE__*/ fixedBase64url(43);\n// SHA384 (48 bytes): base64 = 64 chars total (no padding)\nexport const sha384_hex = /^[0-9a-fA-F]{96}$/;\nexport const sha384_base64 = /*@__PURE__*/ fixedBase64(64, \"\");\nexport const sha384_base64url = /*@__PURE__*/ fixedBase64url(64);\n// SHA512 (64 bytes): base64 = 88 chars total (86 + \"==\")\nexport const sha512_hex = /^[0-9a-fA-F]{128}$/;\nexport const sha512_base64 = /*@__PURE__*/ fixedBase64(86, \"==\");\nexport const sha512_base64url = /*@__PURE__*/ fixedBase64url(86);\n","// import { $ZodType } from \"./schemas.js\";\nimport * as core from \"./core.js\";\nimport * as regexes from \"./regexes.js\";\nimport * as util from \"./util.js\";\nexport const $ZodCheck = /*@__PURE__*/ core.$constructor(\"$ZodCheck\", (inst, def) => {\n var _a;\n inst._zod ?? (inst._zod = {});\n inst._zod.def = def;\n (_a = inst._zod).onattach ?? (_a.onattach = []);\n});\nconst numericOriginMap = {\n number: \"number\",\n bigint: \"bigint\",\n object: \"date\",\n};\nexport const $ZodCheckLessThan = /*@__PURE__*/ core.$constructor(\"$ZodCheckLessThan\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const origin = numericOriginMap[typeof def.value];\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n const curr = (def.inclusive ? bag.maximum : bag.exclusiveMaximum) ?? Number.POSITIVE_INFINITY;\n if (def.value < curr) {\n if (def.inclusive)\n bag.maximum = def.value;\n else\n bag.exclusiveMaximum = def.value;\n }\n });\n inst._zod.check = (payload) => {\n if (def.inclusive ? payload.value <= def.value : payload.value < def.value) {\n return;\n }\n payload.issues.push({\n origin,\n code: \"too_big\",\n maximum: def.value,\n input: payload.value,\n inclusive: def.inclusive,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckGreaterThan = /*@__PURE__*/ core.$constructor(\"$ZodCheckGreaterThan\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const origin = numericOriginMap[typeof def.value];\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n const curr = (def.inclusive ? bag.minimum : bag.exclusiveMinimum) ?? Number.NEGATIVE_INFINITY;\n if (def.value > curr) {\n if (def.inclusive)\n bag.minimum = def.value;\n else\n bag.exclusiveMinimum = def.value;\n }\n });\n inst._zod.check = (payload) => {\n if (def.inclusive ? payload.value >= def.value : payload.value > def.value) {\n return;\n }\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.value,\n input: payload.value,\n inclusive: def.inclusive,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckMultipleOf = \n/*@__PURE__*/ core.$constructor(\"$ZodCheckMultipleOf\", (inst, def) => {\n $ZodCheck.init(inst, def);\n inst._zod.onattach.push((inst) => {\n var _a;\n (_a = inst._zod.bag).multipleOf ?? (_a.multipleOf = def.value);\n });\n inst._zod.check = (payload) => {\n if (typeof payload.value !== typeof def.value)\n throw new Error(\"Cannot mix number and bigint in multiple_of check.\");\n const isMultiple = typeof payload.value === \"bigint\"\n ? payload.value % def.value === BigInt(0)\n : util.floatSafeRemainder(payload.value, def.value) === 0;\n if (isMultiple)\n return;\n payload.issues.push({\n origin: typeof payload.value,\n code: \"not_multiple_of\",\n divisor: def.value,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckNumberFormat = /*@__PURE__*/ core.$constructor(\"$ZodCheckNumberFormat\", (inst, def) => {\n $ZodCheck.init(inst, def); // no format checks\n def.format = def.format || \"float64\";\n const isInt = def.format?.includes(\"int\");\n const origin = isInt ? \"int\" : \"number\";\n const [minimum, maximum] = util.NUMBER_FORMAT_RANGES[def.format];\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = def.format;\n bag.minimum = minimum;\n bag.maximum = maximum;\n if (isInt)\n bag.pattern = regexes.integer;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n if (isInt) {\n if (!Number.isInteger(input)) {\n // invalid_format issue\n // payload.issues.push({\n // expected: def.format,\n // format: def.format,\n // code: \"invalid_format\",\n // input,\n // inst,\n // });\n // invalid_type issue\n payload.issues.push({\n expected: origin,\n format: def.format,\n code: \"invalid_type\",\n continue: false,\n input,\n inst,\n });\n return;\n // not_multiple_of issue\n // payload.issues.push({\n // code: \"not_multiple_of\",\n // origin: \"number\",\n // input,\n // inst,\n // divisor: 1,\n // });\n }\n if (!Number.isSafeInteger(input)) {\n if (input > 0) {\n // too_big\n payload.issues.push({\n input,\n code: \"too_big\",\n maximum: Number.MAX_SAFE_INTEGER,\n note: \"Integers must be within the safe integer range.\",\n inst,\n origin,\n continue: !def.abort,\n });\n }\n else {\n // too_small\n payload.issues.push({\n input,\n code: \"too_small\",\n minimum: Number.MIN_SAFE_INTEGER,\n note: \"Integers must be within the safe integer range.\",\n inst,\n origin,\n continue: !def.abort,\n });\n }\n return;\n }\n }\n if (input < minimum) {\n payload.issues.push({\n origin: \"number\",\n input,\n code: \"too_small\",\n minimum,\n inclusive: true,\n inst,\n continue: !def.abort,\n });\n }\n if (input > maximum) {\n payload.issues.push({\n origin: \"number\",\n input,\n code: \"too_big\",\n maximum,\n inst,\n });\n }\n };\n});\nexport const $ZodCheckBigIntFormat = /*@__PURE__*/ core.$constructor(\"$ZodCheckBigIntFormat\", (inst, def) => {\n $ZodCheck.init(inst, def); // no format checks\n const [minimum, maximum] = util.BIGINT_FORMAT_RANGES[def.format];\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = def.format;\n bag.minimum = minimum;\n bag.maximum = maximum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n if (input < minimum) {\n payload.issues.push({\n origin: \"bigint\",\n input,\n code: \"too_small\",\n minimum: minimum,\n inclusive: true,\n inst,\n continue: !def.abort,\n });\n }\n if (input > maximum) {\n payload.issues.push({\n origin: \"bigint\",\n input,\n code: \"too_big\",\n maximum,\n inst,\n });\n }\n };\n});\nexport const $ZodCheckMaxSize = /*@__PURE__*/ core.$constructor(\"$ZodCheckMaxSize\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.size !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY);\n if (def.maximum < curr)\n inst._zod.bag.maximum = def.maximum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const size = input.size;\n if (size <= def.maximum)\n return;\n payload.issues.push({\n origin: util.getSizableOrigin(input),\n code: \"too_big\",\n maximum: def.maximum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckMinSize = /*@__PURE__*/ core.$constructor(\"$ZodCheckMinSize\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.size !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY);\n if (def.minimum > curr)\n inst._zod.bag.minimum = def.minimum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const size = input.size;\n if (size >= def.minimum)\n return;\n payload.issues.push({\n origin: util.getSizableOrigin(input),\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckSizeEquals = /*@__PURE__*/ core.$constructor(\"$ZodCheckSizeEquals\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.size !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.minimum = def.size;\n bag.maximum = def.size;\n bag.size = def.size;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const size = input.size;\n if (size === def.size)\n return;\n const tooBig = size > def.size;\n payload.issues.push({\n origin: util.getSizableOrigin(input),\n ...(tooBig ? { code: \"too_big\", maximum: def.size } : { code: \"too_small\", minimum: def.size }),\n inclusive: true,\n exact: true,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckMaxLength = /*@__PURE__*/ core.$constructor(\"$ZodCheckMaxLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.length !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY);\n if (def.maximum < curr)\n inst._zod.bag.maximum = def.maximum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const length = input.length;\n if (length <= def.maximum)\n return;\n const origin = util.getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_big\",\n maximum: def.maximum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckMinLength = /*@__PURE__*/ core.$constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.length !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY);\n if (def.minimum > curr)\n inst._zod.bag.minimum = def.minimum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const length = input.length;\n if (length >= def.minimum)\n return;\n const origin = util.getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckLengthEquals = /*@__PURE__*/ core.$constructor(\"$ZodCheckLengthEquals\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.length !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.minimum = def.length;\n bag.maximum = def.length;\n bag.length = def.length;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const length = input.length;\n if (length === def.length)\n return;\n const origin = util.getLengthableOrigin(input);\n const tooBig = length > def.length;\n payload.issues.push({\n origin,\n ...(tooBig ? { code: \"too_big\", maximum: def.length } : { code: \"too_small\", minimum: def.length }),\n inclusive: true,\n exact: true,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckStringFormat = /*@__PURE__*/ core.$constructor(\"$ZodCheckStringFormat\", (inst, def) => {\n var _a, _b;\n $ZodCheck.init(inst, def);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = def.format;\n if (def.pattern) {\n bag.patterns ?? (bag.patterns = new Set());\n bag.patterns.add(def.pattern);\n }\n });\n if (def.pattern)\n (_a = inst._zod).check ?? (_a.check = (payload) => {\n def.pattern.lastIndex = 0;\n if (def.pattern.test(payload.value))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: def.format,\n input: payload.value,\n ...(def.pattern ? { pattern: def.pattern.toString() } : {}),\n inst,\n continue: !def.abort,\n });\n });\n else\n (_b = inst._zod).check ?? (_b.check = () => { });\n});\nexport const $ZodCheckRegex = /*@__PURE__*/ core.$constructor(\"$ZodCheckRegex\", (inst, def) => {\n $ZodCheckStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n def.pattern.lastIndex = 0;\n if (def.pattern.test(payload.value))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"regex\",\n input: payload.value,\n pattern: def.pattern.toString(),\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckLowerCase = /*@__PURE__*/ core.$constructor(\"$ZodCheckLowerCase\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.lowercase);\n $ZodCheckStringFormat.init(inst, def);\n});\nexport const $ZodCheckUpperCase = /*@__PURE__*/ core.$constructor(\"$ZodCheckUpperCase\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.uppercase);\n $ZodCheckStringFormat.init(inst, def);\n});\nexport const $ZodCheckIncludes = /*@__PURE__*/ core.$constructor(\"$ZodCheckIncludes\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const escapedRegex = util.escapeRegex(def.includes);\n const pattern = new RegExp(typeof def.position === \"number\" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);\n def.pattern = pattern;\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.includes(def.includes, def.position))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"includes\",\n includes: def.includes,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckStartsWith = /*@__PURE__*/ core.$constructor(\"$ZodCheckStartsWith\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const pattern = new RegExp(`^${util.escapeRegex(def.prefix)}.*`);\n def.pattern ?? (def.pattern = pattern);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.startsWith(def.prefix))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"starts_with\",\n prefix: def.prefix,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckEndsWith = /*@__PURE__*/ core.$constructor(\"$ZodCheckEndsWith\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const pattern = new RegExp(`.*${util.escapeRegex(def.suffix)}$`);\n def.pattern ?? (def.pattern = pattern);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.endsWith(def.suffix))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"ends_with\",\n suffix: def.suffix,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\n///////////////////////////////////\n///// $ZodCheckProperty /////\n///////////////////////////////////\nfunction handleCheckPropertyResult(result, payload, property) {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(property, result.issues));\n }\n}\nexport const $ZodCheckProperty = /*@__PURE__*/ core.$constructor(\"$ZodCheckProperty\", (inst, def) => {\n $ZodCheck.init(inst, def);\n inst._zod.check = (payload) => {\n const result = def.schema._zod.run({\n value: payload.value[def.property],\n issues: [],\n }, {});\n if (result instanceof Promise) {\n return result.then((result) => handleCheckPropertyResult(result, payload, def.property));\n }\n handleCheckPropertyResult(result, payload, def.property);\n return;\n };\n});\nexport const $ZodCheckMimeType = /*@__PURE__*/ core.$constructor(\"$ZodCheckMimeType\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const mimeSet = new Set(def.mime);\n inst._zod.onattach.push((inst) => {\n inst._zod.bag.mime = def.mime;\n });\n inst._zod.check = (payload) => {\n if (mimeSet.has(payload.value.type))\n return;\n payload.issues.push({\n code: \"invalid_value\",\n values: def.mime,\n input: payload.value.type,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckOverwrite = /*@__PURE__*/ core.$constructor(\"$ZodCheckOverwrite\", (inst, def) => {\n $ZodCheck.init(inst, def);\n inst._zod.check = (payload) => {\n payload.value = def.tx(payload.value);\n };\n});\n","export class Doc {\n constructor(args = []) {\n this.content = [];\n this.indent = 0;\n if (this)\n this.args = args;\n }\n indented(fn) {\n this.indent += 1;\n fn(this);\n this.indent -= 1;\n }\n write(arg) {\n if (typeof arg === \"function\") {\n arg(this, { execution: \"sync\" });\n arg(this, { execution: \"async\" });\n return;\n }\n const content = arg;\n const lines = content.split(\"\\n\").filter((x) => x);\n const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length));\n const dedented = lines.map((x) => x.slice(minIndent)).map((x) => \" \".repeat(this.indent * 2) + x);\n for (const line of dedented) {\n this.content.push(line);\n }\n }\n compile() {\n const F = Function;\n const args = this?.args;\n const content = this?.content ?? [``];\n const lines = [...content.map((x) => ` ${x}`)];\n // console.log(lines.join(\"\\n\"));\n return new F(...args, lines.join(\"\\n\"));\n }\n}\n","export const version = {\n major: 4,\n minor: 1,\n patch: 8,\n};\n","import * as checks from \"./checks.js\";\nimport * as core from \"./core.js\";\nimport { Doc } from \"./doc.js\";\nimport { parse, parseAsync, safeParse, safeParseAsync } from \"./parse.js\";\nimport * as regexes from \"./regexes.js\";\nimport * as util from \"./util.js\";\nimport { version } from \"./versions.js\";\nexport const $ZodType = /*@__PURE__*/ core.$constructor(\"$ZodType\", (inst, def) => {\n var _a;\n inst ?? (inst = {});\n inst._zod.def = def; // set _def property\n inst._zod.bag = inst._zod.bag || {}; // initialize _bag object\n inst._zod.version = version;\n const checks = [...(inst._zod.def.checks ?? [])];\n // if inst is itself a checks.$ZodCheck, run it as a check\n if (inst._zod.traits.has(\"$ZodCheck\")) {\n checks.unshift(inst);\n }\n for (const ch of checks) {\n for (const fn of ch._zod.onattach) {\n fn(inst);\n }\n }\n if (checks.length === 0) {\n // deferred initializer\n // inst._zod.parse is not yet defined\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n inst._zod.deferred?.push(() => {\n inst._zod.run = inst._zod.parse;\n });\n }\n else {\n const runChecks = (payload, checks, ctx) => {\n let isAborted = util.aborted(payload);\n let asyncResult;\n for (const ch of checks) {\n if (ch._zod.def.when) {\n const shouldRun = ch._zod.def.when(payload);\n if (!shouldRun)\n continue;\n }\n else if (isAborted) {\n continue;\n }\n const currLen = payload.issues.length;\n const _ = ch._zod.check(payload);\n if (_ instanceof Promise && ctx?.async === false) {\n throw new core.$ZodAsyncError();\n }\n if (asyncResult || _ instanceof Promise) {\n asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {\n await _;\n const nextLen = payload.issues.length;\n if (nextLen === currLen)\n return;\n if (!isAborted)\n isAborted = util.aborted(payload, currLen);\n });\n }\n else {\n const nextLen = payload.issues.length;\n if (nextLen === currLen)\n continue;\n if (!isAborted)\n isAborted = util.aborted(payload, currLen);\n }\n }\n if (asyncResult) {\n return asyncResult.then(() => {\n return payload;\n });\n }\n return payload;\n };\n // const handleChecksResult = (\n // checkResult: ParsePayload,\n // originalResult: ParsePayload,\n // ctx: ParseContextInternal\n // ): util.MaybeAsync => {\n // // if the checks mutated the value && there are no issues, re-parse the result\n // if (checkResult.value !== originalResult.value && !checkResult.issues.length)\n // return inst._zod.parse(checkResult, ctx);\n // return originalResult;\n // };\n const handleCanaryResult = (canary, payload, ctx) => {\n // abort if the canary is aborted\n if (util.aborted(canary)) {\n canary.aborted = true;\n return canary;\n }\n // run checks first, then\n const checkResult = runChecks(payload, checks, ctx);\n if (checkResult instanceof Promise) {\n if (ctx.async === false)\n throw new core.$ZodAsyncError();\n return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));\n }\n return inst._zod.parse(checkResult, ctx);\n };\n inst._zod.run = (payload, ctx) => {\n if (ctx.skipChecks) {\n return inst._zod.parse(payload, ctx);\n }\n if (ctx.direction === \"backward\") {\n // run canary\n // initial pass (no checks)\n const canary = inst._zod.parse({ value: payload.value, issues: [] }, { ...ctx, skipChecks: true });\n if (canary instanceof Promise) {\n return canary.then((canary) => {\n return handleCanaryResult(canary, payload, ctx);\n });\n }\n return handleCanaryResult(canary, payload, ctx);\n }\n // forward\n const result = inst._zod.parse(payload, ctx);\n if (result instanceof Promise) {\n if (ctx.async === false)\n throw new core.$ZodAsyncError();\n return result.then((result) => runChecks(result, checks, ctx));\n }\n return runChecks(result, checks, ctx);\n };\n }\n inst[\"~standard\"] = {\n validate: (value) => {\n try {\n const r = safeParse(inst, value);\n return r.success ? { value: r.data } : { issues: r.error?.issues };\n }\n catch (_) {\n return safeParseAsync(inst, value).then((r) => (r.success ? { value: r.data } : { issues: r.error?.issues }));\n }\n },\n vendor: \"zod\",\n version: 1,\n };\n});\nexport { clone } from \"./util.js\";\nexport const $ZodString = /*@__PURE__*/ core.$constructor(\"$ZodString\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = [...(inst?._zod.bag?.patterns ?? [])].pop() ?? regexes.string(inst._zod.bag);\n inst._zod.parse = (payload, _) => {\n if (def.coerce)\n try {\n payload.value = String(payload.value);\n }\n catch (_) { }\n if (typeof payload.value === \"string\")\n return payload;\n payload.issues.push({\n expected: \"string\",\n code: \"invalid_type\",\n input: payload.value,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodStringFormat = /*@__PURE__*/ core.$constructor(\"$ZodStringFormat\", (inst, def) => {\n // check initialization must come first\n checks.$ZodCheckStringFormat.init(inst, def);\n $ZodString.init(inst, def);\n});\nexport const $ZodGUID = /*@__PURE__*/ core.$constructor(\"$ZodGUID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.guid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodUUID = /*@__PURE__*/ core.$constructor(\"$ZodUUID\", (inst, def) => {\n if (def.version) {\n const versionMap = {\n v1: 1,\n v2: 2,\n v3: 3,\n v4: 4,\n v5: 5,\n v6: 6,\n v7: 7,\n v8: 8,\n };\n const v = versionMap[def.version];\n if (v === undefined)\n throw new Error(`Invalid UUID version: \"${def.version}\"`);\n def.pattern ?? (def.pattern = regexes.uuid(v));\n }\n else\n def.pattern ?? (def.pattern = regexes.uuid());\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodEmail = /*@__PURE__*/ core.$constructor(\"$ZodEmail\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.email);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodURL = /*@__PURE__*/ core.$constructor(\"$ZodURL\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n try {\n // Trim whitespace from input\n const trimmed = payload.value.trim();\n // @ts-ignore\n const url = new URL(trimmed);\n if (def.hostname) {\n def.hostname.lastIndex = 0;\n if (!def.hostname.test(url.hostname)) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid hostname\",\n pattern: regexes.hostname.source,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n }\n if (def.protocol) {\n def.protocol.lastIndex = 0;\n if (!def.protocol.test(url.protocol.endsWith(\":\") ? url.protocol.slice(0, -1) : url.protocol)) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid protocol\",\n pattern: def.protocol.source,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n }\n // Set the output value based on normalize flag\n if (def.normalize) {\n // Use normalized URL\n payload.value = url.href;\n }\n else {\n // Preserve the original input (trimmed)\n payload.value = trimmed;\n }\n return;\n }\n catch (_) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n };\n});\nexport const $ZodEmoji = /*@__PURE__*/ core.$constructor(\"$ZodEmoji\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.emoji());\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodNanoID = /*@__PURE__*/ core.$constructor(\"$ZodNanoID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.nanoid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodCUID = /*@__PURE__*/ core.$constructor(\"$ZodCUID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.cuid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodCUID2 = /*@__PURE__*/ core.$constructor(\"$ZodCUID2\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.cuid2);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodULID = /*@__PURE__*/ core.$constructor(\"$ZodULID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.ulid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodXID = /*@__PURE__*/ core.$constructor(\"$ZodXID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.xid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodKSUID = /*@__PURE__*/ core.$constructor(\"$ZodKSUID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.ksuid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodISODateTime = /*@__PURE__*/ core.$constructor(\"$ZodISODateTime\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.datetime(def));\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodISODate = /*@__PURE__*/ core.$constructor(\"$ZodISODate\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.date);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodISOTime = /*@__PURE__*/ core.$constructor(\"$ZodISOTime\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.time(def));\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodISODuration = /*@__PURE__*/ core.$constructor(\"$ZodISODuration\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.duration);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodIPv4 = /*@__PURE__*/ core.$constructor(\"$ZodIPv4\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.ipv4);\n $ZodStringFormat.init(inst, def);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = `ipv4`;\n });\n});\nexport const $ZodIPv6 = /*@__PURE__*/ core.$constructor(\"$ZodIPv6\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.ipv6);\n $ZodStringFormat.init(inst, def);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = `ipv6`;\n });\n inst._zod.check = (payload) => {\n try {\n // @ts-ignore\n new URL(`http://[${payload.value}]`);\n // return;\n }\n catch {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"ipv6\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n };\n});\nexport const $ZodCIDRv4 = /*@__PURE__*/ core.$constructor(\"$ZodCIDRv4\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.cidrv4);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodCIDRv6 = /*@__PURE__*/ core.$constructor(\"$ZodCIDRv6\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.cidrv6); // not used for validation\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n const parts = payload.value.split(\"/\");\n try {\n if (parts.length !== 2)\n throw new Error();\n const [address, prefix] = parts;\n if (!prefix)\n throw new Error();\n const prefixNum = Number(prefix);\n if (`${prefixNum}` !== prefix)\n throw new Error();\n if (prefixNum < 0 || prefixNum > 128)\n throw new Error();\n // @ts-ignore\n new URL(`http://[${address}]`);\n }\n catch {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"cidrv6\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n };\n});\n////////////////////////////// ZodBase64 //////////////////////////////\nexport function isValidBase64(data) {\n if (data === \"\")\n return true;\n if (data.length % 4 !== 0)\n return false;\n try {\n // @ts-ignore\n atob(data);\n return true;\n }\n catch {\n return false;\n }\n}\nexport const $ZodBase64 = /*@__PURE__*/ core.$constructor(\"$ZodBase64\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.base64);\n $ZodStringFormat.init(inst, def);\n inst._zod.onattach.push((inst) => {\n inst._zod.bag.contentEncoding = \"base64\";\n });\n inst._zod.check = (payload) => {\n if (isValidBase64(payload.value))\n return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"base64\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\n////////////////////////////// ZodBase64 //////////////////////////////\nexport function isValidBase64URL(data) {\n if (!regexes.base64url.test(data))\n return false;\n const base64 = data.replace(/[-_]/g, (c) => (c === \"-\" ? \"+\" : \"/\"));\n const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, \"=\");\n return isValidBase64(padded);\n}\nexport const $ZodBase64URL = /*@__PURE__*/ core.$constructor(\"$ZodBase64URL\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.base64url);\n $ZodStringFormat.init(inst, def);\n inst._zod.onattach.push((inst) => {\n inst._zod.bag.contentEncoding = \"base64url\";\n });\n inst._zod.check = (payload) => {\n if (isValidBase64URL(payload.value))\n return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"base64url\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodE164 = /*@__PURE__*/ core.$constructor(\"$ZodE164\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.e164);\n $ZodStringFormat.init(inst, def);\n});\n////////////////////////////// ZodJWT //////////////////////////////\nexport function isValidJWT(token, algorithm = null) {\n try {\n const tokensParts = token.split(\".\");\n if (tokensParts.length !== 3)\n return false;\n const [header] = tokensParts;\n if (!header)\n return false;\n // @ts-ignore\n const parsedHeader = JSON.parse(atob(header));\n if (\"typ\" in parsedHeader && parsedHeader?.typ !== \"JWT\")\n return false;\n if (!parsedHeader.alg)\n return false;\n if (algorithm && (!(\"alg\" in parsedHeader) || parsedHeader.alg !== algorithm))\n return false;\n return true;\n }\n catch {\n return false;\n }\n}\nexport const $ZodJWT = /*@__PURE__*/ core.$constructor(\"$ZodJWT\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n if (isValidJWT(payload.value, def.alg))\n return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"jwt\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCustomStringFormat = /*@__PURE__*/ core.$constructor(\"$ZodCustomStringFormat\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n if (def.fn(payload.value))\n return;\n payload.issues.push({\n code: \"invalid_format\",\n format: def.format,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodNumber = /*@__PURE__*/ core.$constructor(\"$ZodNumber\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = inst._zod.bag.pattern ?? regexes.number;\n inst._zod.parse = (payload, _ctx) => {\n if (def.coerce)\n try {\n payload.value = Number(payload.value);\n }\n catch (_) { }\n const input = payload.value;\n if (typeof input === \"number\" && !Number.isNaN(input) && Number.isFinite(input)) {\n return payload;\n }\n const received = typeof input === \"number\"\n ? Number.isNaN(input)\n ? \"NaN\"\n : !Number.isFinite(input)\n ? \"Infinity\"\n : undefined\n : undefined;\n payload.issues.push({\n expected: \"number\",\n code: \"invalid_type\",\n input,\n inst,\n ...(received ? { received } : {}),\n });\n return payload;\n };\n});\nexport const $ZodNumberFormat = /*@__PURE__*/ core.$constructor(\"$ZodNumber\", (inst, def) => {\n checks.$ZodCheckNumberFormat.init(inst, def);\n $ZodNumber.init(inst, def); // no format checksp\n});\nexport const $ZodBoolean = /*@__PURE__*/ core.$constructor(\"$ZodBoolean\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = regexes.boolean;\n inst._zod.parse = (payload, _ctx) => {\n if (def.coerce)\n try {\n payload.value = Boolean(payload.value);\n }\n catch (_) { }\n const input = payload.value;\n if (typeof input === \"boolean\")\n return payload;\n payload.issues.push({\n expected: \"boolean\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodBigInt = /*@__PURE__*/ core.$constructor(\"$ZodBigInt\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = regexes.bigint;\n inst._zod.parse = (payload, _ctx) => {\n if (def.coerce)\n try {\n payload.value = BigInt(payload.value);\n }\n catch (_) { }\n if (typeof payload.value === \"bigint\")\n return payload;\n payload.issues.push({\n expected: \"bigint\",\n code: \"invalid_type\",\n input: payload.value,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodBigIntFormat = /*@__PURE__*/ core.$constructor(\"$ZodBigInt\", (inst, def) => {\n checks.$ZodCheckBigIntFormat.init(inst, def);\n $ZodBigInt.init(inst, def); // no format checks\n});\nexport const $ZodSymbol = /*@__PURE__*/ core.$constructor(\"$ZodSymbol\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (typeof input === \"symbol\")\n return payload;\n payload.issues.push({\n expected: \"symbol\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodUndefined = /*@__PURE__*/ core.$constructor(\"$ZodUndefined\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = regexes.undefined;\n inst._zod.values = new Set([undefined]);\n inst._zod.optin = \"optional\";\n inst._zod.optout = \"optional\";\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (typeof input === \"undefined\")\n return payload;\n payload.issues.push({\n expected: \"undefined\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodNull = /*@__PURE__*/ core.$constructor(\"$ZodNull\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = regexes.null;\n inst._zod.values = new Set([null]);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (input === null)\n return payload;\n payload.issues.push({\n expected: \"null\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodAny = /*@__PURE__*/ core.$constructor(\"$ZodAny\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nexport const $ZodUnknown = /*@__PURE__*/ core.$constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nexport const $ZodNever = /*@__PURE__*/ core.$constructor(\"$ZodNever\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n payload.issues.push({\n expected: \"never\",\n code: \"invalid_type\",\n input: payload.value,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodVoid = /*@__PURE__*/ core.$constructor(\"$ZodVoid\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (typeof input === \"undefined\")\n return payload;\n payload.issues.push({\n expected: \"void\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodDate = /*@__PURE__*/ core.$constructor(\"$ZodDate\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n if (def.coerce) {\n try {\n payload.value = new Date(payload.value);\n }\n catch (_err) { }\n }\n const input = payload.value;\n const isDate = input instanceof Date;\n const isValidDate = isDate && !Number.isNaN(input.getTime());\n if (isValidDate)\n return payload;\n payload.issues.push({\n expected: \"date\",\n code: \"invalid_type\",\n input,\n ...(isDate ? { received: \"Invalid Date\" } : {}),\n inst,\n });\n return payload;\n };\n});\nfunction handleArrayResult(result, final, index) {\n if (result.issues.length) {\n final.issues.push(...util.prefixIssues(index, result.issues));\n }\n final.value[index] = result.value;\n}\nexport const $ZodArray = /*@__PURE__*/ core.$constructor(\"$ZodArray\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n expected: \"array\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n payload.value = Array(input.length);\n const proms = [];\n for (let i = 0; i < input.length; i++) {\n const item = input[i];\n const result = def.element._zod.run({\n value: item,\n issues: [],\n }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => handleArrayResult(result, payload, i)));\n }\n else {\n handleArrayResult(result, payload, i);\n }\n }\n if (proms.length) {\n return Promise.all(proms).then(() => payload);\n }\n return payload; //handleArrayResultsAsync(parseResults, final);\n };\n});\nfunction handlePropertyResult(result, final, key, input) {\n if (result.issues.length) {\n final.issues.push(...util.prefixIssues(key, result.issues));\n }\n if (result.value === undefined) {\n if (key in input) {\n final.value[key] = undefined;\n }\n }\n else {\n final.value[key] = result.value;\n }\n}\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n for (const k of keys) {\n if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) {\n throw new Error(`Invalid element at key \"${k}\": expected a Zod schema`);\n }\n }\n const okeys = util.optionalKeys(def.shape);\n return {\n ...def,\n keys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys),\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst) {\n const unrecognized = [];\n // iterate over input keys\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n for (const key of Object.keys(input)) {\n if (keySet.has(key))\n continue;\n if (t === \"never\") {\n unrecognized.push(key);\n continue;\n }\n const r = _catchall.run({ value: input[key], issues: [] }, ctx);\n if (r instanceof Promise) {\n proms.push(r.then((r) => handlePropertyResult(r, payload, key, input)));\n }\n else {\n handlePropertyResult(r, payload, key, input);\n }\n }\n if (unrecognized.length) {\n payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst,\n });\n }\n if (!proms.length)\n return payload;\n return Promise.all(proms).then(() => {\n return payload;\n });\n}\nexport const $ZodObject = /*@__PURE__*/ core.$constructor(\"$ZodObject\", (inst, def) => {\n // requires cast because technically $ZodObject doesn't extend\n $ZodType.init(inst, def);\n const _normalized = util.cached(() => normalizeDef(def));\n util.defineLazy(inst._zod, \"propValues\", () => {\n const shape = def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n propValues[key] ?? (propValues[key] = new Set());\n for (const v of field.values)\n propValues[key].add(v);\n }\n }\n return propValues;\n });\n const isObject = util.isObject;\n const catchall = def.catchall;\n let value;\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n payload.value = {};\n const proms = [];\n const shape = value.shape;\n for (const key of value.keys) {\n const el = shape[key];\n const r = el._zod.run({ value: input[key], issues: [] }, ctx);\n if (r instanceof Promise) {\n proms.push(r.then((r) => handlePropertyResult(r, payload, key, input)));\n }\n else {\n handlePropertyResult(r, payload, key, input);\n }\n }\n if (!catchall) {\n return proms.length ? Promise.all(proms).then(() => payload) : payload;\n }\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst);\n };\n});\nexport const $ZodObjectJIT = /*@__PURE__*/ core.$constructor(\"$ZodObjectJIT\", (inst, def) => {\n // requires cast because technically $ZodObject doesn't extend\n $ZodObject.init(inst, def);\n const superParse = inst._zod.parse;\n const _normalized = util.cached(() => normalizeDef(def));\n const generateFastpass = (shape) => {\n const doc = new Doc([\"shape\", \"payload\", \"ctx\"]);\n const normalized = _normalized.value;\n const parseStr = (key) => {\n const k = util.esc(key);\n return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;\n };\n doc.write(`const input = payload.value;`);\n const ids = Object.create(null);\n let counter = 0;\n for (const key of normalized.keys) {\n ids[key] = `key_${counter++}`;\n }\n // A: preserve key order {\n doc.write(`const newResult = {};`);\n for (const key of normalized.keys) {\n const id = ids[key];\n const k = util.esc(key);\n doc.write(`const ${id} = ${parseStr(key)};`);\n doc.write(`\n if (${id}.issues.length) {\n payload.issues = payload.issues.concat(${id}.issues.map(iss => ({\n ...iss,\n path: iss.path ? [${k}, ...iss.path] : [${k}]\n })));\n }\n \n \n if (${id}.value === undefined) {\n if (${k} in input) {\n newResult[${k}] = undefined;\n }\n } else {\n newResult[${k}] = ${id}.value;\n }\n \n `);\n }\n doc.write(`payload.value = newResult;`);\n doc.write(`return payload;`);\n const fn = doc.compile();\n return (payload, ctx) => fn(shape, payload, ctx);\n };\n let fastpass;\n const isObject = util.isObject;\n const jit = !core.globalConfig.jitless;\n const allowsEval = util.allowsEval;\n const fastEnabled = jit && allowsEval.value; // && !def.catchall;\n const catchall = def.catchall;\n let value;\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n if (jit && fastEnabled && ctx?.async === false && ctx.jitless !== true) {\n // always synchronous\n if (!fastpass)\n fastpass = generateFastpass(def.shape);\n payload = fastpass(payload, ctx);\n if (!catchall)\n return payload;\n return handleCatchall([], input, payload, ctx, value, inst);\n }\n return superParse(payload, ctx);\n };\n});\nfunction handleUnionResults(results, final, inst, ctx) {\n for (const result of results) {\n if (result.issues.length === 0) {\n final.value = result.value;\n return final;\n }\n }\n const nonaborted = results.filter((r) => !util.aborted(r));\n if (nonaborted.length === 1) {\n final.value = nonaborted[0].value;\n return nonaborted[0];\n }\n final.issues.push({\n code: \"invalid_union\",\n input: final.value,\n inst,\n errors: results.map((result) => result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))),\n });\n return final;\n}\nexport const $ZodUnion = /*@__PURE__*/ core.$constructor(\"$ZodUnion\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"optin\", () => def.options.some((o) => o._zod.optin === \"optional\") ? \"optional\" : undefined);\n util.defineLazy(inst._zod, \"optout\", () => def.options.some((o) => o._zod.optout === \"optional\") ? \"optional\" : undefined);\n util.defineLazy(inst._zod, \"values\", () => {\n if (def.options.every((o) => o._zod.values)) {\n return new Set(def.options.flatMap((option) => Array.from(option._zod.values)));\n }\n return undefined;\n });\n util.defineLazy(inst._zod, \"pattern\", () => {\n if (def.options.every((o) => o._zod.pattern)) {\n const patterns = def.options.map((o) => o._zod.pattern);\n return new RegExp(`^(${patterns.map((p) => util.cleanRegex(p.source)).join(\"|\")})$`);\n }\n return undefined;\n });\n const single = def.options.length === 1;\n const first = def.options[0]._zod.run;\n inst._zod.parse = (payload, ctx) => {\n if (single) {\n return first(payload, ctx);\n }\n let async = false;\n const results = [];\n for (const option of def.options) {\n const result = option._zod.run({\n value: payload.value,\n issues: [],\n }, ctx);\n if (result instanceof Promise) {\n results.push(result);\n async = true;\n }\n else {\n if (result.issues.length === 0)\n return result;\n results.push(result);\n }\n }\n if (!async)\n return handleUnionResults(results, payload, inst, ctx);\n return Promise.all(results).then((results) => {\n return handleUnionResults(results, payload, inst, ctx);\n });\n };\n});\nexport const $ZodDiscriminatedUnion = \n/*@__PURE__*/\ncore.$constructor(\"$ZodDiscriminatedUnion\", (inst, def) => {\n $ZodUnion.init(inst, def);\n const _super = inst._zod.parse;\n util.defineLazy(inst._zod, \"propValues\", () => {\n const propValues = {};\n for (const option of def.options) {\n const pv = option._zod.propValues;\n if (!pv || Object.keys(pv).length === 0)\n throw new Error(`Invalid discriminated union option at index \"${def.options.indexOf(option)}\"`);\n for (const [k, v] of Object.entries(pv)) {\n if (!propValues[k])\n propValues[k] = new Set();\n for (const val of v) {\n propValues[k].add(val);\n }\n }\n }\n return propValues;\n });\n const disc = util.cached(() => {\n const opts = def.options;\n const map = new Map();\n for (const o of opts) {\n const values = o._zod.propValues?.[def.discriminator];\n if (!values || values.size === 0)\n throw new Error(`Invalid discriminated union option at index \"${def.options.indexOf(o)}\"`);\n for (const v of values) {\n if (map.has(v)) {\n throw new Error(`Duplicate discriminator value \"${String(v)}\"`);\n }\n map.set(v, o);\n }\n }\n return map;\n });\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!util.isObject(input)) {\n payload.issues.push({\n code: \"invalid_type\",\n expected: \"object\",\n input,\n inst,\n });\n return payload;\n }\n const opt = disc.value.get(input?.[def.discriminator]);\n if (opt) {\n return opt._zod.run(payload, ctx);\n }\n if (def.unionFallback) {\n return _super(payload, ctx);\n }\n // no matching discriminator\n payload.issues.push({\n code: \"invalid_union\",\n errors: [],\n note: \"No matching discriminator\",\n discriminator: def.discriminator,\n input,\n path: [def.discriminator],\n inst,\n });\n return payload;\n };\n});\nexport const $ZodIntersection = /*@__PURE__*/ core.$constructor(\"$ZodIntersection\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n const left = def.left._zod.run({ value: input, issues: [] }, ctx);\n const right = def.right._zod.run({ value: input, issues: [] }, ctx);\n const async = left instanceof Promise || right instanceof Promise;\n if (async) {\n return Promise.all([left, right]).then(([left, right]) => {\n return handleIntersectionResults(payload, left, right);\n });\n }\n return handleIntersectionResults(payload, left, right);\n };\n});\nfunction mergeValues(a, b) {\n // const aType = parse.t(a);\n // const bType = parse.t(b);\n if (a === b) {\n return { valid: true, data: a };\n }\n if (a instanceof Date && b instanceof Date && +a === +b) {\n return { valid: true, data: a };\n }\n if (util.isPlainObject(a) && util.isPlainObject(b)) {\n const bKeys = Object.keys(b);\n const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);\n const newObj = { ...a, ...b };\n for (const key of sharedKeys) {\n const sharedValue = mergeValues(a[key], b[key]);\n if (!sharedValue.valid) {\n return {\n valid: false,\n mergeErrorPath: [key, ...sharedValue.mergeErrorPath],\n };\n }\n newObj[key] = sharedValue.data;\n }\n return { valid: true, data: newObj };\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) {\n return { valid: false, mergeErrorPath: [] };\n }\n const newArray = [];\n for (let index = 0; index < a.length; index++) {\n const itemA = a[index];\n const itemB = b[index];\n const sharedValue = mergeValues(itemA, itemB);\n if (!sharedValue.valid) {\n return {\n valid: false,\n mergeErrorPath: [index, ...sharedValue.mergeErrorPath],\n };\n }\n newArray.push(sharedValue.data);\n }\n return { valid: true, data: newArray };\n }\n return { valid: false, mergeErrorPath: [] };\n}\nfunction handleIntersectionResults(result, left, right) {\n if (left.issues.length) {\n result.issues.push(...left.issues);\n }\n if (right.issues.length) {\n result.issues.push(...right.issues);\n }\n if (util.aborted(result))\n return result;\n const merged = mergeValues(left.value, right.value);\n if (!merged.valid) {\n throw new Error(`Unmergable intersection. Error path: ` + `${JSON.stringify(merged.mergeErrorPath)}`);\n }\n result.value = merged.data;\n return result;\n}\nexport const $ZodTuple = /*@__PURE__*/ core.$constructor(\"$ZodTuple\", (inst, def) => {\n $ZodType.init(inst, def);\n const items = def.items;\n const optStart = items.length - [...items].reverse().findIndex((item) => item._zod.optin !== \"optional\");\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n input,\n inst,\n expected: \"tuple\",\n code: \"invalid_type\",\n });\n return payload;\n }\n payload.value = [];\n const proms = [];\n if (!def.rest) {\n const tooBig = input.length > items.length;\n const tooSmall = input.length < optStart - 1;\n if (tooBig || tooSmall) {\n payload.issues.push({\n ...(tooBig ? { code: \"too_big\", maximum: items.length } : { code: \"too_small\", minimum: items.length }),\n input,\n inst,\n origin: \"array\",\n });\n return payload;\n }\n }\n let i = -1;\n for (const item of items) {\n i++;\n if (i >= input.length)\n if (i >= optStart)\n continue;\n const result = item._zod.run({\n value: input[i],\n issues: [],\n }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => handleTupleResult(result, payload, i)));\n }\n else {\n handleTupleResult(result, payload, i);\n }\n }\n if (def.rest) {\n const rest = input.slice(items.length);\n for (const el of rest) {\n i++;\n const result = def.rest._zod.run({\n value: el,\n issues: [],\n }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => handleTupleResult(result, payload, i)));\n }\n else {\n handleTupleResult(result, payload, i);\n }\n }\n }\n if (proms.length)\n return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleTupleResult(result, final, index) {\n if (result.issues.length) {\n final.issues.push(...util.prefixIssues(index, result.issues));\n }\n final.value[index] = result.value;\n}\nexport const $ZodRecord = /*@__PURE__*/ core.$constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!util.isPlainObject(input)) {\n payload.issues.push({\n expected: \"record\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n const proms = [];\n if (def.keyType._zod.values) {\n const values = def.keyType._zod.values;\n payload.value = {};\n for (const key of values) {\n if (typeof key === \"string\" || typeof key === \"number\" || typeof key === \"symbol\") {\n const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(key, result.issues));\n }\n payload.value[key] = result.value;\n }));\n }\n else {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(key, result.issues));\n }\n payload.value[key] = result.value;\n }\n }\n }\n let unrecognized;\n for (const key in input) {\n if (!values.has(key)) {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n }\n if (unrecognized && unrecognized.length > 0) {\n payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n });\n }\n }\n else {\n payload.value = {};\n for (const key of Reflect.ownKeys(input)) {\n if (key === \"__proto__\")\n continue;\n const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);\n if (keyResult instanceof Promise) {\n throw new Error(\"Async schemas not supported in object keys currently\");\n }\n if (keyResult.issues.length) {\n payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n input: key,\n path: [key],\n inst,\n });\n payload.value[keyResult.value] = keyResult.value;\n continue;\n }\n const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(key, result.issues));\n }\n payload.value[keyResult.value] = result.value;\n }));\n }\n else {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(key, result.issues));\n }\n payload.value[keyResult.value] = result.value;\n }\n }\n }\n if (proms.length) {\n return Promise.all(proms).then(() => payload);\n }\n return payload;\n };\n});\nexport const $ZodMap = /*@__PURE__*/ core.$constructor(\"$ZodMap\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!(input instanceof Map)) {\n payload.issues.push({\n expected: \"map\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n const proms = [];\n payload.value = new Map();\n for (const [key, value] of input) {\n const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);\n const valueResult = def.valueType._zod.run({ value: value, issues: [] }, ctx);\n if (keyResult instanceof Promise || valueResult instanceof Promise) {\n proms.push(Promise.all([keyResult, valueResult]).then(([keyResult, valueResult]) => {\n handleMapResult(keyResult, valueResult, payload, key, input, inst, ctx);\n }));\n }\n else {\n handleMapResult(keyResult, valueResult, payload, key, input, inst, ctx);\n }\n }\n if (proms.length)\n return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {\n if (keyResult.issues.length) {\n if (util.propertyKeyTypes.has(typeof key)) {\n final.issues.push(...util.prefixIssues(key, keyResult.issues));\n }\n else {\n final.issues.push({\n code: \"invalid_key\",\n origin: \"map\",\n input,\n inst,\n issues: keyResult.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n });\n }\n }\n if (valueResult.issues.length) {\n if (util.propertyKeyTypes.has(typeof key)) {\n final.issues.push(...util.prefixIssues(key, valueResult.issues));\n }\n else {\n final.issues.push({\n origin: \"map\",\n code: \"invalid_element\",\n input,\n inst,\n key: key,\n issues: valueResult.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n });\n }\n }\n final.value.set(keyResult.value, valueResult.value);\n}\nexport const $ZodSet = /*@__PURE__*/ core.$constructor(\"$ZodSet\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!(input instanceof Set)) {\n payload.issues.push({\n input,\n inst,\n expected: \"set\",\n code: \"invalid_type\",\n });\n return payload;\n }\n const proms = [];\n payload.value = new Set();\n for (const item of input) {\n const result = def.valueType._zod.run({ value: item, issues: [] }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => handleSetResult(result, payload)));\n }\n else\n handleSetResult(result, payload);\n }\n if (proms.length)\n return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleSetResult(result, final) {\n if (result.issues.length) {\n final.issues.push(...result.issues);\n }\n final.value.add(result.value);\n}\nexport const $ZodEnum = /*@__PURE__*/ core.$constructor(\"$ZodEnum\", (inst, def) => {\n $ZodType.init(inst, def);\n const values = util.getEnumValues(def.entries);\n const valuesSet = new Set(values);\n inst._zod.values = valuesSet;\n inst._zod.pattern = new RegExp(`^(${values\n .filter((k) => util.propertyKeyTypes.has(typeof k))\n .map((o) => (typeof o === \"string\" ? util.escapeRegex(o) : o.toString()))\n .join(\"|\")})$`);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (valuesSet.has(input)) {\n return payload;\n }\n payload.issues.push({\n code: \"invalid_value\",\n values,\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodLiteral = /*@__PURE__*/ core.$constructor(\"$ZodLiteral\", (inst, def) => {\n $ZodType.init(inst, def);\n if (def.values.length === 0) {\n throw new Error(\"Cannot create literal schema with no valid values\");\n }\n inst._zod.values = new Set(def.values);\n inst._zod.pattern = new RegExp(`^(${def.values\n .map((o) => (typeof o === \"string\" ? util.escapeRegex(o) : o ? util.escapeRegex(o.toString()) : String(o)))\n .join(\"|\")})$`);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (inst._zod.values.has(input)) {\n return payload;\n }\n payload.issues.push({\n code: \"invalid_value\",\n values: def.values,\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodFile = /*@__PURE__*/ core.$constructor(\"$ZodFile\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n // @ts-ignore\n if (input instanceof File)\n return payload;\n payload.issues.push({\n expected: \"file\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodTransform = /*@__PURE__*/ core.$constructor(\"$ZodTransform\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n throw new core.$ZodEncodeError(inst.constructor.name);\n }\n const _out = def.transform(payload.value, payload);\n if (ctx.async) {\n const output = _out instanceof Promise ? _out : Promise.resolve(_out);\n return output.then((output) => {\n payload.value = output;\n return payload;\n });\n }\n if (_out instanceof Promise) {\n throw new core.$ZodAsyncError();\n }\n payload.value = _out;\n return payload;\n };\n});\nfunction handleOptionalResult(result, input) {\n if (result.issues.length && input === undefined) {\n return { issues: [], value: undefined };\n }\n return result;\n}\nexport const $ZodOptional = /*@__PURE__*/ core.$constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n inst._zod.optout = \"optional\";\n util.defineLazy(inst._zod, \"values\", () => {\n return def.innerType._zod.values ? new Set([...def.innerType._zod.values, undefined]) : undefined;\n });\n util.defineLazy(inst._zod, \"pattern\", () => {\n const pattern = def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${util.cleanRegex(pattern.source)})?$`) : undefined;\n });\n inst._zod.parse = (payload, ctx) => {\n if (def.innerType._zod.optin === \"optional\") {\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise)\n return result.then((r) => handleOptionalResult(r, payload.value));\n return handleOptionalResult(result, payload.value);\n }\n if (payload.value === undefined) {\n return payload;\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nexport const $ZodNullable = /*@__PURE__*/ core.$constructor(\"$ZodNullable\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"optin\", () => def.innerType._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n util.defineLazy(inst._zod, \"pattern\", () => {\n const pattern = def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${util.cleanRegex(pattern.source)}|null)$`) : undefined;\n });\n util.defineLazy(inst._zod, \"values\", () => {\n return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : undefined;\n });\n inst._zod.parse = (payload, ctx) => {\n // Forward direction (decode): allow null to pass through\n if (payload.value === null)\n return payload;\n return def.innerType._zod.run(payload, ctx);\n };\n});\nexport const $ZodDefault = /*@__PURE__*/ core.$constructor(\"$ZodDefault\", (inst, def) => {\n $ZodType.init(inst, def);\n // inst._zod.qin = \"true\";\n inst._zod.optin = \"optional\";\n util.defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n return def.innerType._zod.run(payload, ctx);\n }\n // Forward direction (decode): apply defaults for undefined input\n if (payload.value === undefined) {\n payload.value = def.defaultValue;\n /**\n * $ZodDefault returns the default value immediately in forward direction.\n * It doesn't pass the default value into the validator (\"prefault\"). There's no reason to pass the default value through validation. The validity of the default is enforced by TypeScript statically. Otherwise, it's the responsibility of the user to ensure the default is valid. In the case of pipes with divergent in/out types, you can specify the default on the `in` schema of your ZodPipe to set a \"prefault\" for the pipe. */\n return payload;\n }\n // Forward direction: continue with default handling\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then((result) => handleDefaultResult(result, def));\n }\n return handleDefaultResult(result, def);\n };\n});\nfunction handleDefaultResult(payload, def) {\n if (payload.value === undefined) {\n payload.value = def.defaultValue;\n }\n return payload;\n}\nexport const $ZodPrefault = /*@__PURE__*/ core.$constructor(\"$ZodPrefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n util.defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n return def.innerType._zod.run(payload, ctx);\n }\n // Forward direction (decode): apply prefault for undefined input\n if (payload.value === undefined) {\n payload.value = def.defaultValue;\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nexport const $ZodNonOptional = /*@__PURE__*/ core.$constructor(\"$ZodNonOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"values\", () => {\n const v = def.innerType._zod.values;\n return v ? new Set([...v].filter((x) => x !== undefined)) : undefined;\n });\n inst._zod.parse = (payload, ctx) => {\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then((result) => handleNonOptionalResult(result, inst));\n }\n return handleNonOptionalResult(result, inst);\n };\n});\nfunction handleNonOptionalResult(payload, inst) {\n if (!payload.issues.length && payload.value === undefined) {\n payload.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: payload.value,\n inst,\n });\n }\n return payload;\n}\nexport const $ZodSuccess = /*@__PURE__*/ core.$constructor(\"$ZodSuccess\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n throw new core.$ZodEncodeError(\"ZodSuccess\");\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then((result) => {\n payload.value = result.issues.length === 0;\n return payload;\n });\n }\n payload.value = result.issues.length === 0;\n return payload;\n };\n});\nexport const $ZodCatch = /*@__PURE__*/ core.$constructor(\"$ZodCatch\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"optin\", () => def.innerType._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n util.defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n return def.innerType._zod.run(payload, ctx);\n }\n // Forward direction (decode): apply catch logic\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then((result) => {\n payload.value = result.value;\n if (result.issues.length) {\n payload.value = def.catchValue({\n ...payload,\n error: {\n issues: result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n },\n input: payload.value,\n });\n payload.issues = [];\n }\n return payload;\n });\n }\n payload.value = result.value;\n if (result.issues.length) {\n payload.value = def.catchValue({\n ...payload,\n error: {\n issues: result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n },\n input: payload.value,\n });\n payload.issues = [];\n }\n return payload;\n };\n});\nexport const $ZodNaN = /*@__PURE__*/ core.$constructor(\"$ZodNaN\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n if (typeof payload.value !== \"number\" || !Number.isNaN(payload.value)) {\n payload.issues.push({\n input: payload.value,\n inst,\n expected: \"nan\",\n code: \"invalid_type\",\n });\n return payload;\n }\n return payload;\n };\n});\nexport const $ZodPipe = /*@__PURE__*/ core.$constructor(\"$ZodPipe\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"values\", () => def.in._zod.values);\n util.defineLazy(inst._zod, \"optin\", () => def.in._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.out._zod.optout);\n util.defineLazy(inst._zod, \"propValues\", () => def.in._zod.propValues);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n const right = def.out._zod.run(payload, ctx);\n if (right instanceof Promise) {\n return right.then((right) => handlePipeResult(right, def.in, ctx));\n }\n return handlePipeResult(right, def.in, ctx);\n }\n const left = def.in._zod.run(payload, ctx);\n if (left instanceof Promise) {\n return left.then((left) => handlePipeResult(left, def.out, ctx));\n }\n return handlePipeResult(left, def.out, ctx);\n };\n});\nfunction handlePipeResult(left, next, ctx) {\n if (left.issues.length) {\n // prevent further checks\n left.aborted = true;\n return left;\n }\n return next._zod.run({ value: left.value, issues: left.issues }, ctx);\n}\nexport const $ZodCodec = /*@__PURE__*/ core.$constructor(\"$ZodCodec\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"values\", () => def.in._zod.values);\n util.defineLazy(inst._zod, \"optin\", () => def.in._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.out._zod.optout);\n util.defineLazy(inst._zod, \"propValues\", () => def.in._zod.propValues);\n inst._zod.parse = (payload, ctx) => {\n const direction = ctx.direction || \"forward\";\n if (direction === \"forward\") {\n const left = def.in._zod.run(payload, ctx);\n if (left instanceof Promise) {\n return left.then((left) => handleCodecAResult(left, def, ctx));\n }\n return handleCodecAResult(left, def, ctx);\n }\n else {\n const right = def.out._zod.run(payload, ctx);\n if (right instanceof Promise) {\n return right.then((right) => handleCodecAResult(right, def, ctx));\n }\n return handleCodecAResult(right, def, ctx);\n }\n };\n});\nfunction handleCodecAResult(result, def, ctx) {\n if (result.issues.length) {\n // prevent further checks\n result.aborted = true;\n return result;\n }\n const direction = ctx.direction || \"forward\";\n if (direction === \"forward\") {\n const transformed = def.transform(result.value, result);\n if (transformed instanceof Promise) {\n return transformed.then((value) => handleCodecTxResult(result, value, def.out, ctx));\n }\n return handleCodecTxResult(result, transformed, def.out, ctx);\n }\n else {\n const transformed = def.reverseTransform(result.value, result);\n if (transformed instanceof Promise) {\n return transformed.then((value) => handleCodecTxResult(result, value, def.in, ctx));\n }\n return handleCodecTxResult(result, transformed, def.in, ctx);\n }\n}\nfunction handleCodecTxResult(left, value, nextSchema, ctx) {\n // Check if transform added any issues\n if (left.issues.length) {\n left.aborted = true;\n return left;\n }\n return nextSchema._zod.run({ value, issues: left.issues }, ctx);\n}\nexport const $ZodReadonly = /*@__PURE__*/ core.$constructor(\"$ZodReadonly\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"propValues\", () => def.innerType._zod.propValues);\n util.defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n util.defineLazy(inst._zod, \"optin\", () => def.innerType._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n return def.innerType._zod.run(payload, ctx);\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then(handleReadonlyResult);\n }\n return handleReadonlyResult(result);\n };\n});\nfunction handleReadonlyResult(payload) {\n payload.value = Object.freeze(payload.value);\n return payload;\n}\nexport const $ZodTemplateLiteral = /*@__PURE__*/ core.$constructor(\"$ZodTemplateLiteral\", (inst, def) => {\n $ZodType.init(inst, def);\n const regexParts = [];\n for (const part of def.parts) {\n if (typeof part === \"object\" && part !== null) {\n // is Zod schema\n if (!part._zod.pattern) {\n // if (!source)\n throw new Error(`Invalid template literal part, no pattern found: ${[...part._zod.traits].shift()}`);\n }\n const source = part._zod.pattern instanceof RegExp ? part._zod.pattern.source : part._zod.pattern;\n if (!source)\n throw new Error(`Invalid template literal part: ${part._zod.traits}`);\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n regexParts.push(source.slice(start, end));\n }\n else if (part === null || util.primitiveTypes.has(typeof part)) {\n regexParts.push(util.escapeRegex(`${part}`));\n }\n else {\n throw new Error(`Invalid template literal part: ${part}`);\n }\n }\n inst._zod.pattern = new RegExp(`^${regexParts.join(\"\")}$`);\n inst._zod.parse = (payload, _ctx) => {\n if (typeof payload.value !== \"string\") {\n payload.issues.push({\n input: payload.value,\n inst,\n expected: \"template_literal\",\n code: \"invalid_type\",\n });\n return payload;\n }\n inst._zod.pattern.lastIndex = 0;\n if (!inst._zod.pattern.test(payload.value)) {\n payload.issues.push({\n input: payload.value,\n inst,\n code: \"invalid_format\",\n format: def.format ?? \"template_literal\",\n pattern: inst._zod.pattern.source,\n });\n return payload;\n }\n return payload;\n };\n});\nexport const $ZodFunction = /*@__PURE__*/ core.$constructor(\"$ZodFunction\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._def = def;\n inst._zod.def = def;\n inst.implement = (func) => {\n if (typeof func !== \"function\") {\n throw new Error(\"implement() must be called with a function\");\n }\n return function (...args) {\n const parsedArgs = inst._def.input ? parse(inst._def.input, args) : args;\n const result = Reflect.apply(func, this, parsedArgs);\n if (inst._def.output) {\n return parse(inst._def.output, result);\n }\n return result;\n };\n };\n inst.implementAsync = (func) => {\n if (typeof func !== \"function\") {\n throw new Error(\"implementAsync() must be called with a function\");\n }\n return async function (...args) {\n const parsedArgs = inst._def.input ? await parseAsync(inst._def.input, args) : args;\n const result = await Reflect.apply(func, this, parsedArgs);\n if (inst._def.output) {\n return await parseAsync(inst._def.output, result);\n }\n return result;\n };\n };\n inst._zod.parse = (payload, _ctx) => {\n if (typeof payload.value !== \"function\") {\n payload.issues.push({\n code: \"invalid_type\",\n expected: \"function\",\n input: payload.value,\n inst,\n });\n return payload;\n }\n // Check if output is a promise type to determine if we should use async implementation\n const hasPromiseOutput = inst._def.output && inst._def.output._zod.def.type === \"promise\";\n if (hasPromiseOutput) {\n payload.value = inst.implementAsync(payload.value);\n }\n else {\n payload.value = inst.implement(payload.value);\n }\n return payload;\n };\n inst.input = (...args) => {\n const F = inst.constructor;\n if (Array.isArray(args[0])) {\n return new F({\n type: \"function\",\n input: new $ZodTuple({\n type: \"tuple\",\n items: args[0],\n rest: args[1],\n }),\n output: inst._def.output,\n });\n }\n return new F({\n type: \"function\",\n input: args[0],\n output: inst._def.output,\n });\n };\n inst.output = (output) => {\n const F = inst.constructor;\n return new F({\n type: \"function\",\n input: inst._def.input,\n output,\n });\n };\n return inst;\n});\nexport const $ZodPromise = /*@__PURE__*/ core.$constructor(\"$ZodPromise\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n return Promise.resolve(payload.value).then((inner) => def.innerType._zod.run({ value: inner, issues: [] }, ctx));\n };\n});\nexport const $ZodLazy = /*@__PURE__*/ core.$constructor(\"$ZodLazy\", (inst, def) => {\n $ZodType.init(inst, def);\n // let _innerType!: any;\n // util.defineLazy(def, \"getter\", () => {\n // if (!_innerType) {\n // _innerType = def.getter();\n // }\n // return () => _innerType;\n // });\n util.defineLazy(inst._zod, \"innerType\", () => def.getter());\n util.defineLazy(inst._zod, \"pattern\", () => inst._zod.innerType._zod.pattern);\n util.defineLazy(inst._zod, \"propValues\", () => inst._zod.innerType._zod.propValues);\n util.defineLazy(inst._zod, \"optin\", () => inst._zod.innerType._zod.optin ?? undefined);\n util.defineLazy(inst._zod, \"optout\", () => inst._zod.innerType._zod.optout ?? undefined);\n inst._zod.parse = (payload, ctx) => {\n const inner = inst._zod.innerType;\n return inner._zod.run(payload, ctx);\n };\n});\nexport const $ZodCustom = /*@__PURE__*/ core.$constructor(\"$ZodCustom\", (inst, def) => {\n checks.$ZodCheck.init(inst, def);\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _) => {\n return payload;\n };\n inst._zod.check = (payload) => {\n const input = payload.value;\n const r = def.fn(input);\n if (r instanceof Promise) {\n return r.then((r) => handleRefineResult(r, payload, input, inst));\n }\n handleRefineResult(r, payload, input, inst);\n return;\n };\n});\nfunction handleRefineResult(result, payload, input, inst) {\n if (!result) {\n const _iss = {\n code: \"custom\",\n input,\n inst, // incorporates params.error into issue reporting\n path: [...(inst._zod.def.path ?? [])], // incorporates params.error into issue reporting\n continue: !inst._zod.def.abort,\n // params: inst._zod.def.params,\n };\n if (inst._zod.def.params)\n _iss.params = inst._zod.def.params;\n payload.issues.push(util.issue(_iss));\n }\n}\n","export { default as ar } from \"./ar.js\";\nexport { default as az } from \"./az.js\";\nexport { default as be } from \"./be.js\";\nexport { default as ca } from \"./ca.js\";\nexport { default as cs } from \"./cs.js\";\nexport { default as da } from \"./da.js\";\nexport { default as de } from \"./de.js\";\nexport { default as en } from \"./en.js\";\nexport { default as eo } from \"./eo.js\";\nexport { default as es } from \"./es.js\";\nexport { default as fa } from \"./fa.js\";\nexport { default as fi } from \"./fi.js\";\nexport { default as fr } from \"./fr.js\";\nexport { default as frCA } from \"./fr-CA.js\";\nexport { default as he } from \"./he.js\";\nexport { default as hu } from \"./hu.js\";\nexport { default as id } from \"./id.js\";\nexport { default as is } from \"./is.js\";\nexport { default as it } from \"./it.js\";\nexport { default as ja } from \"./ja.js\";\nexport { default as ka } from \"./ka.js\";\nexport { default as kh } from \"./kh.js\";\nexport { default as km } from \"./km.js\";\nexport { default as ko } from \"./ko.js\";\nexport { default as lt } from \"./lt.js\";\nexport { default as mk } from \"./mk.js\";\nexport { default as ms } from \"./ms.js\";\nexport { default as nl } from \"./nl.js\";\nexport { default as no } from \"./no.js\";\nexport { default as ota } from \"./ota.js\";\nexport { default as ps } from \"./ps.js\";\nexport { default as pl } from \"./pl.js\";\nexport { default as pt } from \"./pt.js\";\nexport { default as ru } from \"./ru.js\";\nexport { default as sl } from \"./sl.js\";\nexport { default as sv } from \"./sv.js\";\nexport { default as ta } from \"./ta.js\";\nexport { default as th } from \"./th.js\";\nexport { default as tr } from \"./tr.js\";\nexport { default as ua } from \"./ua.js\";\nexport { default as uk } from \"./uk.js\";\nexport { default as ur } from \"./ur.js\";\nexport { default as vi } from \"./vi.js\";\nexport { default as zhCN } from \"./zh-CN.js\";\nexport { default as zhTW } from \"./zh-TW.js\";\nexport { default as yo } from \"./yo.js\";\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"حرف\", verb: \"أن يحوي\" },\n file: { unit: \"بايت\", verb: \"أن يحوي\" },\n array: { unit: \"عنصر\", verb: \"أن يحوي\" },\n set: { unit: \"عنصر\", verb: \"أن يحوي\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"مدخل\",\n email: \"بريد إلكتروني\",\n url: \"رابط\",\n emoji: \"إيموجي\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"تاريخ ووقت بمعيار ISO\",\n date: \"تاريخ بمعيار ISO\",\n time: \"وقت بمعيار ISO\",\n duration: \"مدة بمعيار ISO\",\n ipv4: \"عنوان IPv4\",\n ipv6: \"عنوان IPv6\",\n cidrv4: \"مدى عناوين بصيغة IPv4\",\n cidrv6: \"مدى عناوين بصيغة IPv6\",\n base64: \"نَص بترميز base64-encoded\",\n base64url: \"نَص بترميز base64url-encoded\",\n json_string: \"نَص على هيئة JSON\",\n e164: \"رقم هاتف بمعيار E.164\",\n jwt: \"JWT\",\n template_literal: \"مدخل\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `مدخلات غير مقبولة: يفترض إدخال ${issue.expected}، ولكن تم إدخال ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `مدخلات غير مقبولة: يفترض إدخال ${util.stringifyPrimitive(issue.values[0])}`;\n return `اختيار غير مقبول: يتوقع انتقاء أحد هذه الخيارات: ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return ` أكبر من اللازم: يفترض أن تكون ${issue.origin ?? \"القيمة\"} ${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"عنصر\"}`;\n return `أكبر من اللازم: يفترض أن تكون ${issue.origin ?? \"القيمة\"} ${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `أصغر من اللازم: يفترض لـ ${issue.origin} أن يكون ${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `أصغر من اللازم: يفترض لـ ${issue.origin} أن يكون ${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `نَص غير مقبول: يجب أن يبدأ بـ \"${issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `نَص غير مقبول: يجب أن ينتهي بـ \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `نَص غير مقبول: يجب أن يتضمَّن \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `نَص غير مقبول: يجب أن يطابق النمط ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} غير مقبول`;\n }\n case \"not_multiple_of\":\n return `رقم غير مقبول: يجب أن يكون من مضاعفات ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `معرف${issue.keys.length > 1 ? \"ات\" : \"\"} غريب${issue.keys.length > 1 ? \"ة\" : \"\"}: ${util.joinValues(issue.keys, \"، \")}`;\n case \"invalid_key\":\n return `معرف غير مقبول في ${issue.origin}`;\n case \"invalid_union\":\n return \"مدخل غير مقبول\";\n case \"invalid_element\":\n return `مدخل غير مقبول في ${issue.origin}`;\n default:\n return \"مدخل غير مقبول\";\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"simvol\", verb: \"olmalıdır\" },\n file: { unit: \"bayt\", verb: \"olmalıdır\" },\n array: { unit: \"element\", verb: \"olmalıdır\" },\n set: { unit: \"element\", verb: \"olmalıdır\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Yanlış dəyər: gözlənilən ${issue.expected}, daxil olan ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Yanlış dəyər: gözlənilən ${util.stringifyPrimitive(issue.values[0])}`;\n return `Yanlış seçim: aşağıdakılardan biri olmalıdır: ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Çox böyük: gözlənilən ${issue.origin ?? \"dəyər\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"element\"}`;\n return `Çox böyük: gözlənilən ${issue.origin ?? \"dəyər\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Çox kiçik: gözlənilən ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Çox kiçik: gözlənilən ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Yanlış mətn: \"${_issue.prefix}\" ilə başlamalıdır`;\n if (_issue.format === \"ends_with\")\n return `Yanlış mətn: \"${_issue.suffix}\" ilə bitməlidir`;\n if (_issue.format === \"includes\")\n return `Yanlış mətn: \"${_issue.includes}\" daxil olmalıdır`;\n if (_issue.format === \"regex\")\n return `Yanlış mətn: ${_issue.pattern} şablonuna uyğun olmalıdır`;\n return `Yanlış ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Yanlış ədəd: ${issue.divisor} ilə bölünə bilən olmalıdır`;\n case \"unrecognized_keys\":\n return `Tanınmayan açar${issue.keys.length > 1 ? \"lar\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} daxilində yanlış açar`;\n case \"invalid_union\":\n return \"Yanlış dəyər\";\n case \"invalid_element\":\n return `${issue.origin} daxilində yanlış dəyər`;\n default:\n return `Yanlış dəyər`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nfunction getBelarusianPlural(count, one, few, many) {\n const absCount = Math.abs(count);\n const lastDigit = absCount % 10;\n const lastTwoDigits = absCount % 100;\n if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {\n return many;\n }\n if (lastDigit === 1) {\n return one;\n }\n if (lastDigit >= 2 && lastDigit <= 4) {\n return few;\n }\n return many;\n}\nconst error = () => {\n const Sizable = {\n string: {\n unit: {\n one: \"сімвал\",\n few: \"сімвалы\",\n many: \"сімвалаў\",\n },\n verb: \"мець\",\n },\n array: {\n unit: {\n one: \"элемент\",\n few: \"элементы\",\n many: \"элементаў\",\n },\n verb: \"мець\",\n },\n set: {\n unit: {\n one: \"элемент\",\n few: \"элементы\",\n many: \"элементаў\",\n },\n verb: \"мець\",\n },\n file: {\n unit: {\n one: \"байт\",\n few: \"байты\",\n many: \"байтаў\",\n },\n verb: \"мець\",\n },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"лік\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"масіў\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"увод\",\n email: \"email адрас\",\n url: \"URL\",\n emoji: \"эмодзі\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO дата і час\",\n date: \"ISO дата\",\n time: \"ISO час\",\n duration: \"ISO працягласць\",\n ipv4: \"IPv4 адрас\",\n ipv6: \"IPv6 адрас\",\n cidrv4: \"IPv4 дыяпазон\",\n cidrv6: \"IPv6 дыяпазон\",\n base64: \"радок у фармаце base64\",\n base64url: \"радок у фармаце base64url\",\n json_string: \"JSON радок\",\n e164: \"нумар E.164\",\n jwt: \"JWT\",\n template_literal: \"увод\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Няправільны ўвод: чакаўся ${issue.expected}, атрымана ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Няправільны ўвод: чакалася ${util.stringifyPrimitive(issue.values[0])}`;\n return `Няправільны варыянт: чакаўся адзін з ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n const maxValue = Number(issue.maximum);\n const unit = getBelarusianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);\n return `Занадта вялікі: чакалася, што ${issue.origin ?? \"значэнне\"} павінна ${sizing.verb} ${adj}${issue.maximum.toString()} ${unit}`;\n }\n return `Занадта вялікі: чакалася, што ${issue.origin ?? \"значэнне\"} павінна быць ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n const minValue = Number(issue.minimum);\n const unit = getBelarusianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);\n return `Занадта малы: чакалася, што ${issue.origin} павінна ${sizing.verb} ${adj}${issue.minimum.toString()} ${unit}`;\n }\n return `Занадта малы: чакалася, што ${issue.origin} павінна быць ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Няправільны радок: павінен пачынацца з \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Няправільны радок: павінен заканчвацца на \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Няправільны радок: павінен змяшчаць \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Няправільны радок: павінен адпавядаць шаблону ${_issue.pattern}`;\n return `Няправільны ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Няправільны лік: павінен быць кратным ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Нераспазнаны ${issue.keys.length > 1 ? \"ключы\" : \"ключ\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Няправільны ключ у ${issue.origin}`;\n case \"invalid_union\":\n return \"Няправільны ўвод\";\n case \"invalid_element\":\n return `Няправільнае значэнне ў ${issue.origin}`;\n default:\n return `Няправільны ўвод`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caràcters\", verb: \"contenir\" },\n file: { unit: \"bytes\", verb: \"contenir\" },\n array: { unit: \"elements\", verb: \"contenir\" },\n set: { unit: \"elements\", verb: \"contenir\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"entrada\",\n email: \"adreça electrònica\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"data i hora ISO\",\n date: \"data ISO\",\n time: \"hora ISO\",\n duration: \"durada ISO\",\n ipv4: \"adreça IPv4\",\n ipv6: \"adreça IPv6\",\n cidrv4: \"rang IPv4\",\n cidrv6: \"rang IPv6\",\n base64: \"cadena codificada en base64\",\n base64url: \"cadena codificada en base64url\",\n json_string: \"cadena JSON\",\n e164: \"número E.164\",\n jwt: \"JWT\",\n template_literal: \"entrada\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Tipus invàlid: s'esperava ${issue.expected}, s'ha rebut ${parsedType(issue.input)}`;\n // return `Tipus invàlid: s'esperava ${issue.expected}, s'ha rebut ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Valor invàlid: s'esperava ${util.stringifyPrimitive(issue.values[0])}`;\n return `Opció invàlida: s'esperava una de ${util.joinValues(issue.values, \" o \")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"com a màxim\" : \"menys de\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Massa gran: s'esperava que ${issue.origin ?? \"el valor\"} contingués ${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Massa gran: s'esperava que ${issue.origin ?? \"el valor\"} fos ${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"com a mínim\" : \"més de\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Massa petit: s'esperava que ${issue.origin} contingués ${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Massa petit: s'esperava que ${issue.origin} fos ${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Format invàlid: ha de començar amb \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Format invàlid: ha d'acabar amb \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Format invàlid: ha d'incloure \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Format invàlid: ha de coincidir amb el patró ${_issue.pattern}`;\n return `Format invàlid per a ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Número invàlid: ha de ser múltiple de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Clau${issue.keys.length > 1 ? \"s\" : \"\"} no reconeguda${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Clau invàlida a ${issue.origin}`;\n case \"invalid_union\":\n return \"Entrada invàlida\"; // Could also be \"Tipus d'unió invàlid\" but \"Entrada invàlida\" is more general\n case \"invalid_element\":\n return `Element invàlid a ${issue.origin}`;\n default:\n return `Entrada invàlida`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"znaků\", verb: \"mít\" },\n file: { unit: \"bajtů\", verb: \"mít\" },\n array: { unit: \"prvků\", verb: \"mít\" },\n set: { unit: \"prvků\", verb: \"mít\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"číslo\";\n }\n case \"string\": {\n return \"řetězec\";\n }\n case \"boolean\": {\n return \"boolean\";\n }\n case \"bigint\": {\n return \"bigint\";\n }\n case \"function\": {\n return \"funkce\";\n }\n case \"symbol\": {\n return \"symbol\";\n }\n case \"undefined\": {\n return \"undefined\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"pole\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"regulární výraz\",\n email: \"e-mailová adresa\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"datum a čas ve formátu ISO\",\n date: \"datum ve formátu ISO\",\n time: \"čas ve formátu ISO\",\n duration: \"doba trvání ISO\",\n ipv4: \"IPv4 adresa\",\n ipv6: \"IPv6 adresa\",\n cidrv4: \"rozsah IPv4\",\n cidrv6: \"rozsah IPv6\",\n base64: \"řetězec zakódovaný ve formátu base64\",\n base64url: \"řetězec zakódovaný ve formátu base64url\",\n json_string: \"řetězec ve formátu JSON\",\n e164: \"číslo E.164\",\n jwt: \"JWT\",\n template_literal: \"vstup\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Neplatný vstup: očekáváno ${issue.expected}, obdrženo ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Neplatný vstup: očekáváno ${util.stringifyPrimitive(issue.values[0])}`;\n return `Neplatná možnost: očekávána jedna z hodnot ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Hodnota je příliš velká: ${issue.origin ?? \"hodnota\"} musí mít ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"prvků\"}`;\n }\n return `Hodnota je příliš velká: ${issue.origin ?? \"hodnota\"} musí být ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Hodnota je příliš malá: ${issue.origin ?? \"hodnota\"} musí mít ${adj}${issue.minimum.toString()} ${sizing.unit ?? \"prvků\"}`;\n }\n return `Hodnota je příliš malá: ${issue.origin ?? \"hodnota\"} musí být ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Neplatný řetězec: musí začínat na \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Neplatný řetězec: musí končit na \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Neplatný řetězec: musí obsahovat \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Neplatný řetězec: musí odpovídat vzoru ${_issue.pattern}`;\n return `Neplatný formát ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Neplatné číslo: musí být násobkem ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Neznámé klíče: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Neplatný klíč v ${issue.origin}`;\n case \"invalid_union\":\n return \"Neplatný vstup\";\n case \"invalid_element\":\n return `Neplatná hodnota v ${issue.origin}`;\n default:\n return `Neplatný vstup`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"tegn\", verb: \"havde\" },\n file: { unit: \"bytes\", verb: \"havde\" },\n array: { unit: \"elementer\", verb: \"indeholdt\" },\n set: { unit: \"elementer\", verb: \"indeholdt\" },\n };\n const TypeNames = {\n string: \"streng\",\n number: \"tal\",\n boolean: \"boolean\",\n array: \"liste\",\n object: \"objekt\",\n set: \"sæt\",\n file: \"fil\",\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n function getTypeName(type) {\n return TypeNames[type] ?? type;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"tal\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"liste\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n return \"objekt\";\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"e-mailadresse\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO dato- og klokkeslæt\",\n date: \"ISO-dato\",\n time: \"ISO-klokkeslæt\",\n duration: \"ISO-varighed\",\n ipv4: \"IPv4-område\",\n ipv6: \"IPv6-område\",\n cidrv4: \"IPv4-spektrum\",\n cidrv6: \"IPv6-spektrum\",\n base64: \"base64-kodet streng\",\n base64url: \"base64url-kodet streng\",\n json_string: \"JSON-streng\",\n e164: \"E.164-nummer\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ugyldigt input: forventede ${getTypeName(issue.expected)}, fik ${getTypeName(parsedType(issue.input))}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ugyldig værdi: forventede ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ugyldigt valg: forventede en af følgende ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n const origin = getTypeName(issue.origin);\n if (sizing)\n return `For stor: forventede ${origin ?? \"value\"} ${sizing.verb} ${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"elementer\"}`;\n return `For stor: forventede ${origin ?? \"value\"} havde ${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n const origin = getTypeName(issue.origin);\n if (sizing) {\n return `For lille: forventede ${origin} ${sizing.verb} ${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `For lille: forventede ${origin} havde ${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Ugyldig streng: skal starte med \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Ugyldig streng: skal ende med \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ugyldig streng: skal indeholde \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ugyldig streng: skal matche mønsteret ${_issue.pattern}`;\n return `Ugyldig ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ugyldigt tal: skal være deleligt med ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Ukendte nøgler\" : \"Ukendt nøgle\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ugyldig nøgle i ${issue.origin}`;\n case \"invalid_union\":\n return \"Ugyldigt input: matcher ingen af de tilladte typer\";\n case \"invalid_element\":\n return `Ugyldig værdi i ${issue.origin}`;\n default:\n return `Ugyldigt input`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"Zeichen\", verb: \"zu haben\" },\n file: { unit: \"Bytes\", verb: \"zu haben\" },\n array: { unit: \"Elemente\", verb: \"zu haben\" },\n set: { unit: \"Elemente\", verb: \"zu haben\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"Zahl\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"Array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"Eingabe\",\n email: \"E-Mail-Adresse\",\n url: \"URL\",\n emoji: \"Emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO-Datum und -Uhrzeit\",\n date: \"ISO-Datum\",\n time: \"ISO-Uhrzeit\",\n duration: \"ISO-Dauer\",\n ipv4: \"IPv4-Adresse\",\n ipv6: \"IPv6-Adresse\",\n cidrv4: \"IPv4-Bereich\",\n cidrv6: \"IPv6-Bereich\",\n base64: \"Base64-codierter String\",\n base64url: \"Base64-URL-codierter String\",\n json_string: \"JSON-String\",\n e164: \"E.164-Nummer\",\n jwt: \"JWT\",\n template_literal: \"Eingabe\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ungültige Eingabe: erwartet ${issue.expected}, erhalten ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ungültige Eingabe: erwartet ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ungültige Option: erwartet eine von ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Zu groß: erwartet, dass ${issue.origin ?? \"Wert\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"Elemente\"} hat`;\n return `Zu groß: erwartet, dass ${issue.origin ?? \"Wert\"} ${adj}${issue.maximum.toString()} ist`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Zu klein: erwartet, dass ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit} hat`;\n }\n return `Zu klein: erwartet, dass ${issue.origin} ${adj}${issue.minimum.toString()} ist`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Ungültiger String: muss mit \"${_issue.prefix}\" beginnen`;\n if (_issue.format === \"ends_with\")\n return `Ungültiger String: muss mit \"${_issue.suffix}\" enden`;\n if (_issue.format === \"includes\")\n return `Ungültiger String: muss \"${_issue.includes}\" enthalten`;\n if (_issue.format === \"regex\")\n return `Ungültiger String: muss dem Muster ${_issue.pattern} entsprechen`;\n return `Ungültig: ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ungültige Zahl: muss ein Vielfaches von ${issue.divisor} sein`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Unbekannte Schlüssel\" : \"Unbekannter Schlüssel\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ungültiger Schlüssel in ${issue.origin}`;\n case \"invalid_union\":\n return \"Ungültige Eingabe\";\n case \"invalid_element\":\n return `Ungültiger Wert in ${issue.origin}`;\n default:\n return `Ungültige Eingabe`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"characters\", verb: \"to have\" },\n file: { unit: \"bytes\", verb: \"to have\" },\n array: { unit: \"items\", verb: \"to have\" },\n set: { unit: \"items\", verb: \"to have\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Invalid input: expected ${issue.expected}, received ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Invalid input: expected ${util.stringifyPrimitive(issue.values[0])}`;\n return `Invalid option: expected one of ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Too big: expected ${issue.origin ?? \"value\"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Too big: expected ${issue.origin ?? \"value\"} to be ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Invalid string: must start with \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Invalid string: must end with \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Invalid string: must include \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Invalid string: must match pattern ${_issue.pattern}`;\n return `Invalid ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Invalid number: must be a multiple of ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Unrecognized key${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Invalid key in ${issue.origin}`;\n case \"invalid_union\":\n return \"Invalid input\";\n case \"invalid_element\":\n return `Invalid value in ${issue.origin}`;\n default:\n return `Invalid input`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"nombro\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tabelo\";\n }\n if (data === null) {\n return \"senvalora\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"karaktrojn\", verb: \"havi\" },\n file: { unit: \"bajtojn\", verb: \"havi\" },\n array: { unit: \"elementojn\", verb: \"havi\" },\n set: { unit: \"elementojn\", verb: \"havi\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"enigo\",\n email: \"retadreso\",\n url: \"URL\",\n emoji: \"emoĝio\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO-datotempo\",\n date: \"ISO-dato\",\n time: \"ISO-tempo\",\n duration: \"ISO-daŭro\",\n ipv4: \"IPv4-adreso\",\n ipv6: \"IPv6-adreso\",\n cidrv4: \"IPv4-rango\",\n cidrv6: \"IPv6-rango\",\n base64: \"64-ume kodita karaktraro\",\n base64url: \"URL-64-ume kodita karaktraro\",\n json_string: \"JSON-karaktraro\",\n e164: \"E.164-nombro\",\n jwt: \"JWT\",\n template_literal: \"enigo\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Nevalida enigo: atendiĝis ${issue.expected}, riceviĝis ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Nevalida enigo: atendiĝis ${util.stringifyPrimitive(issue.values[0])}`;\n return `Nevalida opcio: atendiĝis unu el ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Tro granda: atendiĝis ke ${issue.origin ?? \"valoro\"} havu ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementojn\"}`;\n return `Tro granda: atendiĝis ke ${issue.origin ?? \"valoro\"} havu ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Tro malgranda: atendiĝis ke ${issue.origin} havu ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Tro malgranda: atendiĝis ke ${issue.origin} estu ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Nevalida karaktraro: devas komenciĝi per \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Nevalida karaktraro: devas finiĝi per \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Nevalida karaktraro: devas inkluzivi \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Nevalida karaktraro: devas kongrui kun la modelo ${_issue.pattern}`;\n return `Nevalida ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Nevalida nombro: devas esti oblo de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Nekonata${issue.keys.length > 1 ? \"j\" : \"\"} ŝlosilo${issue.keys.length > 1 ? \"j\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Nevalida ŝlosilo en ${issue.origin}`;\n case \"invalid_union\":\n return \"Nevalida enigo\";\n case \"invalid_element\":\n return `Nevalida valoro en ${issue.origin}`;\n default:\n return `Nevalida enigo`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caracteres\", verb: \"tener\" },\n file: { unit: \"bytes\", verb: \"tener\" },\n array: { unit: \"elementos\", verb: \"tener\" },\n set: { unit: \"elementos\", verb: \"tener\" },\n };\n const TypeNames = {\n string: \"texto\",\n number: \"número\",\n boolean: \"booleano\",\n array: \"arreglo\",\n object: \"objeto\",\n set: \"conjunto\",\n file: \"archivo\",\n date: \"fecha\",\n bigint: \"número grande\",\n symbol: \"símbolo\",\n undefined: \"indefinido\",\n null: \"nulo\",\n function: \"función\",\n map: \"mapa\",\n record: \"registro\",\n tuple: \"tupla\",\n enum: \"enumeración\",\n union: \"unión\",\n literal: \"literal\",\n promise: \"promesa\",\n void: \"vacío\",\n never: \"nunca\",\n unknown: \"desconocido\",\n any: \"cualquiera\",\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n function getTypeName(type) {\n return TypeNames[type] ?? type;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype) {\n return data.constructor.name;\n }\n return \"object\";\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"entrada\",\n email: \"dirección de correo electrónico\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"fecha y hora ISO\",\n date: \"fecha ISO\",\n time: \"hora ISO\",\n duration: \"duración ISO\",\n ipv4: \"dirección IPv4\",\n ipv6: \"dirección IPv6\",\n cidrv4: \"rango IPv4\",\n cidrv6: \"rango IPv6\",\n base64: \"cadena codificada en base64\",\n base64url: \"URL codificada en base64\",\n json_string: \"cadena JSON\",\n e164: \"número E.164\",\n jwt: \"JWT\",\n template_literal: \"entrada\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Entrada inválida: se esperaba ${getTypeName(issue.expected)}, recibido ${getTypeName(parsedType(issue.input))}`;\n // return `Entrada inválida: se esperaba ${issue.expected}, recibido ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Entrada inválida: se esperaba ${util.stringifyPrimitive(issue.values[0])}`;\n return `Opción inválida: se esperaba una de ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n const origin = getTypeName(issue.origin);\n if (sizing)\n return `Demasiado grande: se esperaba que ${origin ?? \"valor\"} tuviera ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementos\"}`;\n return `Demasiado grande: se esperaba que ${origin ?? \"valor\"} fuera ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n const origin = getTypeName(issue.origin);\n if (sizing) {\n return `Demasiado pequeño: se esperaba que ${origin} tuviera ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Demasiado pequeño: se esperaba que ${origin} fuera ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Cadena inválida: debe comenzar con \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Cadena inválida: debe terminar en \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Cadena inválida: debe incluir \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Cadena inválida: debe coincidir con el patrón ${_issue.pattern}`;\n return `Inválido ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Número inválido: debe ser múltiplo de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Llave${issue.keys.length > 1 ? \"s\" : \"\"} desconocida${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Llave inválida en ${getTypeName(issue.origin)}`;\n case \"invalid_union\":\n return \"Entrada inválida\";\n case \"invalid_element\":\n return `Valor inválido en ${getTypeName(issue.origin)}`;\n default:\n return `Entrada inválida`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"کاراکتر\", verb: \"داشته باشد\" },\n file: { unit: \"بایت\", verb: \"داشته باشد\" },\n array: { unit: \"آیتم\", verb: \"داشته باشد\" },\n set: { unit: \"آیتم\", verb: \"داشته باشد\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"عدد\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"آرایه\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ورودی\",\n email: \"آدرس ایمیل\",\n url: \"URL\",\n emoji: \"ایموجی\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"تاریخ و زمان ایزو\",\n date: \"تاریخ ایزو\",\n time: \"زمان ایزو\",\n duration: \"مدت زمان ایزو\",\n ipv4: \"IPv4 آدرس\",\n ipv6: \"IPv6 آدرس\",\n cidrv4: \"IPv4 دامنه\",\n cidrv6: \"IPv6 دامنه\",\n base64: \"base64-encoded رشته\",\n base64url: \"base64url-encoded رشته\",\n json_string: \"JSON رشته\",\n e164: \"E.164 عدد\",\n jwt: \"JWT\",\n template_literal: \"ورودی\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `ورودی نامعتبر: می‌بایست ${issue.expected} می‌بود، ${parsedType(issue.input)} دریافت شد`;\n case \"invalid_value\":\n if (issue.values.length === 1) {\n return `ورودی نامعتبر: می‌بایست ${util.stringifyPrimitive(issue.values[0])} می‌بود`;\n }\n return `گزینه نامعتبر: می‌بایست یکی از ${util.joinValues(issue.values, \"|\")} می‌بود`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `خیلی بزرگ: ${issue.origin ?? \"مقدار\"} باید ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"عنصر\"} باشد`;\n }\n return `خیلی بزرگ: ${issue.origin ?? \"مقدار\"} باید ${adj}${issue.maximum.toString()} باشد`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `خیلی کوچک: ${issue.origin} باید ${adj}${issue.minimum.toString()} ${sizing.unit} باشد`;\n }\n return `خیلی کوچک: ${issue.origin} باید ${adj}${issue.minimum.toString()} باشد`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `رشته نامعتبر: باید با \"${_issue.prefix}\" شروع شود`;\n }\n if (_issue.format === \"ends_with\") {\n return `رشته نامعتبر: باید با \"${_issue.suffix}\" تمام شود`;\n }\n if (_issue.format === \"includes\") {\n return `رشته نامعتبر: باید شامل \"${_issue.includes}\" باشد`;\n }\n if (_issue.format === \"regex\") {\n return `رشته نامعتبر: باید با الگوی ${_issue.pattern} مطابقت داشته باشد`;\n }\n return `${Nouns[_issue.format] ?? issue.format} نامعتبر`;\n }\n case \"not_multiple_of\":\n return `عدد نامعتبر: باید مضرب ${issue.divisor} باشد`;\n case \"unrecognized_keys\":\n return `کلید${issue.keys.length > 1 ? \"های\" : \"\"} ناشناس: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `کلید ناشناس در ${issue.origin}`;\n case \"invalid_union\":\n return `ورودی نامعتبر`;\n case \"invalid_element\":\n return `مقدار نامعتبر در ${issue.origin}`;\n default:\n return `ورودی نامعتبر`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"merkkiä\", subject: \"merkkijonon\" },\n file: { unit: \"tavua\", subject: \"tiedoston\" },\n array: { unit: \"alkiota\", subject: \"listan\" },\n set: { unit: \"alkiota\", subject: \"joukon\" },\n number: { unit: \"\", subject: \"luvun\" },\n bigint: { unit: \"\", subject: \"suuren kokonaisluvun\" },\n int: { unit: \"\", subject: \"kokonaisluvun\" },\n date: { unit: \"\", subject: \"päivämäärän\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"säännöllinen lauseke\",\n email: \"sähköpostiosoite\",\n url: \"URL-osoite\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO-aikaleima\",\n date: \"ISO-päivämäärä\",\n time: \"ISO-aika\",\n duration: \"ISO-kesto\",\n ipv4: \"IPv4-osoite\",\n ipv6: \"IPv6-osoite\",\n cidrv4: \"IPv4-alue\",\n cidrv6: \"IPv6-alue\",\n base64: \"base64-koodattu merkkijono\",\n base64url: \"base64url-koodattu merkkijono\",\n json_string: \"JSON-merkkijono\",\n e164: \"E.164-luku\",\n jwt: \"JWT\",\n template_literal: \"templaattimerkkijono\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Virheellinen tyyppi: odotettiin ${issue.expected}, oli ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Virheellinen syöte: täytyy olla ${util.stringifyPrimitive(issue.values[0])}`;\n return `Virheellinen valinta: täytyy olla yksi seuraavista: ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Liian suuri: ${sizing.subject} täytyy olla ${adj}${issue.maximum.toString()} ${sizing.unit}`.trim();\n }\n return `Liian suuri: arvon täytyy olla ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Liian pieni: ${sizing.subject} täytyy olla ${adj}${issue.minimum.toString()} ${sizing.unit}`.trim();\n }\n return `Liian pieni: arvon täytyy olla ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Virheellinen syöte: täytyy alkaa \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Virheellinen syöte: täytyy loppua \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Virheellinen syöte: täytyy sisältää \"${_issue.includes}\"`;\n if (_issue.format === \"regex\") {\n return `Virheellinen syöte: täytyy vastata säännöllistä lauseketta ${_issue.pattern}`;\n }\n return `Virheellinen ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Virheellinen luku: täytyy olla luvun ${issue.divisor} monikerta`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Tuntemattomat avaimet\" : \"Tuntematon avain\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return \"Virheellinen avain tietueessa\";\n case \"invalid_union\":\n return \"Virheellinen unioni\";\n case \"invalid_element\":\n return \"Virheellinen arvo joukossa\";\n default:\n return `Virheellinen syöte`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caractères\", verb: \"avoir\" },\n file: { unit: \"octets\", verb: \"avoir\" },\n array: { unit: \"éléments\", verb: \"avoir\" },\n set: { unit: \"éléments\", verb: \"avoir\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"nombre\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tableau\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"entrée\",\n email: \"adresse e-mail\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"date et heure ISO\",\n date: \"date ISO\",\n time: \"heure ISO\",\n duration: \"durée ISO\",\n ipv4: \"adresse IPv4\",\n ipv6: \"adresse IPv6\",\n cidrv4: \"plage IPv4\",\n cidrv6: \"plage IPv6\",\n base64: \"chaîne encodée en base64\",\n base64url: \"chaîne encodée en base64url\",\n json_string: \"chaîne JSON\",\n e164: \"numéro E.164\",\n jwt: \"JWT\",\n template_literal: \"entrée\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Entrée invalide : ${issue.expected} attendu, ${parsedType(issue.input)} reçu`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Entrée invalide : ${util.stringifyPrimitive(issue.values[0])} attendu`;\n return `Option invalide : une valeur parmi ${util.joinValues(issue.values, \"|\")} attendue`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Trop grand : ${issue.origin ?? \"valeur\"} doit ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"élément(s)\"}`;\n return `Trop grand : ${issue.origin ?? \"valeur\"} doit être ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Trop petit : ${issue.origin} doit ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Trop petit : ${issue.origin} doit être ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Chaîne invalide : doit commencer par \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Chaîne invalide : doit se terminer par \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Chaîne invalide : doit inclure \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Chaîne invalide : doit correspondre au modèle ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} invalide`;\n }\n case \"not_multiple_of\":\n return `Nombre invalide : doit être un multiple de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Clé${issue.keys.length > 1 ? \"s\" : \"\"} non reconnue${issue.keys.length > 1 ? \"s\" : \"\"} : ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Clé invalide dans ${issue.origin}`;\n case \"invalid_union\":\n return \"Entrée invalide\";\n case \"invalid_element\":\n return `Valeur invalide dans ${issue.origin}`;\n default:\n return `Entrée invalide`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caractères\", verb: \"avoir\" },\n file: { unit: \"octets\", verb: \"avoir\" },\n array: { unit: \"éléments\", verb: \"avoir\" },\n set: { unit: \"éléments\", verb: \"avoir\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"entrée\",\n email: \"adresse courriel\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"date-heure ISO\",\n date: \"date ISO\",\n time: \"heure ISO\",\n duration: \"durée ISO\",\n ipv4: \"adresse IPv4\",\n ipv6: \"adresse IPv6\",\n cidrv4: \"plage IPv4\",\n cidrv6: \"plage IPv6\",\n base64: \"chaîne encodée en base64\",\n base64url: \"chaîne encodée en base64url\",\n json_string: \"chaîne JSON\",\n e164: \"numéro E.164\",\n jwt: \"JWT\",\n template_literal: \"entrée\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Entrée invalide : attendu ${issue.expected}, reçu ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Entrée invalide : attendu ${util.stringifyPrimitive(issue.values[0])}`;\n return `Option invalide : attendu l'une des valeurs suivantes ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"≤\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Trop grand : attendu que ${issue.origin ?? \"la valeur\"} ait ${adj}${issue.maximum.toString()} ${sizing.unit}`;\n return `Trop grand : attendu que ${issue.origin ?? \"la valeur\"} soit ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"≥\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Trop petit : attendu que ${issue.origin} ait ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Trop petit : attendu que ${issue.origin} soit ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Chaîne invalide : doit commencer par \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Chaîne invalide : doit se terminer par \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Chaîne invalide : doit inclure \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Chaîne invalide : doit correspondre au motif ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} invalide`;\n }\n case \"not_multiple_of\":\n return `Nombre invalide : doit être un multiple de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Clé${issue.keys.length > 1 ? \"s\" : \"\"} non reconnue${issue.keys.length > 1 ? \"s\" : \"\"} : ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Clé invalide dans ${issue.origin}`;\n case \"invalid_union\":\n return \"Entrée invalide\";\n case \"invalid_element\":\n return `Valeur invalide dans ${issue.origin}`;\n default:\n return `Entrée invalide`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"אותיות\", verb: \"לכלול\" },\n file: { unit: \"בייטים\", verb: \"לכלול\" },\n array: { unit: \"פריטים\", verb: \"לכלול\" },\n set: { unit: \"פריטים\", verb: \"לכלול\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"קלט\",\n email: \"כתובת אימייל\",\n url: \"כתובת רשת\",\n emoji: \"אימוג'י\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"תאריך וזמן ISO\",\n date: \"תאריך ISO\",\n time: \"זמן ISO\",\n duration: \"משך זמן ISO\",\n ipv4: \"כתובת IPv4\",\n ipv6: \"כתובת IPv6\",\n cidrv4: \"טווח IPv4\",\n cidrv6: \"טווח IPv6\",\n base64: \"מחרוזת בבסיס 64\",\n base64url: \"מחרוזת בבסיס 64 לכתובות רשת\",\n json_string: \"מחרוזת JSON\",\n e164: \"מספר E.164\",\n jwt: \"JWT\",\n template_literal: \"קלט\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `קלט לא תקין: צריך ${issue.expected}, התקבל ${parsedType(issue.input)}`;\n // return `Invalid input: expected ${issue.expected}, received ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `קלט לא תקין: צריך ${util.stringifyPrimitive(issue.values[0])}`;\n return `קלט לא תקין: צריך אחת מהאפשרויות ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `גדול מדי: ${issue.origin ?? \"value\"} צריך להיות ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `גדול מדי: ${issue.origin ?? \"value\"} צריך להיות ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `קטן מדי: ${issue.origin} צריך להיות ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `קטן מדי: ${issue.origin} צריך להיות ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `מחרוזת לא תקינה: חייבת להתחיל ב\"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `מחרוזת לא תקינה: חייבת להסתיים ב \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `מחרוזת לא תקינה: חייבת לכלול \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `מחרוזת לא תקינה: חייבת להתאים לתבנית ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} לא תקין`;\n }\n case \"not_multiple_of\":\n return `מספר לא תקין: חייב להיות מכפלה של ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `מפתח${issue.keys.length > 1 ? \"ות\" : \"\"} לא מזוה${issue.keys.length > 1 ? \"ים\" : \"ה\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `מפתח לא תקין ב${issue.origin}`;\n case \"invalid_union\":\n return \"קלט לא תקין\";\n case \"invalid_element\":\n return `ערך לא תקין ב${issue.origin}`;\n default:\n return `קלט לא תקין`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"karakter\", verb: \"legyen\" },\n file: { unit: \"byte\", verb: \"legyen\" },\n array: { unit: \"elem\", verb: \"legyen\" },\n set: { unit: \"elem\", verb: \"legyen\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"szám\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tömb\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"bemenet\",\n email: \"email cím\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO időbélyeg\",\n date: \"ISO dátum\",\n time: \"ISO idő\",\n duration: \"ISO időintervallum\",\n ipv4: \"IPv4 cím\",\n ipv6: \"IPv6 cím\",\n cidrv4: \"IPv4 tartomány\",\n cidrv6: \"IPv6 tartomány\",\n base64: \"base64-kódolt string\",\n base64url: \"base64url-kódolt string\",\n json_string: \"JSON string\",\n e164: \"E.164 szám\",\n jwt: \"JWT\",\n template_literal: \"bemenet\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Érvénytelen bemenet: a várt érték ${issue.expected}, a kapott érték ${parsedType(issue.input)}`;\n // return `Invalid input: expected ${issue.expected}, received ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Érvénytelen bemenet: a várt érték ${util.stringifyPrimitive(issue.values[0])}`;\n return `Érvénytelen opció: valamelyik érték várt ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Túl nagy: ${issue.origin ?? \"érték\"} mérete túl nagy ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elem\"}`;\n return `Túl nagy: a bemeneti érték ${issue.origin ?? \"érték\"} túl nagy: ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Túl kicsi: a bemeneti érték ${issue.origin} mérete túl kicsi ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Túl kicsi: a bemeneti érték ${issue.origin} túl kicsi ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Érvénytelen string: \"${_issue.prefix}\" értékkel kell kezdődnie`;\n if (_issue.format === \"ends_with\")\n return `Érvénytelen string: \"${_issue.suffix}\" értékkel kell végződnie`;\n if (_issue.format === \"includes\")\n return `Érvénytelen string: \"${_issue.includes}\" értéket kell tartalmaznia`;\n if (_issue.format === \"regex\")\n return `Érvénytelen string: ${_issue.pattern} mintának kell megfelelnie`;\n return `Érvénytelen ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Érvénytelen szám: ${issue.divisor} többszörösének kell lennie`;\n case \"unrecognized_keys\":\n return `Ismeretlen kulcs${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Érvénytelen kulcs ${issue.origin}`;\n case \"invalid_union\":\n return \"Érvénytelen bemenet\";\n case \"invalid_element\":\n return `Érvénytelen érték: ${issue.origin}`;\n default:\n return `Érvénytelen bemenet`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"karakter\", verb: \"memiliki\" },\n file: { unit: \"byte\", verb: \"memiliki\" },\n array: { unit: \"item\", verb: \"memiliki\" },\n set: { unit: \"item\", verb: \"memiliki\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"alamat email\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"tanggal dan waktu format ISO\",\n date: \"tanggal format ISO\",\n time: \"jam format ISO\",\n duration: \"durasi format ISO\",\n ipv4: \"alamat IPv4\",\n ipv6: \"alamat IPv6\",\n cidrv4: \"rentang alamat IPv4\",\n cidrv6: \"rentang alamat IPv6\",\n base64: \"string dengan enkode base64\",\n base64url: \"string dengan enkode base64url\",\n json_string: \"string JSON\",\n e164: \"angka E.164\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Input tidak valid: diharapkan ${issue.expected}, diterima ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Input tidak valid: diharapkan ${util.stringifyPrimitive(issue.values[0])}`;\n return `Pilihan tidak valid: diharapkan salah satu dari ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Terlalu besar: diharapkan ${issue.origin ?? \"value\"} memiliki ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elemen\"}`;\n return `Terlalu besar: diharapkan ${issue.origin ?? \"value\"} menjadi ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Terlalu kecil: diharapkan ${issue.origin} memiliki ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Terlalu kecil: diharapkan ${issue.origin} menjadi ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `String tidak valid: harus dimulai dengan \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `String tidak valid: harus berakhir dengan \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `String tidak valid: harus menyertakan \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `String tidak valid: harus sesuai pola ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} tidak valid`;\n }\n case \"not_multiple_of\":\n return `Angka tidak valid: harus kelipatan dari ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Kunci tidak dikenali ${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Kunci tidak valid di ${issue.origin}`;\n case \"invalid_union\":\n return \"Input tidak valid\";\n case \"invalid_element\":\n return `Nilai tidak valid di ${issue.origin}`;\n default:\n return `Input tidak valid`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"númer\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"fylki\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"stafi\", verb: \"að hafa\" },\n file: { unit: \"bæti\", verb: \"að hafa\" },\n array: { unit: \"hluti\", verb: \"að hafa\" },\n set: { unit: \"hluti\", verb: \"að hafa\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"gildi\",\n email: \"netfang\",\n url: \"vefslóð\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO dagsetning og tími\",\n date: \"ISO dagsetning\",\n time: \"ISO tími\",\n duration: \"ISO tímalengd\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded strengur\",\n base64url: \"base64url-encoded strengur\",\n json_string: \"JSON strengur\",\n e164: \"E.164 tölugildi\",\n jwt: \"JWT\",\n template_literal: \"gildi\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Rangt gildi: Þú slóst inn ${parsedType(issue.input)} þar sem á að vera ${issue.expected}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Rangt gildi: gert ráð fyrir ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ógilt val: má vera eitt af eftirfarandi ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Of stórt: gert er ráð fyrir að ${issue.origin ?? \"gildi\"} hafi ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"hluti\"}`;\n return `Of stórt: gert er ráð fyrir að ${issue.origin ?? \"gildi\"} sé ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Of lítið: gert er ráð fyrir að ${issue.origin} hafi ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Of lítið: gert er ráð fyrir að ${issue.origin} sé ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Ógildur strengur: verður að byrja á \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Ógildur strengur: verður að enda á \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ógildur strengur: verður að innihalda \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ógildur strengur: verður að fylgja mynstri ${_issue.pattern}`;\n return `Rangt ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Röng tala: verður að vera margfeldi af ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Óþekkt ${issue.keys.length > 1 ? \"ir lyklar\" : \"ur lykill\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Rangur lykill í ${issue.origin}`;\n case \"invalid_union\":\n return \"Rangt gildi\";\n case \"invalid_element\":\n return `Rangt gildi í ${issue.origin}`;\n default:\n return `Rangt gildi`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caratteri\", verb: \"avere\" },\n file: { unit: \"byte\", verb: \"avere\" },\n array: { unit: \"elementi\", verb: \"avere\" },\n set: { unit: \"elementi\", verb: \"avere\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"numero\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"vettore\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"indirizzo email\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"data e ora ISO\",\n date: \"data ISO\",\n time: \"ora ISO\",\n duration: \"durata ISO\",\n ipv4: \"indirizzo IPv4\",\n ipv6: \"indirizzo IPv6\",\n cidrv4: \"intervallo IPv4\",\n cidrv6: \"intervallo IPv6\",\n base64: \"stringa codificata in base64\",\n base64url: \"URL codificata in base64\",\n json_string: \"stringa JSON\",\n e164: \"numero E.164\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Input non valido: atteso ${issue.expected}, ricevuto ${parsedType(issue.input)}`;\n // return `Input non valido: atteso ${issue.expected}, ricevuto ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Input non valido: atteso ${util.stringifyPrimitive(issue.values[0])}`;\n return `Opzione non valida: atteso uno tra ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Troppo grande: ${issue.origin ?? \"valore\"} deve avere ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementi\"}`;\n return `Troppo grande: ${issue.origin ?? \"valore\"} deve essere ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Troppo piccolo: ${issue.origin} deve avere ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Troppo piccolo: ${issue.origin} deve essere ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Stringa non valida: deve iniziare con \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Stringa non valida: deve terminare con \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Stringa non valida: deve includere \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Stringa non valida: deve corrispondere al pattern ${_issue.pattern}`;\n return `Invalid ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Numero non valido: deve essere un multiplo di ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Chiav${issue.keys.length > 1 ? \"i\" : \"e\"} non riconosciut${issue.keys.length > 1 ? \"e\" : \"a\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Chiave non valida in ${issue.origin}`;\n case \"invalid_union\":\n return \"Input non valido\";\n case \"invalid_element\":\n return `Valore non valido in ${issue.origin}`;\n default:\n return `Input non valido`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"文字\", verb: \"である\" },\n file: { unit: \"バイト\", verb: \"である\" },\n array: { unit: \"要素\", verb: \"である\" },\n set: { unit: \"要素\", verb: \"である\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"数値\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"配列\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"入力値\",\n email: \"メールアドレス\",\n url: \"URL\",\n emoji: \"絵文字\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO日時\",\n date: \"ISO日付\",\n time: \"ISO時刻\",\n duration: \"ISO期間\",\n ipv4: \"IPv4アドレス\",\n ipv6: \"IPv6アドレス\",\n cidrv4: \"IPv4範囲\",\n cidrv6: \"IPv6範囲\",\n base64: \"base64エンコード文字列\",\n base64url: \"base64urlエンコード文字列\",\n json_string: \"JSON文字列\",\n e164: \"E.164番号\",\n jwt: \"JWT\",\n template_literal: \"入力値\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `無効な入力: ${issue.expected}が期待されましたが、${parsedType(issue.input)}が入力されました`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `無効な入力: ${util.stringifyPrimitive(issue.values[0])}が期待されました`;\n return `無効な選択: ${util.joinValues(issue.values, \"、\")}のいずれかである必要があります`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"以下である\" : \"より小さい\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `大きすぎる値: ${issue.origin ?? \"値\"}は${issue.maximum.toString()}${sizing.unit ?? \"要素\"}${adj}必要があります`;\n return `大きすぎる値: ${issue.origin ?? \"値\"}は${issue.maximum.toString()}${adj}必要があります`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"以上である\" : \"より大きい\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `小さすぎる値: ${issue.origin}は${issue.minimum.toString()}${sizing.unit}${adj}必要があります`;\n return `小さすぎる値: ${issue.origin}は${issue.minimum.toString()}${adj}必要があります`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `無効な文字列: \"${_issue.prefix}\"で始まる必要があります`;\n if (_issue.format === \"ends_with\")\n return `無効な文字列: \"${_issue.suffix}\"で終わる必要があります`;\n if (_issue.format === \"includes\")\n return `無効な文字列: \"${_issue.includes}\"を含む必要があります`;\n if (_issue.format === \"regex\")\n return `無効な文字列: パターン${_issue.pattern}に一致する必要があります`;\n return `無効な${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `無効な数値: ${issue.divisor}の倍数である必要があります`;\n case \"unrecognized_keys\":\n return `認識されていないキー${issue.keys.length > 1 ? \"群\" : \"\"}: ${util.joinValues(issue.keys, \"、\")}`;\n case \"invalid_key\":\n return `${issue.origin}内の無効なキー`;\n case \"invalid_union\":\n return \"無効な入力\";\n case \"invalid_element\":\n return `${issue.origin}内の無効な値`;\n default:\n return `無効な入力`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"რიცხვი\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"მასივი\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n const typeMap = {\n string: \"სტრინგი\",\n boolean: \"ბულეანი\",\n undefined: \"undefined\",\n bigint: \"bigint\",\n symbol: \"symbol\",\n function: \"ფუნქცია\",\n };\n return typeMap[t] ?? t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"სიმბოლო\", verb: \"უნდა შეიცავდეს\" },\n file: { unit: \"ბაიტი\", verb: \"უნდა შეიცავდეს\" },\n array: { unit: \"ელემენტი\", verb: \"უნდა შეიცავდეს\" },\n set: { unit: \"ელემენტი\", verb: \"უნდა შეიცავდეს\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"შეყვანა\",\n email: \"ელ-ფოსტის მისამართი\",\n url: \"URL\",\n emoji: \"ემოჯი\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"თარიღი-დრო\",\n date: \"თარიღი\",\n time: \"დრო\",\n duration: \"ხანგრძლივობა\",\n ipv4: \"IPv4 მისამართი\",\n ipv6: \"IPv6 მისამართი\",\n cidrv4: \"IPv4 დიაპაზონი\",\n cidrv6: \"IPv6 დიაპაზონი\",\n base64: \"base64-კოდირებული სტრინგი\",\n base64url: \"base64url-კოდირებული სტრინგი\",\n json_string: \"JSON სტრინგი\",\n e164: \"E.164 ნომერი\",\n jwt: \"JWT\",\n template_literal: \"შეყვანა\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `არასწორი შეყვანა: მოსალოდნელი ${issue.expected}, მიღებული ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `არასწორი შეყვანა: მოსალოდნელი ${util.stringifyPrimitive(issue.values[0])}`;\n return `არასწორი ვარიანტი: მოსალოდნელია ერთ-ერთი ${util.joinValues(issue.values, \"|\")}-დან`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `ზედმეტად დიდი: მოსალოდნელი ${issue.origin ?? \"მნიშვნელობა\"} ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit}`;\n return `ზედმეტად დიდი: მოსალოდნელი ${issue.origin ?? \"მნიშვნელობა\"} იყოს ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `ზედმეტად პატარა: მოსალოდნელი ${issue.origin} ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `ზედმეტად პატარა: მოსალოდნელი ${issue.origin} იყოს ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `არასწორი სტრინგი: უნდა იწყებოდეს \"${_issue.prefix}\"-ით`;\n }\n if (_issue.format === \"ends_with\")\n return `არასწორი სტრინგი: უნდა მთავრდებოდეს \"${_issue.suffix}\"-ით`;\n if (_issue.format === \"includes\")\n return `არასწორი სტრინგი: უნდა შეიცავდეს \"${_issue.includes}\"-ს`;\n if (_issue.format === \"regex\")\n return `არასწორი სტრინგი: უნდა შეესაბამებოდეს შაბლონს ${_issue.pattern}`;\n return `არასწორი ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `არასწორი რიცხვი: უნდა იყოს ${issue.divisor}-ის ჯერადი`;\n case \"unrecognized_keys\":\n return `უცნობი გასაღებ${issue.keys.length > 1 ? \"ები\" : \"ი\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `არასწორი გასაღები ${issue.origin}-ში`;\n case \"invalid_union\":\n return \"არასწორი შეყვანა\";\n case \"invalid_element\":\n return `არასწორი მნიშვნელობა ${issue.origin}-ში`;\n default:\n return `არასწორი შეყვანა`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"តួអក្សរ\", verb: \"គួរមាន\" },\n file: { unit: \"បៃ\", verb: \"គួរមាន\" },\n array: { unit: \"ធាតុ\", verb: \"គួរមាន\" },\n set: { unit: \"ធាតុ\", verb: \"គួរមាន\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"មិនមែនជាលេខ (NaN)\" : \"លេខ\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"អារេ (Array)\";\n }\n if (data === null) {\n return \"គ្មានតម្លៃ (null)\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ទិន្នន័យបញ្ចូល\",\n email: \"អាសយដ្ឋានអ៊ីមែល\",\n url: \"URL\",\n emoji: \"សញ្ញាអារម្មណ៍\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"កាលបរិច្ឆេទ និងម៉ោង ISO\",\n date: \"កាលបរិច្ឆេទ ISO\",\n time: \"ម៉ោង ISO\",\n duration: \"រយៈពេល ISO\",\n ipv4: \"អាសយដ្ឋាន IPv4\",\n ipv6: \"អាសយដ្ឋាន IPv6\",\n cidrv4: \"ដែនអាសយដ្ឋាន IPv4\",\n cidrv6: \"ដែនអាសយដ្ឋាន IPv6\",\n base64: \"ខ្សែអក្សរអ៊ិកូដ base64\",\n base64url: \"ខ្សែអក្សរអ៊ិកូដ base64url\",\n json_string: \"ខ្សែអក្សរ JSON\",\n e164: \"លេខ E.164\",\n jwt: \"JWT\",\n template_literal: \"ទិន្នន័យបញ្ចូល\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `ទិន្នន័យបញ្ចូលមិនត្រឹមត្រូវ៖ ត្រូវការ ${issue.expected} ប៉ុន្តែទទួលបាន ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `ទិន្នន័យបញ្ចូលមិនត្រឹមត្រូវ៖ ត្រូវការ ${util.stringifyPrimitive(issue.values[0])}`;\n return `ជម្រើសមិនត្រឹមត្រូវ៖ ត្រូវជាមួយក្នុងចំណោម ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `ធំពេក៖ ត្រូវការ ${issue.origin ?? \"តម្លៃ\"} ${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"ធាតុ\"}`;\n return `ធំពេក៖ ត្រូវការ ${issue.origin ?? \"តម្លៃ\"} ${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `តូចពេក៖ ត្រូវការ ${issue.origin} ${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `តូចពេក៖ ត្រូវការ ${issue.origin} ${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវចាប់ផ្តើមដោយ \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវបញ្ចប់ដោយ \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវមាន \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវតែផ្គូផ្គងនឹងទម្រង់ដែលបានកំណត់ ${_issue.pattern}`;\n return `មិនត្រឹមត្រូវ៖ ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `លេខមិនត្រឹមត្រូវ៖ ត្រូវតែជាពហុគុណនៃ ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `រកឃើញសោមិនស្គាល់៖ ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `សោមិនត្រឹមត្រូវនៅក្នុង ${issue.origin}`;\n case \"invalid_union\":\n return `ទិន្នន័យមិនត្រឹមត្រូវ`;\n case \"invalid_element\":\n return `ទិន្នន័យមិនត្រឹមត្រូវនៅក្នុង ${issue.origin}`;\n default:\n return `ទិន្នន័យមិនត្រឹមត្រូវ`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import km from \"./km.js\";\n/** @deprecated Use `km` instead. */\nexport default function () {\n return km();\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"문자\", verb: \"to have\" },\n file: { unit: \"바이트\", verb: \"to have\" },\n array: { unit: \"개\", verb: \"to have\" },\n set: { unit: \"개\", verb: \"to have\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"입력\",\n email: \"이메일 주소\",\n url: \"URL\",\n emoji: \"이모지\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO 날짜시간\",\n date: \"ISO 날짜\",\n time: \"ISO 시간\",\n duration: \"ISO 기간\",\n ipv4: \"IPv4 주소\",\n ipv6: \"IPv6 주소\",\n cidrv4: \"IPv4 범위\",\n cidrv6: \"IPv6 범위\",\n base64: \"base64 인코딩 문자열\",\n base64url: \"base64url 인코딩 문자열\",\n json_string: \"JSON 문자열\",\n e164: \"E.164 번호\",\n jwt: \"JWT\",\n template_literal: \"입력\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `잘못된 입력: 예상 타입은 ${issue.expected}, 받은 타입은 ${parsedType(issue.input)}입니다`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `잘못된 입력: 값은 ${util.stringifyPrimitive(issue.values[0])} 이어야 합니다`;\n return `잘못된 옵션: ${util.joinValues(issue.values, \"또는 \")} 중 하나여야 합니다`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"이하\" : \"미만\";\n const suffix = adj === \"미만\" ? \"이어야 합니다\" : \"여야 합니다\";\n const sizing = getSizing(issue.origin);\n const unit = sizing?.unit ?? \"요소\";\n if (sizing)\n return `${issue.origin ?? \"값\"}이 너무 큽니다: ${issue.maximum.toString()}${unit} ${adj}${suffix}`;\n return `${issue.origin ?? \"값\"}이 너무 큽니다: ${issue.maximum.toString()} ${adj}${suffix}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"이상\" : \"초과\";\n const suffix = adj === \"이상\" ? \"이어야 합니다\" : \"여야 합니다\";\n const sizing = getSizing(issue.origin);\n const unit = sizing?.unit ?? \"요소\";\n if (sizing) {\n return `${issue.origin ?? \"값\"}이 너무 작습니다: ${issue.minimum.toString()}${unit} ${adj}${suffix}`;\n }\n return `${issue.origin ?? \"값\"}이 너무 작습니다: ${issue.minimum.toString()} ${adj}${suffix}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `잘못된 문자열: \"${_issue.prefix}\"(으)로 시작해야 합니다`;\n }\n if (_issue.format === \"ends_with\")\n return `잘못된 문자열: \"${_issue.suffix}\"(으)로 끝나야 합니다`;\n if (_issue.format === \"includes\")\n return `잘못된 문자열: \"${_issue.includes}\"을(를) 포함해야 합니다`;\n if (_issue.format === \"regex\")\n return `잘못된 문자열: 정규식 ${_issue.pattern} 패턴과 일치해야 합니다`;\n return `잘못된 ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `잘못된 숫자: ${issue.divisor}의 배수여야 합니다`;\n case \"unrecognized_keys\":\n return `인식할 수 없는 키: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `잘못된 키: ${issue.origin}`;\n case \"invalid_union\":\n return `잘못된 입력`;\n case \"invalid_element\":\n return `잘못된 값: ${issue.origin}`;\n default:\n return `잘못된 입력`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n return parsedTypeFromType(t, data);\n};\nconst parsedTypeFromType = (t, data = undefined) => {\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"skaičius\";\n }\n case \"bigint\": {\n return \"sveikasis skaičius\";\n }\n case \"string\": {\n return \"eilutė\";\n }\n case \"boolean\": {\n return \"loginė reikšmė\";\n }\n case \"undefined\":\n case \"void\": {\n return \"neapibrėžta reikšmė\";\n }\n case \"function\": {\n return \"funkcija\";\n }\n case \"symbol\": {\n return \"simbolis\";\n }\n case \"object\": {\n if (data === undefined)\n return \"nežinomas objektas\";\n if (data === null)\n return \"nulinė reikšmė\";\n if (Array.isArray(data))\n return \"masyvas\";\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n return \"objektas\";\n }\n //Zod types below\n case \"null\": {\n return \"nulinė reikšmė\";\n }\n }\n return t;\n};\nconst capitalizeFirstCharacter = (text) => {\n return text.charAt(0).toUpperCase() + text.slice(1);\n};\nfunction getUnitTypeFromNumber(number) {\n const abs = Math.abs(number);\n const last = abs % 10;\n const last2 = abs % 100;\n if ((last2 >= 11 && last2 <= 19) || last === 0)\n return \"many\";\n if (last === 1)\n return \"one\";\n return \"few\";\n}\nconst error = () => {\n const Sizable = {\n string: {\n unit: {\n one: \"simbolis\",\n few: \"simboliai\",\n many: \"simbolių\",\n },\n verb: {\n smaller: {\n inclusive: \"turi būti ne ilgesnė kaip\",\n notInclusive: \"turi būti trumpesnė kaip\",\n },\n bigger: {\n inclusive: \"turi būti ne trumpesnė kaip\",\n notInclusive: \"turi būti ilgesnė kaip\",\n },\n },\n },\n file: {\n unit: {\n one: \"baitas\",\n few: \"baitai\",\n many: \"baitų\",\n },\n verb: {\n smaller: {\n inclusive: \"turi būti ne didesnis kaip\",\n notInclusive: \"turi būti mažesnis kaip\",\n },\n bigger: {\n inclusive: \"turi būti ne mažesnis kaip\",\n notInclusive: \"turi būti didesnis kaip\",\n },\n },\n },\n array: {\n unit: {\n one: \"elementą\",\n few: \"elementus\",\n many: \"elementų\",\n },\n verb: {\n smaller: {\n inclusive: \"turi turėti ne daugiau kaip\",\n notInclusive: \"turi turėti mažiau kaip\",\n },\n bigger: {\n inclusive: \"turi turėti ne mažiau kaip\",\n notInclusive: \"turi turėti daugiau kaip\",\n },\n },\n },\n set: {\n unit: {\n one: \"elementą\",\n few: \"elementus\",\n many: \"elementų\",\n },\n verb: {\n smaller: {\n inclusive: \"turi turėti ne daugiau kaip\",\n notInclusive: \"turi turėti mažiau kaip\",\n },\n bigger: {\n inclusive: \"turi turėti ne mažiau kaip\",\n notInclusive: \"turi turėti daugiau kaip\",\n },\n },\n },\n };\n function getSizing(origin, unitType, inclusive, targetShouldBe) {\n const result = Sizable[origin] ?? null;\n if (result === null)\n return result;\n return {\n unit: result.unit[unitType],\n verb: result.verb[targetShouldBe][inclusive ? \"inclusive\" : \"notInclusive\"],\n };\n }\n const Nouns = {\n regex: \"įvestis\",\n email: \"el. pašto adresas\",\n url: \"URL\",\n emoji: \"jaustukas\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO data ir laikas\",\n date: \"ISO data\",\n time: \"ISO laikas\",\n duration: \"ISO trukmė\",\n ipv4: \"IPv4 adresas\",\n ipv6: \"IPv6 adresas\",\n cidrv4: \"IPv4 tinklo prefiksas (CIDR)\",\n cidrv6: \"IPv6 tinklo prefiksas (CIDR)\",\n base64: \"base64 užkoduota eilutė\",\n base64url: \"base64url užkoduota eilutė\",\n json_string: \"JSON eilutė\",\n e164: \"E.164 numeris\",\n jwt: \"JWT\",\n template_literal: \"įvestis\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Gautas tipas ${parsedType(issue.input)}, o tikėtasi - ${parsedTypeFromType(issue.expected)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Privalo būti ${util.stringifyPrimitive(issue.values[0])}`;\n return `Privalo būti vienas iš ${util.joinValues(issue.values, \"|\")} pasirinkimų`;\n case \"too_big\": {\n const origin = parsedTypeFromType(issue.origin);\n const sizing = getSizing(issue.origin, getUnitTypeFromNumber(Number(issue.maximum)), issue.inclusive ?? false, \"smaller\");\n if (sizing?.verb)\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} ${sizing.verb} ${issue.maximum.toString()} ${sizing.unit ?? \"elementų\"}`;\n const adj = issue.inclusive ? \"ne didesnis kaip\" : \"mažesnis kaip\";\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} turi būti ${adj} ${issue.maximum.toString()} ${sizing?.unit}`;\n }\n case \"too_small\": {\n const origin = parsedTypeFromType(issue.origin);\n const sizing = getSizing(issue.origin, getUnitTypeFromNumber(Number(issue.minimum)), issue.inclusive ?? false, \"bigger\");\n if (sizing?.verb)\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} ${sizing.verb} ${issue.minimum.toString()} ${sizing.unit ?? \"elementų\"}`;\n const adj = issue.inclusive ? \"ne mažesnis kaip\" : \"didesnis kaip\";\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} turi būti ${adj} ${issue.minimum.toString()} ${sizing?.unit}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Eilutė privalo prasidėti \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Eilutė privalo pasibaigti \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Eilutė privalo įtraukti \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Eilutė privalo atitikti ${_issue.pattern}`;\n return `Neteisingas ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Skaičius privalo būti ${issue.divisor} kartotinis.`;\n case \"unrecognized_keys\":\n return `Neatpažint${issue.keys.length > 1 ? \"i\" : \"as\"} rakt${issue.keys.length > 1 ? \"ai\" : \"as\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return \"Rastas klaidingas raktas\";\n case \"invalid_union\":\n return \"Klaidinga įvestis\";\n case \"invalid_element\": {\n const origin = parsedTypeFromType(issue.origin);\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} turi klaidingą įvestį`;\n }\n default:\n return \"Klaidinga įvestis\";\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"знаци\", verb: \"да имаат\" },\n file: { unit: \"бајти\", verb: \"да имаат\" },\n array: { unit: \"ставки\", verb: \"да имаат\" },\n set: { unit: \"ставки\", verb: \"да имаат\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"број\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"низа\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"внес\",\n email: \"адреса на е-пошта\",\n url: \"URL\",\n emoji: \"емоџи\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO датум и време\",\n date: \"ISO датум\",\n time: \"ISO време\",\n duration: \"ISO времетраење\",\n ipv4: \"IPv4 адреса\",\n ipv6: \"IPv6 адреса\",\n cidrv4: \"IPv4 опсег\",\n cidrv6: \"IPv6 опсег\",\n base64: \"base64-енкодирана низа\",\n base64url: \"base64url-енкодирана низа\",\n json_string: \"JSON низа\",\n e164: \"E.164 број\",\n jwt: \"JWT\",\n template_literal: \"внес\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Грешен внес: се очекува ${issue.expected}, примено ${parsedType(issue.input)}`;\n // return `Invalid input: expected ${issue.expected}, received ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Invalid input: expected ${util.stringifyPrimitive(issue.values[0])}`;\n return `Грешана опција: се очекува една ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Премногу голем: се очекува ${issue.origin ?? \"вредноста\"} да има ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"елементи\"}`;\n return `Премногу голем: се очекува ${issue.origin ?? \"вредноста\"} да биде ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Премногу мал: се очекува ${issue.origin} да има ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Премногу мал: се очекува ${issue.origin} да биде ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Неважечка низа: мора да започнува со \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Неважечка низа: мора да завршува со \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Неважечка низа: мора да вклучува \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Неважечка низа: мора да одгоара на патернот ${_issue.pattern}`;\n return `Invalid ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Грешен број: мора да биде делив со ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Непрепознаени клучеви\" : \"Непрепознаен клуч\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Грешен клуч во ${issue.origin}`;\n case \"invalid_union\":\n return \"Грешен внес\";\n case \"invalid_element\":\n return `Грешна вредност во ${issue.origin}`;\n default:\n return `Грешен внес`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"aksara\", verb: \"mempunyai\" },\n file: { unit: \"bait\", verb: \"mempunyai\" },\n array: { unit: \"elemen\", verb: \"mempunyai\" },\n set: { unit: \"elemen\", verb: \"mempunyai\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"nombor\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"alamat e-mel\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"tarikh masa ISO\",\n date: \"tarikh ISO\",\n time: \"masa ISO\",\n duration: \"tempoh ISO\",\n ipv4: \"alamat IPv4\",\n ipv6: \"alamat IPv6\",\n cidrv4: \"julat IPv4\",\n cidrv6: \"julat IPv6\",\n base64: \"string dikodkan base64\",\n base64url: \"string dikodkan base64url\",\n json_string: \"string JSON\",\n e164: \"nombor E.164\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Input tidak sah: dijangka ${issue.expected}, diterima ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Input tidak sah: dijangka ${util.stringifyPrimitive(issue.values[0])}`;\n return `Pilihan tidak sah: dijangka salah satu daripada ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Terlalu besar: dijangka ${issue.origin ?? \"nilai\"} ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elemen\"}`;\n return `Terlalu besar: dijangka ${issue.origin ?? \"nilai\"} adalah ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Terlalu kecil: dijangka ${issue.origin} ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Terlalu kecil: dijangka ${issue.origin} adalah ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `String tidak sah: mesti bermula dengan \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `String tidak sah: mesti berakhir dengan \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `String tidak sah: mesti mengandungi \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `String tidak sah: mesti sepadan dengan corak ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} tidak sah`;\n }\n case \"not_multiple_of\":\n return `Nombor tidak sah: perlu gandaan ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Kunci tidak dikenali: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Kunci tidak sah dalam ${issue.origin}`;\n case \"invalid_union\":\n return \"Input tidak sah\";\n case \"invalid_element\":\n return `Nilai tidak sah dalam ${issue.origin}`;\n default:\n return `Input tidak sah`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"tekens\" },\n file: { unit: \"bytes\" },\n array: { unit: \"elementen\" },\n set: { unit: \"elementen\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"getal\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"invoer\",\n email: \"emailadres\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datum en tijd\",\n date: \"ISO datum\",\n time: \"ISO tijd\",\n duration: \"ISO duur\",\n ipv4: \"IPv4-adres\",\n ipv6: \"IPv6-adres\",\n cidrv4: \"IPv4-bereik\",\n cidrv6: \"IPv6-bereik\",\n base64: \"base64-gecodeerde tekst\",\n base64url: \"base64 URL-gecodeerde tekst\",\n json_string: \"JSON string\",\n e164: \"E.164-nummer\",\n jwt: \"JWT\",\n template_literal: \"invoer\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ongeldige invoer: verwacht ${issue.expected}, ontving ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ongeldige invoer: verwacht ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ongeldige optie: verwacht één van ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Te lang: verwacht dat ${issue.origin ?? \"waarde\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementen\"} bevat`;\n return `Te lang: verwacht dat ${issue.origin ?? \"waarde\"} ${adj}${issue.maximum.toString()} is`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Te kort: verwacht dat ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit} bevat`;\n }\n return `Te kort: verwacht dat ${issue.origin} ${adj}${issue.minimum.toString()} is`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Ongeldige tekst: moet met \"${_issue.prefix}\" beginnen`;\n }\n if (_issue.format === \"ends_with\")\n return `Ongeldige tekst: moet op \"${_issue.suffix}\" eindigen`;\n if (_issue.format === \"includes\")\n return `Ongeldige tekst: moet \"${_issue.includes}\" bevatten`;\n if (_issue.format === \"regex\")\n return `Ongeldige tekst: moet overeenkomen met patroon ${_issue.pattern}`;\n return `Ongeldig: ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ongeldig getal: moet een veelvoud van ${issue.divisor} zijn`;\n case \"unrecognized_keys\":\n return `Onbekende key${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ongeldige key in ${issue.origin}`;\n case \"invalid_union\":\n return \"Ongeldige invoer\";\n case \"invalid_element\":\n return `Ongeldige waarde in ${issue.origin}`;\n default:\n return `Ongeldige invoer`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"tegn\", verb: \"å ha\" },\n file: { unit: \"bytes\", verb: \"å ha\" },\n array: { unit: \"elementer\", verb: \"å inneholde\" },\n set: { unit: \"elementer\", verb: \"å inneholde\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"tall\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"liste\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"e-postadresse\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO dato- og klokkeslett\",\n date: \"ISO-dato\",\n time: \"ISO-klokkeslett\",\n duration: \"ISO-varighet\",\n ipv4: \"IPv4-område\",\n ipv6: \"IPv6-område\",\n cidrv4: \"IPv4-spekter\",\n cidrv6: \"IPv6-spekter\",\n base64: \"base64-enkodet streng\",\n base64url: \"base64url-enkodet streng\",\n json_string: \"JSON-streng\",\n e164: \"E.164-nummer\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ugyldig input: forventet ${issue.expected}, fikk ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ugyldig verdi: forventet ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ugyldig valg: forventet en av ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `For stor(t): forventet ${issue.origin ?? \"value\"} til å ha ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementer\"}`;\n return `For stor(t): forventet ${issue.origin ?? \"value\"} til å ha ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `For lite(n): forventet ${issue.origin} til å ha ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `For lite(n): forventet ${issue.origin} til å ha ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Ugyldig streng: må starte med \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Ugyldig streng: må ende med \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ugyldig streng: må inneholde \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ugyldig streng: må matche mønsteret ${_issue.pattern}`;\n return `Ugyldig ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ugyldig tall: må være et multiplum av ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Ukjente nøkler\" : \"Ukjent nøkkel\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ugyldig nøkkel i ${issue.origin}`;\n case \"invalid_union\":\n return \"Ugyldig input\";\n case \"invalid_element\":\n return `Ugyldig verdi i ${issue.origin}`;\n default:\n return `Ugyldig input`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"harf\", verb: \"olmalıdır\" },\n file: { unit: \"bayt\", verb: \"olmalıdır\" },\n array: { unit: \"unsur\", verb: \"olmalıdır\" },\n set: { unit: \"unsur\", verb: \"olmalıdır\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"numara\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"saf\";\n }\n if (data === null) {\n return \"gayb\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"giren\",\n email: \"epostagâh\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO hengâmı\",\n date: \"ISO tarihi\",\n time: \"ISO zamanı\",\n duration: \"ISO müddeti\",\n ipv4: \"IPv4 nişânı\",\n ipv6: \"IPv6 nişânı\",\n cidrv4: \"IPv4 menzili\",\n cidrv6: \"IPv6 menzili\",\n base64: \"base64-şifreli metin\",\n base64url: \"base64url-şifreli metin\",\n json_string: \"JSON metin\",\n e164: \"E.164 sayısı\",\n jwt: \"JWT\",\n template_literal: \"giren\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Fâsit giren: umulan ${issue.expected}, alınan ${parsedType(issue.input)}`;\n // return `Fâsit giren: umulan ${issue.expected}, alınan ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Fâsit giren: umulan ${util.stringifyPrimitive(issue.values[0])}`;\n return `Fâsit tercih: mûteberler ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Fazla büyük: ${issue.origin ?? \"value\"}, ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"} sahip olmalıydı.`;\n return `Fazla büyük: ${issue.origin ?? \"value\"}, ${adj}${issue.maximum.toString()} olmalıydı.`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Fazla küçük: ${issue.origin}, ${adj}${issue.minimum.toString()} ${sizing.unit} sahip olmalıydı.`;\n }\n return `Fazla küçük: ${issue.origin}, ${adj}${issue.minimum.toString()} olmalıydı.`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Fâsit metin: \"${_issue.prefix}\" ile başlamalı.`;\n if (_issue.format === \"ends_with\")\n return `Fâsit metin: \"${_issue.suffix}\" ile bitmeli.`;\n if (_issue.format === \"includes\")\n return `Fâsit metin: \"${_issue.includes}\" ihtivâ etmeli.`;\n if (_issue.format === \"regex\")\n return `Fâsit metin: ${_issue.pattern} nakşına uymalı.`;\n return `Fâsit ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Fâsit sayı: ${issue.divisor} katı olmalıydı.`;\n case \"unrecognized_keys\":\n return `Tanınmayan anahtar ${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} için tanınmayan anahtar var.`;\n case \"invalid_union\":\n return \"Giren tanınamadı.\";\n case \"invalid_element\":\n return `${issue.origin} için tanınmayan kıymet var.`;\n default:\n return `Kıymet tanınamadı.`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"توکي\", verb: \"ولري\" },\n file: { unit: \"بایټس\", verb: \"ولري\" },\n array: { unit: \"توکي\", verb: \"ولري\" },\n set: { unit: \"توکي\", verb: \"ولري\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"عدد\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"ارې\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ورودي\",\n email: \"بریښنالیک\",\n url: \"یو آر ال\",\n emoji: \"ایموجي\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"نیټه او وخت\",\n date: \"نېټه\",\n time: \"وخت\",\n duration: \"موده\",\n ipv4: \"د IPv4 پته\",\n ipv6: \"د IPv6 پته\",\n cidrv4: \"د IPv4 ساحه\",\n cidrv6: \"د IPv6 ساحه\",\n base64: \"base64-encoded متن\",\n base64url: \"base64url-encoded متن\",\n json_string: \"JSON متن\",\n e164: \"د E.164 شمېره\",\n jwt: \"JWT\",\n template_literal: \"ورودي\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `ناسم ورودي: باید ${issue.expected} وای, مګر ${parsedType(issue.input)} ترلاسه شو`;\n case \"invalid_value\":\n if (issue.values.length === 1) {\n return `ناسم ورودي: باید ${util.stringifyPrimitive(issue.values[0])} وای`;\n }\n return `ناسم انتخاب: باید یو له ${util.joinValues(issue.values, \"|\")} څخه وای`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `ډیر لوی: ${issue.origin ?? \"ارزښت\"} باید ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"عنصرونه\"} ولري`;\n }\n return `ډیر لوی: ${issue.origin ?? \"ارزښت\"} باید ${adj}${issue.maximum.toString()} وي`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `ډیر کوچنی: ${issue.origin} باید ${adj}${issue.minimum.toString()} ${sizing.unit} ولري`;\n }\n return `ډیر کوچنی: ${issue.origin} باید ${adj}${issue.minimum.toString()} وي`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `ناسم متن: باید د \"${_issue.prefix}\" سره پیل شي`;\n }\n if (_issue.format === \"ends_with\") {\n return `ناسم متن: باید د \"${_issue.suffix}\" سره پای ته ورسيږي`;\n }\n if (_issue.format === \"includes\") {\n return `ناسم متن: باید \"${_issue.includes}\" ولري`;\n }\n if (_issue.format === \"regex\") {\n return `ناسم متن: باید د ${_issue.pattern} سره مطابقت ولري`;\n }\n return `${Nouns[_issue.format] ?? issue.format} ناسم دی`;\n }\n case \"not_multiple_of\":\n return `ناسم عدد: باید د ${issue.divisor} مضرب وي`;\n case \"unrecognized_keys\":\n return `ناسم ${issue.keys.length > 1 ? \"کلیډونه\" : \"کلیډ\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `ناسم کلیډ په ${issue.origin} کې`;\n case \"invalid_union\":\n return `ناسمه ورودي`;\n case \"invalid_element\":\n return `ناسم عنصر په ${issue.origin} کې`;\n default:\n return `ناسمه ورودي`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"znaków\", verb: \"mieć\" },\n file: { unit: \"bajtów\", verb: \"mieć\" },\n array: { unit: \"elementów\", verb: \"mieć\" },\n set: { unit: \"elementów\", verb: \"mieć\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"liczba\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tablica\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"wyrażenie\",\n email: \"adres email\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"data i godzina w formacie ISO\",\n date: \"data w formacie ISO\",\n time: \"godzina w formacie ISO\",\n duration: \"czas trwania ISO\",\n ipv4: \"adres IPv4\",\n ipv6: \"adres IPv6\",\n cidrv4: \"zakres IPv4\",\n cidrv6: \"zakres IPv6\",\n base64: \"ciąg znaków zakodowany w formacie base64\",\n base64url: \"ciąg znaków zakodowany w formacie base64url\",\n json_string: \"ciąg znaków w formacie JSON\",\n e164: \"liczba E.164\",\n jwt: \"JWT\",\n template_literal: \"wejście\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Nieprawidłowe dane wejściowe: oczekiwano ${issue.expected}, otrzymano ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Nieprawidłowe dane wejściowe: oczekiwano ${util.stringifyPrimitive(issue.values[0])}`;\n return `Nieprawidłowa opcja: oczekiwano jednej z wartości ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Za duża wartość: oczekiwano, że ${issue.origin ?? \"wartość\"} będzie mieć ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementów\"}`;\n }\n return `Zbyt duż(y/a/e): oczekiwano, że ${issue.origin ?? \"wartość\"} będzie wynosić ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Za mała wartość: oczekiwano, że ${issue.origin ?? \"wartość\"} będzie mieć ${adj}${issue.minimum.toString()} ${sizing.unit ?? \"elementów\"}`;\n }\n return `Zbyt mał(y/a/e): oczekiwano, że ${issue.origin ?? \"wartość\"} będzie wynosić ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Nieprawidłowy ciąg znaków: musi zaczynać się od \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Nieprawidłowy ciąg znaków: musi kończyć się na \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Nieprawidłowy ciąg znaków: musi zawierać \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Nieprawidłowy ciąg znaków: musi odpowiadać wzorcowi ${_issue.pattern}`;\n return `Nieprawidłow(y/a/e) ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Nieprawidłowa liczba: musi być wielokrotnością ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Nierozpoznane klucze${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Nieprawidłowy klucz w ${issue.origin}`;\n case \"invalid_union\":\n return \"Nieprawidłowe dane wejściowe\";\n case \"invalid_element\":\n return `Nieprawidłowa wartość w ${issue.origin}`;\n default:\n return `Nieprawidłowe dane wejściowe`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caracteres\", verb: \"ter\" },\n file: { unit: \"bytes\", verb: \"ter\" },\n array: { unit: \"itens\", verb: \"ter\" },\n set: { unit: \"itens\", verb: \"ter\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"número\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"nulo\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"padrão\",\n email: \"endereço de e-mail\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"data e hora ISO\",\n date: \"data ISO\",\n time: \"hora ISO\",\n duration: \"duração ISO\",\n ipv4: \"endereço IPv4\",\n ipv6: \"endereço IPv6\",\n cidrv4: \"faixa de IPv4\",\n cidrv6: \"faixa de IPv6\",\n base64: \"texto codificado em base64\",\n base64url: \"URL codificada em base64\",\n json_string: \"texto JSON\",\n e164: \"número E.164\",\n jwt: \"JWT\",\n template_literal: \"entrada\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Tipo inválido: esperado ${issue.expected}, recebido ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Entrada inválida: esperado ${util.stringifyPrimitive(issue.values[0])}`;\n return `Opção inválida: esperada uma das ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Muito grande: esperado que ${issue.origin ?? \"valor\"} tivesse ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementos\"}`;\n return `Muito grande: esperado que ${issue.origin ?? \"valor\"} fosse ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Muito pequeno: esperado que ${issue.origin} tivesse ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Muito pequeno: esperado que ${issue.origin} fosse ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Texto inválido: deve começar com \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Texto inválido: deve terminar com \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Texto inválido: deve incluir \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Texto inválido: deve corresponder ao padrão ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} inválido`;\n }\n case \"not_multiple_of\":\n return `Número inválido: deve ser múltiplo de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Chave${issue.keys.length > 1 ? \"s\" : \"\"} desconhecida${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Chave inválida em ${issue.origin}`;\n case \"invalid_union\":\n return \"Entrada inválida\";\n case \"invalid_element\":\n return `Valor inválido em ${issue.origin}`;\n default:\n return `Campo inválido`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nfunction getRussianPlural(count, one, few, many) {\n const absCount = Math.abs(count);\n const lastDigit = absCount % 10;\n const lastTwoDigits = absCount % 100;\n if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {\n return many;\n }\n if (lastDigit === 1) {\n return one;\n }\n if (lastDigit >= 2 && lastDigit <= 4) {\n return few;\n }\n return many;\n}\nconst error = () => {\n const Sizable = {\n string: {\n unit: {\n one: \"символ\",\n few: \"символа\",\n many: \"символов\",\n },\n verb: \"иметь\",\n },\n file: {\n unit: {\n one: \"байт\",\n few: \"байта\",\n many: \"байт\",\n },\n verb: \"иметь\",\n },\n array: {\n unit: {\n one: \"элемент\",\n few: \"элемента\",\n many: \"элементов\",\n },\n verb: \"иметь\",\n },\n set: {\n unit: {\n one: \"элемент\",\n few: \"элемента\",\n many: \"элементов\",\n },\n verb: \"иметь\",\n },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"число\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"массив\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ввод\",\n email: \"email адрес\",\n url: \"URL\",\n emoji: \"эмодзи\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO дата и время\",\n date: \"ISO дата\",\n time: \"ISO время\",\n duration: \"ISO длительность\",\n ipv4: \"IPv4 адрес\",\n ipv6: \"IPv6 адрес\",\n cidrv4: \"IPv4 диапазон\",\n cidrv6: \"IPv6 диапазон\",\n base64: \"строка в формате base64\",\n base64url: \"строка в формате base64url\",\n json_string: \"JSON строка\",\n e164: \"номер E.164\",\n jwt: \"JWT\",\n template_literal: \"ввод\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Неверный ввод: ожидалось ${issue.expected}, получено ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Неверный ввод: ожидалось ${util.stringifyPrimitive(issue.values[0])}`;\n return `Неверный вариант: ожидалось одно из ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n const maxValue = Number(issue.maximum);\n const unit = getRussianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);\n return `Слишком большое значение: ожидалось, что ${issue.origin ?? \"значение\"} будет иметь ${adj}${issue.maximum.toString()} ${unit}`;\n }\n return `Слишком большое значение: ожидалось, что ${issue.origin ?? \"значение\"} будет ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n const minValue = Number(issue.minimum);\n const unit = getRussianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);\n return `Слишком маленькое значение: ожидалось, что ${issue.origin} будет иметь ${adj}${issue.minimum.toString()} ${unit}`;\n }\n return `Слишком маленькое значение: ожидалось, что ${issue.origin} будет ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Неверная строка: должна начинаться с \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Неверная строка: должна заканчиваться на \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Неверная строка: должна содержать \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Неверная строка: должна соответствовать шаблону ${_issue.pattern}`;\n return `Неверный ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Неверное число: должно быть кратным ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Нераспознанн${issue.keys.length > 1 ? \"ые\" : \"ый\"} ключ${issue.keys.length > 1 ? \"и\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Неверный ключ в ${issue.origin}`;\n case \"invalid_union\":\n return \"Неверные входные данные\";\n case \"invalid_element\":\n return `Неверное значение в ${issue.origin}`;\n default:\n return `Неверные входные данные`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"znakov\", verb: \"imeti\" },\n file: { unit: \"bajtov\", verb: \"imeti\" },\n array: { unit: \"elementov\", verb: \"imeti\" },\n set: { unit: \"elementov\", verb: \"imeti\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"število\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tabela\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"vnos\",\n email: \"e-poštni naslov\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datum in čas\",\n date: \"ISO datum\",\n time: \"ISO čas\",\n duration: \"ISO trajanje\",\n ipv4: \"IPv4 naslov\",\n ipv6: \"IPv6 naslov\",\n cidrv4: \"obseg IPv4\",\n cidrv6: \"obseg IPv6\",\n base64: \"base64 kodiran niz\",\n base64url: \"base64url kodiran niz\",\n json_string: \"JSON niz\",\n e164: \"E.164 številka\",\n jwt: \"JWT\",\n template_literal: \"vnos\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Neveljaven vnos: pričakovano ${issue.expected}, prejeto ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Neveljaven vnos: pričakovano ${util.stringifyPrimitive(issue.values[0])}`;\n return `Neveljavna možnost: pričakovano eno izmed ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Preveliko: pričakovano, da bo ${issue.origin ?? \"vrednost\"} imelo ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementov\"}`;\n return `Preveliko: pričakovano, da bo ${issue.origin ?? \"vrednost\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Premajhno: pričakovano, da bo ${issue.origin} imelo ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Premajhno: pričakovano, da bo ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Neveljaven niz: mora se začeti z \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Neveljaven niz: mora se končati z \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Neveljaven niz: mora vsebovati \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Neveljaven niz: mora ustrezati vzorcu ${_issue.pattern}`;\n return `Neveljaven ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Neveljavno število: mora biti večkratnik ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Neprepoznan${issue.keys.length > 1 ? \"i ključi\" : \" ključ\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Neveljaven ključ v ${issue.origin}`;\n case \"invalid_union\":\n return \"Neveljaven vnos\";\n case \"invalid_element\":\n return `Neveljavna vrednost v ${issue.origin}`;\n default:\n return \"Neveljaven vnos\";\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"tecken\", verb: \"att ha\" },\n file: { unit: \"bytes\", verb: \"att ha\" },\n array: { unit: \"objekt\", verb: \"att innehålla\" },\n set: { unit: \"objekt\", verb: \"att innehålla\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"antal\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"lista\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"reguljärt uttryck\",\n email: \"e-postadress\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO-datum och tid\",\n date: \"ISO-datum\",\n time: \"ISO-tid\",\n duration: \"ISO-varaktighet\",\n ipv4: \"IPv4-intervall\",\n ipv6: \"IPv6-intervall\",\n cidrv4: \"IPv4-spektrum\",\n cidrv6: \"IPv6-spektrum\",\n base64: \"base64-kodad sträng\",\n base64url: \"base64url-kodad sträng\",\n json_string: \"JSON-sträng\",\n e164: \"E.164-nummer\",\n jwt: \"JWT\",\n template_literal: \"mall-literal\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ogiltig inmatning: förväntat ${issue.expected}, fick ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ogiltig inmatning: förväntat ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ogiltigt val: förväntade en av ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `För stor(t): förväntade ${issue.origin ?? \"värdet\"} att ha ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"element\"}`;\n }\n return `För stor(t): förväntat ${issue.origin ?? \"värdet\"} att ha ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `För lite(t): förväntade ${issue.origin ?? \"värdet\"} att ha ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `För lite(t): förväntade ${issue.origin ?? \"värdet\"} att ha ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Ogiltig sträng: måste börja med \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Ogiltig sträng: måste sluta med \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ogiltig sträng: måste innehålla \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ogiltig sträng: måste matcha mönstret \"${_issue.pattern}\"`;\n return `Ogiltig(t) ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ogiltigt tal: måste vara en multipel av ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Okända nycklar\" : \"Okänd nyckel\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ogiltig nyckel i ${issue.origin ?? \"värdet\"}`;\n case \"invalid_union\":\n return \"Ogiltig input\";\n case \"invalid_element\":\n return `Ogiltigt värde i ${issue.origin ?? \"värdet\"}`;\n default:\n return `Ogiltig input`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"எழுத்துக்கள்\", verb: \"கொண்டிருக்க வேண்டும்\" },\n file: { unit: \"பைட்டுகள்\", verb: \"கொண்டிருக்க வேண்டும்\" },\n array: { unit: \"உறுப்புகள்\", verb: \"கொண்டிருக்க வேண்டும்\" },\n set: { unit: \"உறுப்புகள்\", verb: \"கொண்டிருக்க வேண்டும்\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"எண் அல்லாதது\" : \"எண்\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"அணி\";\n }\n if (data === null) {\n return \"வெறுமை\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"உள்ளீடு\",\n email: \"மின்னஞ்சல் முகவரி\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO தேதி நேரம்\",\n date: \"ISO தேதி\",\n time: \"ISO நேரம்\",\n duration: \"ISO கால அளவு\",\n ipv4: \"IPv4 முகவரி\",\n ipv6: \"IPv6 முகவரி\",\n cidrv4: \"IPv4 வரம்பு\",\n cidrv6: \"IPv6 வரம்பு\",\n base64: \"base64-encoded சரம்\",\n base64url: \"base64url-encoded சரம்\",\n json_string: \"JSON சரம்\",\n e164: \"E.164 எண்\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `தவறான உள்ளீடு: எதிர்பார்க்கப்பட்டது ${issue.expected}, பெறப்பட்டது ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `தவறான உள்ளீடு: எதிர்பார்க்கப்பட்டது ${util.stringifyPrimitive(issue.values[0])}`;\n return `தவறான விருப்பம்: எதிர்பார்க்கப்பட்டது ${util.joinValues(issue.values, \"|\")} இல் ஒன்று`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `மிக பெரியது: எதிர்பார்க்கப்பட்டது ${issue.origin ?? \"மதிப்பு\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"உறுப்புகள்\"} ஆக இருக்க வேண்டும்`;\n }\n return `மிக பெரியது: எதிர்பார்க்கப்பட்டது ${issue.origin ?? \"மதிப்பு\"} ${adj}${issue.maximum.toString()} ஆக இருக்க வேண்டும்`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `மிகச் சிறியது: எதிர்பார்க்கப்பட்டது ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit} ஆக இருக்க வேண்டும்`; //\n }\n return `மிகச் சிறியது: எதிர்பார்க்கப்பட்டது ${issue.origin} ${adj}${issue.minimum.toString()} ஆக இருக்க வேண்டும்`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `தவறான சரம்: \"${_issue.prefix}\" இல் தொடங்க வேண்டும்`;\n if (_issue.format === \"ends_with\")\n return `தவறான சரம்: \"${_issue.suffix}\" இல் முடிவடைய வேண்டும்`;\n if (_issue.format === \"includes\")\n return `தவறான சரம்: \"${_issue.includes}\" ஐ உள்ளடக்க வேண்டும்`;\n if (_issue.format === \"regex\")\n return `தவறான சரம்: ${_issue.pattern} முறைபாட்டுடன் பொருந்த வேண்டும்`;\n return `தவறான ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `தவறான எண்: ${issue.divisor} இன் பலமாக இருக்க வேண்டும்`;\n case \"unrecognized_keys\":\n return `அடையாளம் தெரியாத விசை${issue.keys.length > 1 ? \"கள்\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} இல் தவறான விசை`;\n case \"invalid_union\":\n return \"தவறான உள்ளீடு\";\n case \"invalid_element\":\n return `${issue.origin} இல் தவறான மதிப்பு`;\n default:\n return `தவறான உள்ளீடு`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"ตัวอักษร\", verb: \"ควรมี\" },\n file: { unit: \"ไบต์\", verb: \"ควรมี\" },\n array: { unit: \"รายการ\", verb: \"ควรมี\" },\n set: { unit: \"รายการ\", verb: \"ควรมี\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"ไม่ใช่ตัวเลข (NaN)\" : \"ตัวเลข\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"อาร์เรย์ (Array)\";\n }\n if (data === null) {\n return \"ไม่มีค่า (null)\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ข้อมูลที่ป้อน\",\n email: \"ที่อยู่อีเมล\",\n url: \"URL\",\n emoji: \"อิโมจิ\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"วันที่เวลาแบบ ISO\",\n date: \"วันที่แบบ ISO\",\n time: \"เวลาแบบ ISO\",\n duration: \"ช่วงเวลาแบบ ISO\",\n ipv4: \"ที่อยู่ IPv4\",\n ipv6: \"ที่อยู่ IPv6\",\n cidrv4: \"ช่วง IP แบบ IPv4\",\n cidrv6: \"ช่วง IP แบบ IPv6\",\n base64: \"ข้อความแบบ Base64\",\n base64url: \"ข้อความแบบ Base64 สำหรับ URL\",\n json_string: \"ข้อความแบบ JSON\",\n e164: \"เบอร์โทรศัพท์ระหว่างประเทศ (E.164)\",\n jwt: \"โทเคน JWT\",\n template_literal: \"ข้อมูลที่ป้อน\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `ประเภทข้อมูลไม่ถูกต้อง: ควรเป็น ${issue.expected} แต่ได้รับ ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `ค่าไม่ถูกต้อง: ควรเป็น ${util.stringifyPrimitive(issue.values[0])}`;\n return `ตัวเลือกไม่ถูกต้อง: ควรเป็นหนึ่งใน ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"ไม่เกิน\" : \"น้อยกว่า\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `เกินกำหนด: ${issue.origin ?? \"ค่า\"} ควรมี${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"รายการ\"}`;\n return `เกินกำหนด: ${issue.origin ?? \"ค่า\"} ควรมี${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"อย่างน้อย\" : \"มากกว่า\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `น้อยกว่ากำหนด: ${issue.origin} ควรมี${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `น้อยกว่ากำหนด: ${issue.origin} ควรมี${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `รูปแบบไม่ถูกต้อง: ข้อความต้องขึ้นต้นด้วย \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `รูปแบบไม่ถูกต้อง: ข้อความต้องลงท้ายด้วย \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `รูปแบบไม่ถูกต้อง: ข้อความต้องมี \"${_issue.includes}\" อยู่ในข้อความ`;\n if (_issue.format === \"regex\")\n return `รูปแบบไม่ถูกต้อง: ต้องตรงกับรูปแบบที่กำหนด ${_issue.pattern}`;\n return `รูปแบบไม่ถูกต้อง: ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `ตัวเลขไม่ถูกต้อง: ต้องเป็นจำนวนที่หารด้วย ${issue.divisor} ได้ลงตัว`;\n case \"unrecognized_keys\":\n return `พบคีย์ที่ไม่รู้จัก: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `คีย์ไม่ถูกต้องใน ${issue.origin}`;\n case \"invalid_union\":\n return \"ข้อมูลไม่ถูกต้อง: ไม่ตรงกับรูปแบบยูเนียนที่กำหนดไว้\";\n case \"invalid_element\":\n return `ข้อมูลไม่ถูกต้องใน ${issue.origin}`;\n default:\n return `ข้อมูลไม่ถูกต้อง`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"karakter\", verb: \"olmalı\" },\n file: { unit: \"bayt\", verb: \"olmalı\" },\n array: { unit: \"öğe\", verb: \"olmalı\" },\n set: { unit: \"öğe\", verb: \"olmalı\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"girdi\",\n email: \"e-posta adresi\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO tarih ve saat\",\n date: \"ISO tarih\",\n time: \"ISO saat\",\n duration: \"ISO süre\",\n ipv4: \"IPv4 adresi\",\n ipv6: \"IPv6 adresi\",\n cidrv4: \"IPv4 aralığı\",\n cidrv6: \"IPv6 aralığı\",\n base64: \"base64 ile şifrelenmiş metin\",\n base64url: \"base64url ile şifrelenmiş metin\",\n json_string: \"JSON dizesi\",\n e164: \"E.164 sayısı\",\n jwt: \"JWT\",\n template_literal: \"Şablon dizesi\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Geçersiz değer: beklenen ${issue.expected}, alınan ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Geçersiz değer: beklenen ${util.stringifyPrimitive(issue.values[0])}`;\n return `Geçersiz seçenek: aşağıdakilerden biri olmalı: ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Çok büyük: beklenen ${issue.origin ?? \"değer\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"öğe\"}`;\n return `Çok büyük: beklenen ${issue.origin ?? \"değer\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Çok küçük: beklenen ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Çok küçük: beklenen ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Geçersiz metin: \"${_issue.prefix}\" ile başlamalı`;\n if (_issue.format === \"ends_with\")\n return `Geçersiz metin: \"${_issue.suffix}\" ile bitmeli`;\n if (_issue.format === \"includes\")\n return `Geçersiz metin: \"${_issue.includes}\" içermeli`;\n if (_issue.format === \"regex\")\n return `Geçersiz metin: ${_issue.pattern} desenine uymalı`;\n return `Geçersiz ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Geçersiz sayı: ${issue.divisor} ile tam bölünebilmeli`;\n case \"unrecognized_keys\":\n return `Tanınmayan anahtar${issue.keys.length > 1 ? \"lar\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} içinde geçersiz anahtar`;\n case \"invalid_union\":\n return \"Geçersiz değer\";\n case \"invalid_element\":\n return `${issue.origin} içinde geçersiz değer`;\n default:\n return `Geçersiz değer`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"символів\", verb: \"матиме\" },\n file: { unit: \"байтів\", verb: \"матиме\" },\n array: { unit: \"елементів\", verb: \"матиме\" },\n set: { unit: \"елементів\", verb: \"матиме\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"число\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"масив\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"вхідні дані\",\n email: \"адреса електронної пошти\",\n url: \"URL\",\n emoji: \"емодзі\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"дата та час ISO\",\n date: \"дата ISO\",\n time: \"час ISO\",\n duration: \"тривалість ISO\",\n ipv4: \"адреса IPv4\",\n ipv6: \"адреса IPv6\",\n cidrv4: \"діапазон IPv4\",\n cidrv6: \"діапазон IPv6\",\n base64: \"рядок у кодуванні base64\",\n base64url: \"рядок у кодуванні base64url\",\n json_string: \"рядок JSON\",\n e164: \"номер E.164\",\n jwt: \"JWT\",\n template_literal: \"вхідні дані\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Неправильні вхідні дані: очікується ${issue.expected}, отримано ${parsedType(issue.input)}`;\n // return `Неправильні вхідні дані: очікується ${issue.expected}, отримано ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Неправильні вхідні дані: очікується ${util.stringifyPrimitive(issue.values[0])}`;\n return `Неправильна опція: очікується одне з ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Занадто велике: очікується, що ${issue.origin ?? \"значення\"} ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"елементів\"}`;\n return `Занадто велике: очікується, що ${issue.origin ?? \"значення\"} буде ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Занадто мале: очікується, що ${issue.origin} ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Занадто мале: очікується, що ${issue.origin} буде ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Неправильний рядок: повинен починатися з \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Неправильний рядок: повинен закінчуватися на \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Неправильний рядок: повинен містити \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Неправильний рядок: повинен відповідати шаблону ${_issue.pattern}`;\n return `Неправильний ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Неправильне число: повинно бути кратним ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Нерозпізнаний ключ${issue.keys.length > 1 ? \"і\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Неправильний ключ у ${issue.origin}`;\n case \"invalid_union\":\n return \"Неправильні вхідні дані\";\n case \"invalid_element\":\n return `Неправильне значення у ${issue.origin}`;\n default:\n return `Неправильні вхідні дані`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import uk from \"./uk.js\";\n/** @deprecated Use `uk` instead. */\nexport default function () {\n return uk();\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"حروف\", verb: \"ہونا\" },\n file: { unit: \"بائٹس\", verb: \"ہونا\" },\n array: { unit: \"آئٹمز\", verb: \"ہونا\" },\n set: { unit: \"آئٹمز\", verb: \"ہونا\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"نمبر\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"آرے\";\n }\n if (data === null) {\n return \"نل\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ان پٹ\",\n email: \"ای میل ایڈریس\",\n url: \"یو آر ایل\",\n emoji: \"ایموجی\",\n uuid: \"یو یو آئی ڈی\",\n uuidv4: \"یو یو آئی ڈی وی 4\",\n uuidv6: \"یو یو آئی ڈی وی 6\",\n nanoid: \"نینو آئی ڈی\",\n guid: \"جی یو آئی ڈی\",\n cuid: \"سی یو آئی ڈی\",\n cuid2: \"سی یو آئی ڈی 2\",\n ulid: \"یو ایل آئی ڈی\",\n xid: \"ایکس آئی ڈی\",\n ksuid: \"کے ایس یو آئی ڈی\",\n datetime: \"آئی ایس او ڈیٹ ٹائم\",\n date: \"آئی ایس او تاریخ\",\n time: \"آئی ایس او وقت\",\n duration: \"آئی ایس او مدت\",\n ipv4: \"آئی پی وی 4 ایڈریس\",\n ipv6: \"آئی پی وی 6 ایڈریس\",\n cidrv4: \"آئی پی وی 4 رینج\",\n cidrv6: \"آئی پی وی 6 رینج\",\n base64: \"بیس 64 ان کوڈڈ سٹرنگ\",\n base64url: \"بیس 64 یو آر ایل ان کوڈڈ سٹرنگ\",\n json_string: \"جے ایس او این سٹرنگ\",\n e164: \"ای 164 نمبر\",\n jwt: \"جے ڈبلیو ٹی\",\n template_literal: \"ان پٹ\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `غلط ان پٹ: ${issue.expected} متوقع تھا، ${parsedType(issue.input)} موصول ہوا`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `غلط ان پٹ: ${util.stringifyPrimitive(issue.values[0])} متوقع تھا`;\n return `غلط آپشن: ${util.joinValues(issue.values, \"|\")} میں سے ایک متوقع تھا`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `بہت بڑا: ${issue.origin ?? \"ویلیو\"} کے ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"عناصر\"} ہونے متوقع تھے`;\n return `بہت بڑا: ${issue.origin ?? \"ویلیو\"} کا ${adj}${issue.maximum.toString()} ہونا متوقع تھا`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `بہت چھوٹا: ${issue.origin} کے ${adj}${issue.minimum.toString()} ${sizing.unit} ہونے متوقع تھے`;\n }\n return `بہت چھوٹا: ${issue.origin} کا ${adj}${issue.minimum.toString()} ہونا متوقع تھا`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `غلط سٹرنگ: \"${_issue.prefix}\" سے شروع ہونا چاہیے`;\n }\n if (_issue.format === \"ends_with\")\n return `غلط سٹرنگ: \"${_issue.suffix}\" پر ختم ہونا چاہیے`;\n if (_issue.format === \"includes\")\n return `غلط سٹرنگ: \"${_issue.includes}\" شامل ہونا چاہیے`;\n if (_issue.format === \"regex\")\n return `غلط سٹرنگ: پیٹرن ${_issue.pattern} سے میچ ہونا چاہیے`;\n return `غلط ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `غلط نمبر: ${issue.divisor} کا مضاعف ہونا چاہیے`;\n case \"unrecognized_keys\":\n return `غیر تسلیم شدہ کی${issue.keys.length > 1 ? \"ز\" : \"\"}: ${util.joinValues(issue.keys, \"، \")}`;\n case \"invalid_key\":\n return `${issue.origin} میں غلط کی`;\n case \"invalid_union\":\n return \"غلط ان پٹ\";\n case \"invalid_element\":\n return `${issue.origin} میں غلط ویلیو`;\n default:\n return `غلط ان پٹ`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"ký tự\", verb: \"có\" },\n file: { unit: \"byte\", verb: \"có\" },\n array: { unit: \"phần tử\", verb: \"có\" },\n set: { unit: \"phần tử\", verb: \"có\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"số\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"mảng\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"đầu vào\",\n email: \"địa chỉ email\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ngày giờ ISO\",\n date: \"ngày ISO\",\n time: \"giờ ISO\",\n duration: \"khoảng thời gian ISO\",\n ipv4: \"địa chỉ IPv4\",\n ipv6: \"địa chỉ IPv6\",\n cidrv4: \"dải IPv4\",\n cidrv6: \"dải IPv6\",\n base64: \"chuỗi mã hóa base64\",\n base64url: \"chuỗi mã hóa base64url\",\n json_string: \"chuỗi JSON\",\n e164: \"số E.164\",\n jwt: \"JWT\",\n template_literal: \"đầu vào\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Đầu vào không hợp lệ: mong đợi ${issue.expected}, nhận được ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Đầu vào không hợp lệ: mong đợi ${util.stringifyPrimitive(issue.values[0])}`;\n return `Tùy chọn không hợp lệ: mong đợi một trong các giá trị ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Quá lớn: mong đợi ${issue.origin ?? \"giá trị\"} ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"phần tử\"}`;\n return `Quá lớn: mong đợi ${issue.origin ?? \"giá trị\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Quá nhỏ: mong đợi ${issue.origin} ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Quá nhỏ: mong đợi ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Chuỗi không hợp lệ: phải bắt đầu bằng \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Chuỗi không hợp lệ: phải kết thúc bằng \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Chuỗi không hợp lệ: phải bao gồm \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Chuỗi không hợp lệ: phải khớp với mẫu ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} không hợp lệ`;\n }\n case \"not_multiple_of\":\n return `Số không hợp lệ: phải là bội số của ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Khóa không được nhận dạng: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Khóa không hợp lệ trong ${issue.origin}`;\n case \"invalid_union\":\n return \"Đầu vào không hợp lệ\";\n case \"invalid_element\":\n return `Giá trị không hợp lệ trong ${issue.origin}`;\n default:\n return `Đầu vào không hợp lệ`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"字符\", verb: \"包含\" },\n file: { unit: \"字节\", verb: \"包含\" },\n array: { unit: \"项\", verb: \"包含\" },\n set: { unit: \"项\", verb: \"包含\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"非数字(NaN)\" : \"数字\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"数组\";\n }\n if (data === null) {\n return \"空值(null)\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"输入\",\n email: \"电子邮件\",\n url: \"URL\",\n emoji: \"表情符号\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO日期时间\",\n date: \"ISO日期\",\n time: \"ISO时间\",\n duration: \"ISO时长\",\n ipv4: \"IPv4地址\",\n ipv6: \"IPv6地址\",\n cidrv4: \"IPv4网段\",\n cidrv6: \"IPv6网段\",\n base64: \"base64编码字符串\",\n base64url: \"base64url编码字符串\",\n json_string: \"JSON字符串\",\n e164: \"E.164号码\",\n jwt: \"JWT\",\n template_literal: \"输入\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `无效输入:期望 ${issue.expected},实际接收 ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `无效输入:期望 ${util.stringifyPrimitive(issue.values[0])}`;\n return `无效选项:期望以下之一 ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `数值过大:期望 ${issue.origin ?? \"值\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"个元素\"}`;\n return `数值过大:期望 ${issue.origin ?? \"值\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `数值过小:期望 ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `数值过小:期望 ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `无效字符串:必须以 \"${_issue.prefix}\" 开头`;\n if (_issue.format === \"ends_with\")\n return `无效字符串:必须以 \"${_issue.suffix}\" 结尾`;\n if (_issue.format === \"includes\")\n return `无效字符串:必须包含 \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `无效字符串:必须满足正则表达式 ${_issue.pattern}`;\n return `无效${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `无效数字:必须是 ${issue.divisor} 的倍数`;\n case \"unrecognized_keys\":\n return `出现未知的键(key): ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} 中的键(key)无效`;\n case \"invalid_union\":\n return \"无效输入\";\n case \"invalid_element\":\n return `${issue.origin} 中包含无效值(value)`;\n default:\n return `无效输入`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"字元\", verb: \"擁有\" },\n file: { unit: \"位元組\", verb: \"擁有\" },\n array: { unit: \"項目\", verb: \"擁有\" },\n set: { unit: \"項目\", verb: \"擁有\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"輸入\",\n email: \"郵件地址\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO 日期時間\",\n date: \"ISO 日期\",\n time: \"ISO 時間\",\n duration: \"ISO 期間\",\n ipv4: \"IPv4 位址\",\n ipv6: \"IPv6 位址\",\n cidrv4: \"IPv4 範圍\",\n cidrv6: \"IPv6 範圍\",\n base64: \"base64 編碼字串\",\n base64url: \"base64url 編碼字串\",\n json_string: \"JSON 字串\",\n e164: \"E.164 數值\",\n jwt: \"JWT\",\n template_literal: \"輸入\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `無效的輸入值:預期為 ${issue.expected},但收到 ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `無效的輸入值:預期為 ${util.stringifyPrimitive(issue.values[0])}`;\n return `無效的選項:預期為以下其中之一 ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `數值過大:預期 ${issue.origin ?? \"值\"} 應為 ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"個元素\"}`;\n return `數值過大:預期 ${issue.origin ?? \"值\"} 應為 ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `數值過小:預期 ${issue.origin} 應為 ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `數值過小:預期 ${issue.origin} 應為 ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `無效的字串:必須以 \"${_issue.prefix}\" 開頭`;\n }\n if (_issue.format === \"ends_with\")\n return `無效的字串:必須以 \"${_issue.suffix}\" 結尾`;\n if (_issue.format === \"includes\")\n return `無效的字串:必須包含 \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `無效的字串:必須符合格式 ${_issue.pattern}`;\n return `無效的 ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `無效的數字:必須為 ${issue.divisor} 的倍數`;\n case \"unrecognized_keys\":\n return `無法識別的鍵值${issue.keys.length > 1 ? \"們\" : \"\"}:${util.joinValues(issue.keys, \"、\")}`;\n case \"invalid_key\":\n return `${issue.origin} 中有無效的鍵值`;\n case \"invalid_union\":\n return \"無效的輸入值\";\n case \"invalid_element\":\n return `${issue.origin} 中有無效的值`;\n default:\n return `無效的輸入值`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"àmi\", verb: \"ní\" },\n file: { unit: \"bytes\", verb: \"ní\" },\n array: { unit: \"nkan\", verb: \"ní\" },\n set: { unit: \"nkan\", verb: \"ní\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"nọ́mbà\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"akopọ\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ẹ̀rọ ìbáwọlé\",\n email: \"àdírẹ́sì ìmẹ́lì\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"àkókò ISO\",\n date: \"ọjọ́ ISO\",\n time: \"àkókò ISO\",\n duration: \"àkókò tó pé ISO\",\n ipv4: \"àdírẹ́sì IPv4\",\n ipv6: \"àdírẹ́sì IPv6\",\n cidrv4: \"àgbègbè IPv4\",\n cidrv6: \"àgbègbè IPv6\",\n base64: \"ọ̀rọ̀ tí a kọ́ ní base64\",\n base64url: \"ọ̀rọ̀ base64url\",\n json_string: \"ọ̀rọ̀ JSON\",\n e164: \"nọ́mbà E.164\",\n jwt: \"JWT\",\n template_literal: \"ẹ̀rọ ìbáwọlé\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ìbáwọlé aṣìṣe: a ní láti fi ${issue.expected}, àmọ̀ a rí ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ìbáwọlé aṣìṣe: a ní láti fi ${util.stringifyPrimitive(issue.values[0])}`;\n return `Àṣàyàn aṣìṣe: yan ọ̀kan lára ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Tó pọ̀ jù: a ní láti jẹ́ pé ${issue.origin ?? \"iye\"} ${sizing.verb} ${adj}${issue.maximum} ${sizing.unit}`;\n return `Tó pọ̀ jù: a ní láti jẹ́ ${adj}${issue.maximum}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Kéré ju: a ní láti jẹ́ pé ${issue.origin} ${sizing.verb} ${adj}${issue.minimum} ${sizing.unit}`;\n return `Kéré ju: a ní láti jẹ́ ${adj}${issue.minimum}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ bẹ̀rẹ̀ pẹ̀lú \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ parí pẹ̀lú \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ ní \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ bá àpẹẹrẹ mu ${_issue.pattern}`;\n return `Aṣìṣe: ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Nọ́mbà aṣìṣe: gbọ́dọ̀ jẹ́ èyà pípín ti ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Bọtìnì àìmọ̀: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Bọtìnì aṣìṣe nínú ${issue.origin}`;\n case \"invalid_union\":\n return \"Ìbáwọlé aṣìṣe\";\n case \"invalid_element\":\n return `Iye aṣìṣe nínú ${issue.origin}`;\n default:\n return \"Ìbáwọlé aṣìṣe\";\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","export const $output = Symbol(\"ZodOutput\");\nexport const $input = Symbol(\"ZodInput\");\nexport class $ZodRegistry {\n constructor() {\n this._map = new WeakMap();\n this._idmap = new Map();\n }\n add(schema, ..._meta) {\n const meta = _meta[0];\n this._map.set(schema, meta);\n if (meta && typeof meta === \"object\" && \"id\" in meta) {\n if (this._idmap.has(meta.id)) {\n throw new Error(`ID ${meta.id} already exists in the registry`);\n }\n this._idmap.set(meta.id, schema);\n }\n return this;\n }\n clear() {\n this._map = new WeakMap();\n this._idmap = new Map();\n return this;\n }\n remove(schema) {\n const meta = this._map.get(schema);\n if (meta && typeof meta === \"object\" && \"id\" in meta) {\n this._idmap.delete(meta.id);\n }\n this._map.delete(schema);\n return this;\n }\n get(schema) {\n // return this._map.get(schema) as any;\n // inherit metadata\n const p = schema._zod.parent;\n if (p) {\n const pm = { ...(this.get(p) ?? {}) };\n delete pm.id; // do not inherit id\n const f = { ...pm, ...this._map.get(schema) };\n return Object.keys(f).length ? f : undefined;\n }\n return this._map.get(schema);\n }\n has(schema) {\n return this._map.has(schema);\n }\n}\n// registries\nexport function registry() {\n return new $ZodRegistry();\n}\nexport const globalRegistry = /*@__PURE__*/ registry();\n","import * as checks from \"./checks.js\";\nimport * as schemas from \"./schemas.js\";\nimport * as util from \"./util.js\";\nexport function _string(Class, params) {\n return new Class({\n type: \"string\",\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedString(Class, params) {\n return new Class({\n type: \"string\",\n coerce: true,\n ...util.normalizeParams(params),\n });\n}\nexport function _email(Class, params) {\n return new Class({\n type: \"string\",\n format: \"email\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _guid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"guid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _uuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _uuidv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v4\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uuidv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v6\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uuidv7(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v7\",\n ...util.normalizeParams(params),\n });\n}\nexport function _url(Class, params) {\n return new Class({\n type: \"string\",\n format: \"url\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _emoji(Class, params) {\n return new Class({\n type: \"string\",\n format: \"emoji\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _nanoid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"nanoid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _cuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cuid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _cuid2(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cuid2\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _ulid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ulid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _xid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"xid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _ksuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ksuid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _ipv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ipv4\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _ipv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ipv6\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _cidrv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cidrv4\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _cidrv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cidrv6\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _base64(Class, params) {\n return new Class({\n type: \"string\",\n format: \"base64\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _base64url(Class, params) {\n return new Class({\n type: \"string\",\n format: \"base64url\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _e164(Class, params) {\n return new Class({\n type: \"string\",\n format: \"e164\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _jwt(Class, params) {\n return new Class({\n type: \"string\",\n format: \"jwt\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport const TimePrecision = {\n Any: null,\n Minute: -1,\n Second: 0,\n Millisecond: 3,\n Microsecond: 6,\n};\nexport function _isoDateTime(Class, params) {\n return new Class({\n type: \"string\",\n format: \"datetime\",\n check: \"string_format\",\n offset: false,\n local: false,\n precision: null,\n ...util.normalizeParams(params),\n });\n}\nexport function _isoDate(Class, params) {\n return new Class({\n type: \"string\",\n format: \"date\",\n check: \"string_format\",\n ...util.normalizeParams(params),\n });\n}\nexport function _isoTime(Class, params) {\n return new Class({\n type: \"string\",\n format: \"time\",\n check: \"string_format\",\n precision: null,\n ...util.normalizeParams(params),\n });\n}\nexport function _isoDuration(Class, params) {\n return new Class({\n type: \"string\",\n format: \"duration\",\n check: \"string_format\",\n ...util.normalizeParams(params),\n });\n}\nexport function _number(Class, params) {\n return new Class({\n type: \"number\",\n checks: [],\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedNumber(Class, params) {\n return new Class({\n type: \"number\",\n coerce: true,\n checks: [],\n ...util.normalizeParams(params),\n });\n}\nexport function _int(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"safeint\",\n ...util.normalizeParams(params),\n });\n}\nexport function _float32(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"float32\",\n ...util.normalizeParams(params),\n });\n}\nexport function _float64(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"float64\",\n ...util.normalizeParams(params),\n });\n}\nexport function _int32(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"int32\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uint32(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"uint32\",\n ...util.normalizeParams(params),\n });\n}\nexport function _boolean(Class, params) {\n return new Class({\n type: \"boolean\",\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedBoolean(Class, params) {\n return new Class({\n type: \"boolean\",\n coerce: true,\n ...util.normalizeParams(params),\n });\n}\nexport function _bigint(Class, params) {\n return new Class({\n type: \"bigint\",\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedBigint(Class, params) {\n return new Class({\n type: \"bigint\",\n coerce: true,\n ...util.normalizeParams(params),\n });\n}\nexport function _int64(Class, params) {\n return new Class({\n type: \"bigint\",\n check: \"bigint_format\",\n abort: false,\n format: \"int64\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uint64(Class, params) {\n return new Class({\n type: \"bigint\",\n check: \"bigint_format\",\n abort: false,\n format: \"uint64\",\n ...util.normalizeParams(params),\n });\n}\nexport function _symbol(Class, params) {\n return new Class({\n type: \"symbol\",\n ...util.normalizeParams(params),\n });\n}\nexport function _undefined(Class, params) {\n return new Class({\n type: \"undefined\",\n ...util.normalizeParams(params),\n });\n}\nexport function _null(Class, params) {\n return new Class({\n type: \"null\",\n ...util.normalizeParams(params),\n });\n}\nexport function _any(Class) {\n return new Class({\n type: \"any\",\n });\n}\nexport function _unknown(Class) {\n return new Class({\n type: \"unknown\",\n });\n}\nexport function _never(Class, params) {\n return new Class({\n type: \"never\",\n ...util.normalizeParams(params),\n });\n}\nexport function _void(Class, params) {\n return new Class({\n type: \"void\",\n ...util.normalizeParams(params),\n });\n}\nexport function _date(Class, params) {\n return new Class({\n type: \"date\",\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedDate(Class, params) {\n return new Class({\n type: \"date\",\n coerce: true,\n ...util.normalizeParams(params),\n });\n}\nexport function _nan(Class, params) {\n return new Class({\n type: \"nan\",\n ...util.normalizeParams(params),\n });\n}\nexport function _lt(value, params) {\n return new checks.$ZodCheckLessThan({\n check: \"less_than\",\n ...util.normalizeParams(params),\n value,\n inclusive: false,\n });\n}\nexport function _lte(value, params) {\n return new checks.$ZodCheckLessThan({\n check: \"less_than\",\n ...util.normalizeParams(params),\n value,\n inclusive: true,\n });\n}\nexport { \n/** @deprecated Use `z.lte()` instead. */\n_lte as _max, };\nexport function _gt(value, params) {\n return new checks.$ZodCheckGreaterThan({\n check: \"greater_than\",\n ...util.normalizeParams(params),\n value,\n inclusive: false,\n });\n}\nexport function _gte(value, params) {\n return new checks.$ZodCheckGreaterThan({\n check: \"greater_than\",\n ...util.normalizeParams(params),\n value,\n inclusive: true,\n });\n}\nexport { \n/** @deprecated Use `z.gte()` instead. */\n_gte as _min, };\nexport function _positive(params) {\n return _gt(0, params);\n}\n// negative\nexport function _negative(params) {\n return _lt(0, params);\n}\n// nonpositive\nexport function _nonpositive(params) {\n return _lte(0, params);\n}\n// nonnegative\nexport function _nonnegative(params) {\n return _gte(0, params);\n}\nexport function _multipleOf(value, params) {\n return new checks.$ZodCheckMultipleOf({\n check: \"multiple_of\",\n ...util.normalizeParams(params),\n value,\n });\n}\nexport function _maxSize(maximum, params) {\n return new checks.$ZodCheckMaxSize({\n check: \"max_size\",\n ...util.normalizeParams(params),\n maximum,\n });\n}\nexport function _minSize(minimum, params) {\n return new checks.$ZodCheckMinSize({\n check: \"min_size\",\n ...util.normalizeParams(params),\n minimum,\n });\n}\nexport function _size(size, params) {\n return new checks.$ZodCheckSizeEquals({\n check: \"size_equals\",\n ...util.normalizeParams(params),\n size,\n });\n}\nexport function _maxLength(maximum, params) {\n const ch = new checks.$ZodCheckMaxLength({\n check: \"max_length\",\n ...util.normalizeParams(params),\n maximum,\n });\n return ch;\n}\nexport function _minLength(minimum, params) {\n return new checks.$ZodCheckMinLength({\n check: \"min_length\",\n ...util.normalizeParams(params),\n minimum,\n });\n}\nexport function _length(length, params) {\n return new checks.$ZodCheckLengthEquals({\n check: \"length_equals\",\n ...util.normalizeParams(params),\n length,\n });\n}\nexport function _regex(pattern, params) {\n return new checks.$ZodCheckRegex({\n check: \"string_format\",\n format: \"regex\",\n ...util.normalizeParams(params),\n pattern,\n });\n}\nexport function _lowercase(params) {\n return new checks.$ZodCheckLowerCase({\n check: \"string_format\",\n format: \"lowercase\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uppercase(params) {\n return new checks.$ZodCheckUpperCase({\n check: \"string_format\",\n format: \"uppercase\",\n ...util.normalizeParams(params),\n });\n}\nexport function _includes(includes, params) {\n return new checks.$ZodCheckIncludes({\n check: \"string_format\",\n format: \"includes\",\n ...util.normalizeParams(params),\n includes,\n });\n}\nexport function _startsWith(prefix, params) {\n return new checks.$ZodCheckStartsWith({\n check: \"string_format\",\n format: \"starts_with\",\n ...util.normalizeParams(params),\n prefix,\n });\n}\nexport function _endsWith(suffix, params) {\n return new checks.$ZodCheckEndsWith({\n check: \"string_format\",\n format: \"ends_with\",\n ...util.normalizeParams(params),\n suffix,\n });\n}\nexport function _property(property, schema, params) {\n return new checks.$ZodCheckProperty({\n check: \"property\",\n property,\n schema,\n ...util.normalizeParams(params),\n });\n}\nexport function _mime(types, params) {\n return new checks.$ZodCheckMimeType({\n check: \"mime_type\",\n mime: types,\n ...util.normalizeParams(params),\n });\n}\nexport function _overwrite(tx) {\n return new checks.$ZodCheckOverwrite({\n check: \"overwrite\",\n tx,\n });\n}\n// normalize\nexport function _normalize(form) {\n return _overwrite((input) => input.normalize(form));\n}\n// trim\nexport function _trim() {\n return _overwrite((input) => input.trim());\n}\n// toLowerCase\nexport function _toLowerCase() {\n return _overwrite((input) => input.toLowerCase());\n}\n// toUpperCase\nexport function _toUpperCase() {\n return _overwrite((input) => input.toUpperCase());\n}\nexport function _array(Class, element, params) {\n return new Class({\n type: \"array\",\n element,\n // get element() {\n // return element;\n // },\n ...util.normalizeParams(params),\n });\n}\nexport function _union(Class, options, params) {\n return new Class({\n type: \"union\",\n options,\n ...util.normalizeParams(params),\n });\n}\nexport function _discriminatedUnion(Class, discriminator, options, params) {\n return new Class({\n type: \"union\",\n options,\n discriminator,\n ...util.normalizeParams(params),\n });\n}\nexport function _intersection(Class, left, right) {\n return new Class({\n type: \"intersection\",\n left,\n right,\n });\n}\n// export function _tuple(\n// Class: util.SchemaClass,\n// items: [],\n// params?: string | $ZodTupleParams\n// ): schemas.$ZodTuple<[], null>;\nexport function _tuple(Class, items, _paramsOrRest, _params) {\n const hasRest = _paramsOrRest instanceof schemas.$ZodType;\n const params = hasRest ? _params : _paramsOrRest;\n const rest = hasRest ? _paramsOrRest : null;\n return new Class({\n type: \"tuple\",\n items,\n rest,\n ...util.normalizeParams(params),\n });\n}\nexport function _record(Class, keyType, valueType, params) {\n return new Class({\n type: \"record\",\n keyType,\n valueType,\n ...util.normalizeParams(params),\n });\n}\nexport function _map(Class, keyType, valueType, params) {\n return new Class({\n type: \"map\",\n keyType,\n valueType,\n ...util.normalizeParams(params),\n });\n}\nexport function _set(Class, valueType, params) {\n return new Class({\n type: \"set\",\n valueType,\n ...util.normalizeParams(params),\n });\n}\nexport function _enum(Class, values, params) {\n const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;\n // if (Array.isArray(values)) {\n // for (const value of values) {\n // entries[value] = value;\n // }\n // } else {\n // Object.assign(entries, values);\n // }\n // const entries: util.EnumLike = {};\n // for (const val of values) {\n // entries[val] = val;\n // }\n return new Class({\n type: \"enum\",\n entries,\n ...util.normalizeParams(params),\n });\n}\n/** @deprecated This API has been merged into `z.enum()`. Use `z.enum()` instead.\n *\n * ```ts\n * enum Colors { red, green, blue }\n * z.enum(Colors);\n * ```\n */\nexport function _nativeEnum(Class, entries, params) {\n return new Class({\n type: \"enum\",\n entries,\n ...util.normalizeParams(params),\n });\n}\nexport function _literal(Class, value, params) {\n return new Class({\n type: \"literal\",\n values: Array.isArray(value) ? value : [value],\n ...util.normalizeParams(params),\n });\n}\nexport function _file(Class, params) {\n return new Class({\n type: \"file\",\n ...util.normalizeParams(params),\n });\n}\nexport function _transform(Class, fn) {\n return new Class({\n type: \"transform\",\n transform: fn,\n });\n}\nexport function _optional(Class, innerType) {\n return new Class({\n type: \"optional\",\n innerType,\n });\n}\nexport function _nullable(Class, innerType) {\n return new Class({\n type: \"nullable\",\n innerType,\n });\n}\nexport function _default(Class, innerType, defaultValue) {\n return new Class({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : util.shallowClone(defaultValue);\n },\n });\n}\nexport function _nonoptional(Class, innerType, params) {\n return new Class({\n type: \"nonoptional\",\n innerType,\n ...util.normalizeParams(params),\n });\n}\nexport function _success(Class, innerType) {\n return new Class({\n type: \"success\",\n innerType,\n });\n}\nexport function _catch(Class, innerType, catchValue) {\n return new Class({\n type: \"catch\",\n innerType,\n catchValue: (typeof catchValue === \"function\" ? catchValue : () => catchValue),\n });\n}\nexport function _pipe(Class, in_, out) {\n return new Class({\n type: \"pipe\",\n in: in_,\n out,\n });\n}\nexport function _readonly(Class, innerType) {\n return new Class({\n type: \"readonly\",\n innerType,\n });\n}\nexport function _templateLiteral(Class, parts, params) {\n return new Class({\n type: \"template_literal\",\n parts,\n ...util.normalizeParams(params),\n });\n}\nexport function _lazy(Class, getter) {\n return new Class({\n type: \"lazy\",\n getter,\n });\n}\nexport function _promise(Class, innerType) {\n return new Class({\n type: \"promise\",\n innerType,\n });\n}\nexport function _custom(Class, fn, _params) {\n const norm = util.normalizeParams(_params);\n norm.abort ?? (norm.abort = true); // default to abort:false\n const schema = new Class({\n type: \"custom\",\n check: \"custom\",\n fn: fn,\n ...norm,\n });\n return schema;\n}\n// same as _custom but defaults to abort:false\nexport function _refine(Class, fn, _params) {\n const schema = new Class({\n type: \"custom\",\n check: \"custom\",\n fn: fn,\n ...util.normalizeParams(_params),\n });\n return schema;\n}\nexport function _superRefine(fn) {\n const ch = _check((payload) => {\n payload.addIssue = (issue) => {\n if (typeof issue === \"string\") {\n payload.issues.push(util.issue(issue, payload.value, ch._zod.def));\n }\n else {\n // for Zod 3 backwards compatibility\n const _issue = issue;\n if (_issue.fatal)\n _issue.continue = false;\n _issue.code ?? (_issue.code = \"custom\");\n _issue.input ?? (_issue.input = payload.value);\n _issue.inst ?? (_issue.inst = ch);\n _issue.continue ?? (_issue.continue = !ch._zod.def.abort); // abort is always undefined, so this is always true...\n payload.issues.push(util.issue(_issue));\n }\n };\n return fn(payload.value, payload);\n });\n return ch;\n}\nexport function _check(fn, params) {\n const ch = new checks.$ZodCheck({\n check: \"custom\",\n ...util.normalizeParams(params),\n });\n ch._zod.check = fn;\n return ch;\n}\nexport function _stringbool(Classes, _params) {\n const params = util.normalizeParams(_params);\n let truthyArray = params.truthy ?? [\"true\", \"1\", \"yes\", \"on\", \"y\", \"enabled\"];\n let falsyArray = params.falsy ?? [\"false\", \"0\", \"no\", \"off\", \"n\", \"disabled\"];\n if (params.case !== \"sensitive\") {\n truthyArray = truthyArray.map((v) => (typeof v === \"string\" ? v.toLowerCase() : v));\n falsyArray = falsyArray.map((v) => (typeof v === \"string\" ? v.toLowerCase() : v));\n }\n const truthySet = new Set(truthyArray);\n const falsySet = new Set(falsyArray);\n const _Codec = Classes.Codec ?? schemas.$ZodCodec;\n const _Boolean = Classes.Boolean ?? schemas.$ZodBoolean;\n const _String = Classes.String ?? schemas.$ZodString;\n const stringSchema = new _String({ type: \"string\", error: params.error });\n const booleanSchema = new _Boolean({ type: \"boolean\", error: params.error });\n const codec = new _Codec({\n type: \"pipe\",\n in: stringSchema,\n out: booleanSchema,\n transform: ((input, payload) => {\n let data = input;\n if (params.case !== \"sensitive\")\n data = data.toLowerCase();\n if (truthySet.has(data)) {\n return true;\n }\n else if (falsySet.has(data)) {\n return false;\n }\n else {\n payload.issues.push({\n code: \"invalid_value\",\n expected: \"stringbool\",\n values: [...truthySet, ...falsySet],\n input: payload.value,\n inst: codec,\n continue: false,\n });\n return {};\n }\n }),\n reverseTransform: ((input, _payload) => {\n if (input === true) {\n return truthyArray[0] || \"true\";\n }\n else {\n return falsyArray[0] || \"false\";\n }\n }),\n error: params.error,\n });\n return codec;\n}\nexport function _stringFormat(Class, format, fnOrRegex, _params = {}) {\n const params = util.normalizeParams(_params);\n const def = {\n ...util.normalizeParams(_params),\n check: \"string_format\",\n type: \"string\",\n format,\n fn: typeof fnOrRegex === \"function\" ? fnOrRegex : (val) => fnOrRegex.test(val),\n ...params,\n };\n if (fnOrRegex instanceof RegExp) {\n def.pattern = fnOrRegex;\n }\n const inst = new Class(def);\n return inst;\n}\n","import { $ZodRegistry, globalRegistry } from \"./registries.js\";\nimport { getEnumValues } from \"./util.js\";\nexport class JSONSchemaGenerator {\n constructor(params) {\n this.counter = 0;\n this.metadataRegistry = params?.metadata ?? globalRegistry;\n this.target = params?.target ?? \"draft-2020-12\";\n this.unrepresentable = params?.unrepresentable ?? \"throw\";\n this.override = params?.override ?? (() => { });\n this.io = params?.io ?? \"output\";\n this.seen = new Map();\n }\n process(schema, _params = { path: [], schemaPath: [] }) {\n var _a;\n const def = schema._zod.def;\n const formatMap = {\n guid: \"uuid\",\n url: \"uri\",\n datetime: \"date-time\",\n json_string: \"json-string\",\n regex: \"\", // do not set\n };\n // check for schema in seens\n const seen = this.seen.get(schema);\n if (seen) {\n seen.count++;\n // check if cycle\n const isCycle = _params.schemaPath.includes(schema);\n if (isCycle) {\n seen.cycle = _params.path;\n }\n return seen.schema;\n }\n // initialize\n const result = { schema: {}, count: 1, cycle: undefined, path: _params.path };\n this.seen.set(schema, result);\n // custom method overrides default behavior\n const overrideSchema = schema._zod.toJSONSchema?.();\n if (overrideSchema) {\n result.schema = overrideSchema;\n }\n else {\n const params = {\n ..._params,\n schemaPath: [..._params.schemaPath, schema],\n path: _params.path,\n };\n const parent = schema._zod.parent;\n if (parent) {\n // schema was cloned from another schema\n result.ref = parent;\n this.process(parent, params);\n this.seen.get(parent).isParent = true;\n }\n else {\n const _json = result.schema;\n switch (def.type) {\n case \"string\": {\n const json = _json;\n json.type = \"string\";\n const { minimum, maximum, format, patterns, contentEncoding } = schema._zod\n .bag;\n if (typeof minimum === \"number\")\n json.minLength = minimum;\n if (typeof maximum === \"number\")\n json.maxLength = maximum;\n // custom pattern overrides format\n if (format) {\n json.format = formatMap[format] ?? format;\n if (json.format === \"\")\n delete json.format; // empty format is not valid\n }\n if (contentEncoding)\n json.contentEncoding = contentEncoding;\n if (patterns && patterns.size > 0) {\n const regexes = [...patterns];\n if (regexes.length === 1)\n json.pattern = regexes[0].source;\n else if (regexes.length > 1) {\n result.schema.allOf = [\n ...regexes.map((regex) => ({\n ...(this.target === \"draft-7\" || this.target === \"draft-4\" || this.target === \"openapi-3.0\"\n ? { type: \"string\" }\n : {}),\n pattern: regex.source,\n })),\n ];\n }\n }\n break;\n }\n case \"number\": {\n const json = _json;\n const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;\n if (typeof format === \"string\" && format.includes(\"int\"))\n json.type = \"integer\";\n else\n json.type = \"number\";\n if (typeof exclusiveMinimum === \"number\") {\n if (this.target === \"draft-4\" || this.target === \"openapi-3.0\") {\n json.minimum = exclusiveMinimum;\n json.exclusiveMinimum = true;\n }\n else {\n json.exclusiveMinimum = exclusiveMinimum;\n }\n }\n if (typeof minimum === \"number\") {\n json.minimum = minimum;\n if (typeof exclusiveMinimum === \"number\" && this.target !== \"draft-4\") {\n if (exclusiveMinimum >= minimum)\n delete json.minimum;\n else\n delete json.exclusiveMinimum;\n }\n }\n if (typeof exclusiveMaximum === \"number\") {\n if (this.target === \"draft-4\" || this.target === \"openapi-3.0\") {\n json.maximum = exclusiveMaximum;\n json.exclusiveMaximum = true;\n }\n else {\n json.exclusiveMaximum = exclusiveMaximum;\n }\n }\n if (typeof maximum === \"number\") {\n json.maximum = maximum;\n if (typeof exclusiveMaximum === \"number\" && this.target !== \"draft-4\") {\n if (exclusiveMaximum <= maximum)\n delete json.maximum;\n else\n delete json.exclusiveMaximum;\n }\n }\n if (typeof multipleOf === \"number\")\n json.multipleOf = multipleOf;\n break;\n }\n case \"boolean\": {\n const json = _json;\n json.type = \"boolean\";\n break;\n }\n case \"bigint\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"BigInt cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"symbol\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Symbols cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"null\": {\n if (this.target === \"openapi-3.0\") {\n _json.type = \"string\";\n _json.nullable = true;\n _json.enum = [null];\n }\n else\n _json.type = \"null\";\n break;\n }\n case \"any\": {\n break;\n }\n case \"unknown\": {\n break;\n }\n case \"undefined\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Undefined cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"void\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Void cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"never\": {\n _json.not = {};\n break;\n }\n case \"date\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Date cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"array\": {\n const json = _json;\n const { minimum, maximum } = schema._zod.bag;\n if (typeof minimum === \"number\")\n json.minItems = minimum;\n if (typeof maximum === \"number\")\n json.maxItems = maximum;\n json.type = \"array\";\n json.items = this.process(def.element, { ...params, path: [...params.path, \"items\"] });\n break;\n }\n case \"object\": {\n const json = _json;\n json.type = \"object\";\n json.properties = {};\n const shape = def.shape; // params.shapeCache.get(schema)!;\n for (const key in shape) {\n json.properties[key] = this.process(shape[key], {\n ...params,\n path: [...params.path, \"properties\", key],\n });\n }\n // required keys\n const allKeys = new Set(Object.keys(shape));\n // const optionalKeys = new Set(def.optional);\n const requiredKeys = new Set([...allKeys].filter((key) => {\n const v = def.shape[key]._zod;\n if (this.io === \"input\") {\n return v.optin === undefined;\n }\n else {\n return v.optout === undefined;\n }\n }));\n if (requiredKeys.size > 0) {\n json.required = Array.from(requiredKeys);\n }\n // catchall\n if (def.catchall?._zod.def.type === \"never\") {\n // strict\n json.additionalProperties = false;\n }\n else if (!def.catchall) {\n // regular\n if (this.io === \"output\")\n json.additionalProperties = false;\n }\n else if (def.catchall) {\n json.additionalProperties = this.process(def.catchall, {\n ...params,\n path: [...params.path, \"additionalProperties\"],\n });\n }\n break;\n }\n case \"union\": {\n const json = _json;\n const options = def.options.map((x, i) => this.process(x, {\n ...params,\n path: [...params.path, \"anyOf\", i],\n }));\n json.anyOf = options;\n break;\n }\n case \"intersection\": {\n const json = _json;\n const a = this.process(def.left, {\n ...params,\n path: [...params.path, \"allOf\", 0],\n });\n const b = this.process(def.right, {\n ...params,\n path: [...params.path, \"allOf\", 1],\n });\n const isSimpleIntersection = (val) => \"allOf\" in val && Object.keys(val).length === 1;\n const allOf = [\n ...(isSimpleIntersection(a) ? a.allOf : [a]),\n ...(isSimpleIntersection(b) ? b.allOf : [b]),\n ];\n json.allOf = allOf;\n break;\n }\n case \"tuple\": {\n const json = _json;\n json.type = \"array\";\n const prefixPath = this.target === \"draft-2020-12\" ? \"prefixItems\" : \"items\";\n const restPath = this.target === \"draft-2020-12\" ? \"items\" : this.target === \"openapi-3.0\" ? \"items\" : \"additionalItems\";\n const prefixItems = def.items.map((x, i) => this.process(x, {\n ...params,\n path: [...params.path, prefixPath, i],\n }));\n const rest = def.rest\n ? this.process(def.rest, {\n ...params,\n path: [...params.path, restPath, ...(this.target === \"openapi-3.0\" ? [def.items.length] : [])],\n })\n : null;\n if (this.target === \"draft-2020-12\") {\n json.prefixItems = prefixItems;\n if (rest) {\n json.items = rest;\n }\n }\n else if (this.target === \"openapi-3.0\") {\n json.items = {\n anyOf: prefixItems,\n };\n if (rest) {\n json.items.anyOf.push(rest);\n }\n json.minItems = prefixItems.length;\n if (!rest) {\n json.maxItems = prefixItems.length;\n }\n }\n else {\n json.items = prefixItems;\n if (rest) {\n json.additionalItems = rest;\n }\n }\n // length\n const { minimum, maximum } = schema._zod.bag;\n if (typeof minimum === \"number\")\n json.minItems = minimum;\n if (typeof maximum === \"number\")\n json.maxItems = maximum;\n break;\n }\n case \"record\": {\n const json = _json;\n json.type = \"object\";\n if (this.target === \"draft-7\" || this.target === \"draft-2020-12\") {\n json.propertyNames = this.process(def.keyType, {\n ...params,\n path: [...params.path, \"propertyNames\"],\n });\n }\n json.additionalProperties = this.process(def.valueType, {\n ...params,\n path: [...params.path, \"additionalProperties\"],\n });\n break;\n }\n case \"map\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Map cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"set\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Set cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"enum\": {\n const json = _json;\n const values = getEnumValues(def.entries);\n // Number enums can have both string and number values\n if (values.every((v) => typeof v === \"number\"))\n json.type = \"number\";\n if (values.every((v) => typeof v === \"string\"))\n json.type = \"string\";\n json.enum = values;\n break;\n }\n case \"literal\": {\n const json = _json;\n const vals = [];\n for (const val of def.values) {\n if (val === undefined) {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Literal `undefined` cannot be represented in JSON Schema\");\n }\n else {\n // do not add to vals\n }\n }\n else if (typeof val === \"bigint\") {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"BigInt literals cannot be represented in JSON Schema\");\n }\n else {\n vals.push(Number(val));\n }\n }\n else {\n vals.push(val);\n }\n }\n if (vals.length === 0) {\n // do nothing (an undefined literal was stripped)\n }\n else if (vals.length === 1) {\n const val = vals[0];\n json.type = val === null ? \"null\" : typeof val;\n if (this.target === \"draft-4\" || this.target === \"openapi-3.0\") {\n json.enum = [val];\n }\n else {\n json.const = val;\n }\n }\n else {\n if (vals.every((v) => typeof v === \"number\"))\n json.type = \"number\";\n if (vals.every((v) => typeof v === \"string\"))\n json.type = \"string\";\n if (vals.every((v) => typeof v === \"boolean\"))\n json.type = \"string\";\n if (vals.every((v) => v === null))\n json.type = \"null\";\n json.enum = vals;\n }\n break;\n }\n case \"file\": {\n const json = _json;\n const file = {\n type: \"string\",\n format: \"binary\",\n contentEncoding: \"binary\",\n };\n const { minimum, maximum, mime } = schema._zod.bag;\n if (minimum !== undefined)\n file.minLength = minimum;\n if (maximum !== undefined)\n file.maxLength = maximum;\n if (mime) {\n if (mime.length === 1) {\n file.contentMediaType = mime[0];\n Object.assign(json, file);\n }\n else {\n json.anyOf = mime.map((m) => {\n const mFile = { ...file, contentMediaType: m };\n return mFile;\n });\n }\n }\n else {\n Object.assign(json, file);\n }\n // if (this.unrepresentable === \"throw\") {\n // throw new Error(\"File cannot be represented in JSON Schema\");\n // }\n break;\n }\n case \"transform\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Transforms cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"nullable\": {\n const inner = this.process(def.innerType, params);\n if (this.target === \"openapi-3.0\") {\n result.ref = def.innerType;\n _json.nullable = true;\n }\n else {\n _json.anyOf = [inner, { type: \"null\" }];\n }\n break;\n }\n case \"nonoptional\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n break;\n }\n case \"success\": {\n const json = _json;\n json.type = \"boolean\";\n break;\n }\n case \"default\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n _json.default = JSON.parse(JSON.stringify(def.defaultValue));\n break;\n }\n case \"prefault\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n if (this.io === \"input\")\n _json._prefault = JSON.parse(JSON.stringify(def.defaultValue));\n break;\n }\n case \"catch\": {\n // use conditionals\n this.process(def.innerType, params);\n result.ref = def.innerType;\n let catchValue;\n try {\n catchValue = def.catchValue(undefined);\n }\n catch {\n throw new Error(\"Dynamic catch values are not supported in JSON Schema\");\n }\n _json.default = catchValue;\n break;\n }\n case \"nan\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"NaN cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"template_literal\": {\n const json = _json;\n const pattern = schema._zod.pattern;\n if (!pattern)\n throw new Error(\"Pattern not found in template literal\");\n json.type = \"string\";\n json.pattern = pattern.source;\n break;\n }\n case \"pipe\": {\n const innerType = this.io === \"input\" ? (def.in._zod.def.type === \"transform\" ? def.out : def.in) : def.out;\n this.process(innerType, params);\n result.ref = innerType;\n break;\n }\n case \"readonly\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n _json.readOnly = true;\n break;\n }\n // passthrough types\n case \"promise\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n break;\n }\n case \"optional\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n break;\n }\n case \"lazy\": {\n const innerType = schema._zod.innerType;\n this.process(innerType, params);\n result.ref = innerType;\n break;\n }\n case \"custom\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Custom types cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"function\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Function types cannot be represented in JSON Schema\");\n }\n break;\n }\n default: {\n def;\n }\n }\n }\n }\n // metadata\n const meta = this.metadataRegistry.get(schema);\n if (meta)\n Object.assign(result.schema, meta);\n if (this.io === \"input\" && isTransforming(schema)) {\n // examples/defaults only apply to output type of pipe\n delete result.schema.examples;\n delete result.schema.default;\n }\n // set prefault as default\n if (this.io === \"input\" && result.schema._prefault)\n (_a = result.schema).default ?? (_a.default = result.schema._prefault);\n delete result.schema._prefault;\n // pulling fresh from this.seen in case it was overwritten\n const _result = this.seen.get(schema);\n return _result.schema;\n }\n emit(schema, _params) {\n const params = {\n cycles: _params?.cycles ?? \"ref\",\n reused: _params?.reused ?? \"inline\",\n // unrepresentable: _params?.unrepresentable ?? \"throw\",\n // uri: _params?.uri ?? ((id) => `${id}`),\n external: _params?.external ?? undefined,\n };\n // iterate over seen map;\n const root = this.seen.get(schema);\n if (!root)\n throw new Error(\"Unprocessed schema. This is a bug in Zod.\");\n // initialize result with root schema fields\n // Object.assign(result, seen.cached);\n // returns a ref to the schema\n // defId will be empty if the ref points to an external schema (or #)\n const makeURI = (entry) => {\n // comparing the seen objects because sometimes\n // multiple schemas map to the same seen object.\n // e.g. lazy\n // external is configured\n const defsSegment = this.target === \"draft-2020-12\" ? \"$defs\" : \"definitions\";\n if (params.external) {\n const externalId = params.external.registry.get(entry[0])?.id; // ?? \"__shared\";// `__schema${this.counter++}`;\n // check if schema is in the external registry\n const uriGenerator = params.external.uri ?? ((id) => id);\n if (externalId) {\n return { ref: uriGenerator(externalId) };\n }\n // otherwise, add to __shared\n const id = entry[1].defId ?? entry[1].schema.id ?? `schema${this.counter++}`;\n entry[1].defId = id; // set defId so it will be reused if needed\n return { defId: id, ref: `${uriGenerator(\"__shared\")}#/${defsSegment}/${id}` };\n }\n if (entry[1] === root) {\n return { ref: \"#\" };\n }\n // self-contained schema\n const uriPrefix = `#`;\n const defUriPrefix = `${uriPrefix}/${defsSegment}/`;\n const defId = entry[1].schema.id ?? `__schema${this.counter++}`;\n return { defId, ref: defUriPrefix + defId };\n };\n // stored cached version in `def` property\n // remove all properties, set $ref\n const extractToDef = (entry) => {\n // if the schema is already a reference, do not extract it\n if (entry[1].schema.$ref) {\n return;\n }\n const seen = entry[1];\n const { ref, defId } = makeURI(entry);\n seen.def = { ...seen.schema };\n // defId won't be set if the schema is a reference to an external schema\n if (defId)\n seen.defId = defId;\n // wipe away all properties except $ref\n const schema = seen.schema;\n for (const key in schema) {\n delete schema[key];\n }\n schema.$ref = ref;\n };\n // throw on cycles\n // break cycles\n if (params.cycles === \"throw\") {\n for (const entry of this.seen.entries()) {\n const seen = entry[1];\n if (seen.cycle) {\n throw new Error(\"Cycle detected: \" +\n `#/${seen.cycle?.join(\"/\")}/` +\n '\\n\\nSet the `cycles` parameter to `\"ref\"` to resolve cyclical schemas with defs.');\n }\n }\n }\n // extract schemas into $defs\n for (const entry of this.seen.entries()) {\n const seen = entry[1];\n // convert root schema to # $ref\n if (schema === entry[0]) {\n extractToDef(entry); // this has special handling for the root schema\n continue;\n }\n // extract schemas that are in the external registry\n if (params.external) {\n const ext = params.external.registry.get(entry[0])?.id;\n if (schema !== entry[0] && ext) {\n extractToDef(entry);\n continue;\n }\n }\n // extract schemas with `id` meta\n const id = this.metadataRegistry.get(entry[0])?.id;\n if (id) {\n extractToDef(entry);\n continue;\n }\n // break cycles\n if (seen.cycle) {\n // any\n extractToDef(entry);\n continue;\n }\n // extract reused schemas\n if (seen.count > 1) {\n if (params.reused === \"ref\") {\n extractToDef(entry);\n // biome-ignore lint:\n continue;\n }\n }\n }\n // flatten _refs\n const flattenRef = (zodSchema, params) => {\n const seen = this.seen.get(zodSchema);\n const schema = seen.def ?? seen.schema;\n const _cached = { ...schema };\n // already seen\n if (seen.ref === null) {\n return;\n }\n // flatten ref if defined\n const ref = seen.ref;\n seen.ref = null; // prevent recursion\n if (ref) {\n flattenRef(ref, params);\n // merge referenced schema into current\n const refSchema = this.seen.get(ref).schema;\n if (refSchema.$ref &&\n (params.target === \"draft-7\" || params.target === \"draft-4\" || params.target === \"openapi-3.0\")) {\n schema.allOf = schema.allOf ?? [];\n schema.allOf.push(refSchema);\n }\n else {\n Object.assign(schema, refSchema);\n Object.assign(schema, _cached); // prevent overwriting any fields in the original schema\n }\n }\n // execute overrides\n if (!seen.isParent)\n this.override({\n zodSchema: zodSchema,\n jsonSchema: schema,\n path: seen.path ?? [],\n });\n };\n for (const entry of [...this.seen.entries()].reverse()) {\n flattenRef(entry[0], { target: this.target });\n }\n const result = {};\n if (this.target === \"draft-2020-12\") {\n result.$schema = \"https://json-schema.org/draft/2020-12/schema\";\n }\n else if (this.target === \"draft-7\") {\n result.$schema = \"http://json-schema.org/draft-07/schema#\";\n }\n else if (this.target === \"draft-4\") {\n result.$schema = \"http://json-schema.org/draft-04/schema#\";\n }\n else if (this.target === \"openapi-3.0\") {\n // OpenAPI 3.0 schema objects should not include a $schema property\n }\n else {\n // @ts-ignore\n console.warn(`Invalid target: ${this.target}`);\n }\n if (params.external?.uri) {\n const id = params.external.registry.get(schema)?.id;\n if (!id)\n throw new Error(\"Schema is missing an `id` property\");\n result.$id = params.external.uri(id);\n }\n Object.assign(result, root.def);\n // build defs object\n const defs = params.external?.defs ?? {};\n for (const entry of this.seen.entries()) {\n const seen = entry[1];\n if (seen.def && seen.defId) {\n defs[seen.defId] = seen.def;\n }\n }\n // set definitions in result\n if (params.external) {\n }\n else {\n if (Object.keys(defs).length > 0) {\n if (this.target === \"draft-2020-12\") {\n result.$defs = defs;\n }\n else {\n result.definitions = defs;\n }\n }\n }\n try {\n // this \"finalizes\" this schema and ensures all cycles are removed\n // each call to .emit() is functionally independent\n // though the seen map is shared\n return JSON.parse(JSON.stringify(result));\n }\n catch (_err) {\n throw new Error(\"Error converting schema to JSON.\");\n }\n }\n}\nexport function toJSONSchema(input, _params) {\n if (input instanceof $ZodRegistry) {\n const gen = new JSONSchemaGenerator(_params);\n const defs = {};\n for (const entry of input._idmap.entries()) {\n const [_, schema] = entry;\n gen.process(schema);\n }\n const schemas = {};\n const external = {\n registry: input,\n uri: _params?.uri,\n defs,\n };\n for (const entry of input._idmap.entries()) {\n const [key, schema] = entry;\n schemas[key] = gen.emit(schema, {\n ..._params,\n external,\n });\n }\n if (Object.keys(defs).length > 0) {\n const defsSegment = gen.target === \"draft-2020-12\" ? \"$defs\" : \"definitions\";\n schemas.__shared = {\n [defsSegment]: defs,\n };\n }\n return { schemas };\n }\n const gen = new JSONSchemaGenerator(_params);\n gen.process(input);\n return gen.emit(input, _params);\n}\nfunction isTransforming(_schema, _ctx) {\n const ctx = _ctx ?? { seen: new Set() };\n if (ctx.seen.has(_schema))\n return false;\n ctx.seen.add(_schema);\n const schema = _schema;\n const def = schema._zod.def;\n switch (def.type) {\n case \"string\":\n case \"number\":\n case \"bigint\":\n case \"boolean\":\n case \"date\":\n case \"symbol\":\n case \"undefined\":\n case \"null\":\n case \"any\":\n case \"unknown\":\n case \"never\":\n case \"void\":\n case \"literal\":\n case \"enum\":\n case \"nan\":\n case \"file\":\n case \"template_literal\":\n return false;\n case \"array\": {\n return isTransforming(def.element, ctx);\n }\n case \"object\": {\n for (const key in def.shape) {\n if (isTransforming(def.shape[key], ctx))\n return true;\n }\n return false;\n }\n case \"union\": {\n for (const option of def.options) {\n if (isTransforming(option, ctx))\n return true;\n }\n return false;\n }\n case \"intersection\": {\n return isTransforming(def.left, ctx) || isTransforming(def.right, ctx);\n }\n case \"tuple\": {\n for (const item of def.items) {\n if (isTransforming(item, ctx))\n return true;\n }\n if (def.rest && isTransforming(def.rest, ctx))\n return true;\n return false;\n }\n case \"record\": {\n return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx);\n }\n case \"map\": {\n return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx);\n }\n case \"set\": {\n return isTransforming(def.valueType, ctx);\n }\n // inner types\n case \"promise\":\n case \"optional\":\n case \"nonoptional\":\n case \"nullable\":\n case \"readonly\":\n return isTransforming(def.innerType, ctx);\n case \"lazy\":\n return isTransforming(def.getter(), ctx);\n case \"default\": {\n return isTransforming(def.innerType, ctx);\n }\n case \"prefault\": {\n return isTransforming(def.innerType, ctx);\n }\n case \"custom\": {\n return false;\n }\n case \"transform\": {\n return true;\n }\n case \"pipe\": {\n return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);\n }\n case \"success\": {\n return false;\n }\n case \"catch\": {\n return false;\n }\n case \"function\": {\n return false;\n }\n default:\n def;\n }\n throw new Error(`Unknown schema type: ${def.type}`);\n}\n","export {};\n","import * as core from \"../core/index.js\";\nimport * as schemas from \"./schemas.js\";\nexport const ZodISODateTime = /*@__PURE__*/ core.$constructor(\"ZodISODateTime\", (inst, def) => {\n core.$ZodISODateTime.init(inst, def);\n schemas.ZodStringFormat.init(inst, def);\n});\nexport function datetime(params) {\n return core._isoDateTime(ZodISODateTime, params);\n}\nexport const ZodISODate = /*@__PURE__*/ core.$constructor(\"ZodISODate\", (inst, def) => {\n core.$ZodISODate.init(inst, def);\n schemas.ZodStringFormat.init(inst, def);\n});\nexport function date(params) {\n return core._isoDate(ZodISODate, params);\n}\nexport const ZodISOTime = /*@__PURE__*/ core.$constructor(\"ZodISOTime\", (inst, def) => {\n core.$ZodISOTime.init(inst, def);\n schemas.ZodStringFormat.init(inst, def);\n});\nexport function time(params) {\n return core._isoTime(ZodISOTime, params);\n}\nexport const ZodISODuration = /*@__PURE__*/ core.$constructor(\"ZodISODuration\", (inst, def) => {\n core.$ZodISODuration.init(inst, def);\n schemas.ZodStringFormat.init(inst, def);\n});\nexport function duration(params) {\n return core._isoDuration(ZodISODuration, params);\n}\n","import * as core from \"../core/index.js\";\nimport { $ZodError } from \"../core/index.js\";\nimport * as util from \"../core/util.js\";\nconst initializer = (inst, issues) => {\n $ZodError.init(inst, issues);\n inst.name = \"ZodError\";\n Object.defineProperties(inst, {\n format: {\n value: (mapper) => core.formatError(inst, mapper),\n // enumerable: false,\n },\n flatten: {\n value: (mapper) => core.flattenError(inst, mapper),\n // enumerable: false,\n },\n addIssue: {\n value: (issue) => {\n inst.issues.push(issue);\n inst.message = JSON.stringify(inst.issues, util.jsonStringifyReplacer, 2);\n },\n // enumerable: false,\n },\n addIssues: {\n value: (issues) => {\n inst.issues.push(...issues);\n inst.message = JSON.stringify(inst.issues, util.jsonStringifyReplacer, 2);\n },\n // enumerable: false,\n },\n isEmpty: {\n get() {\n return inst.issues.length === 0;\n },\n // enumerable: false,\n },\n });\n // Object.defineProperty(inst, \"isEmpty\", {\n // get() {\n // return inst.issues.length === 0;\n // },\n // });\n};\nexport const ZodError = core.$constructor(\"ZodError\", initializer);\nexport const ZodRealError = core.$constructor(\"ZodError\", initializer, {\n Parent: Error,\n});\n// /** @deprecated Use `z.core.$ZodErrorMapCtx` instead. */\n// export type ErrorMapCtx = core.$ZodErrorMapCtx;\n","import * as core from \"../core/index.js\";\nimport { ZodRealError } from \"./errors.js\";\nexport const parse = /* @__PURE__ */ core._parse(ZodRealError);\nexport const parseAsync = /* @__PURE__ */ core._parseAsync(ZodRealError);\nexport const safeParse = /* @__PURE__ */ core._safeParse(ZodRealError);\nexport const safeParseAsync = /* @__PURE__ */ core._safeParseAsync(ZodRealError);\n// Codec functions\nexport const encode = /* @__PURE__ */ core._encode(ZodRealError);\nexport const decode = /* @__PURE__ */ core._decode(ZodRealError);\nexport const encodeAsync = /* @__PURE__ */ core._encodeAsync(ZodRealError);\nexport const decodeAsync = /* @__PURE__ */ core._decodeAsync(ZodRealError);\nexport const safeEncode = /* @__PURE__ */ core._safeEncode(ZodRealError);\nexport const safeDecode = /* @__PURE__ */ core._safeDecode(ZodRealError);\nexport const safeEncodeAsync = /* @__PURE__ */ core._safeEncodeAsync(ZodRealError);\nexport const safeDecodeAsync = /* @__PURE__ */ core._safeDecodeAsync(ZodRealError);\n","import * as core from \"../core/index.js\";\nimport { util } from \"../core/index.js\";\nimport * as checks from \"./checks.js\";\nimport * as iso from \"./iso.js\";\nimport * as parse from \"./parse.js\";\nexport const ZodType = /*@__PURE__*/ core.$constructor(\"ZodType\", (inst, def) => {\n core.$ZodType.init(inst, def);\n inst.def = def;\n inst.type = def.type;\n Object.defineProperty(inst, \"_def\", { value: def });\n // base methods\n inst.check = (...checks) => {\n return inst.clone({\n ...def,\n checks: [\n ...(def.checks ?? []),\n ...checks.map((ch) => typeof ch === \"function\" ? { _zod: { check: ch, def: { check: \"custom\" }, onattach: [] } } : ch),\n ],\n }\n // { parent: true }\n );\n };\n inst.clone = (def, params) => core.clone(inst, def, params);\n inst.brand = () => inst;\n inst.register = ((reg, meta) => {\n reg.add(inst, meta);\n return inst;\n });\n // parsing\n inst.parse = (data, params) => parse.parse(inst, data, params, { callee: inst.parse });\n inst.safeParse = (data, params) => parse.safeParse(inst, data, params);\n inst.parseAsync = async (data, params) => parse.parseAsync(inst, data, params, { callee: inst.parseAsync });\n inst.safeParseAsync = async (data, params) => parse.safeParseAsync(inst, data, params);\n inst.spa = inst.safeParseAsync;\n // encoding/decoding\n inst.encode = (data, params) => parse.encode(inst, data, params);\n inst.decode = (data, params) => parse.decode(inst, data, params);\n inst.encodeAsync = async (data, params) => parse.encodeAsync(inst, data, params);\n inst.decodeAsync = async (data, params) => parse.decodeAsync(inst, data, params);\n inst.safeEncode = (data, params) => parse.safeEncode(inst, data, params);\n inst.safeDecode = (data, params) => parse.safeDecode(inst, data, params);\n inst.safeEncodeAsync = async (data, params) => parse.safeEncodeAsync(inst, data, params);\n inst.safeDecodeAsync = async (data, params) => parse.safeDecodeAsync(inst, data, params);\n // refinements\n inst.refine = (check, params) => inst.check(refine(check, params));\n inst.superRefine = (refinement) => inst.check(superRefine(refinement));\n inst.overwrite = (fn) => inst.check(checks.overwrite(fn));\n // wrappers\n inst.optional = () => optional(inst);\n inst.nullable = () => nullable(inst);\n inst.nullish = () => optional(nullable(inst));\n inst.nonoptional = (params) => nonoptional(inst, params);\n inst.array = () => array(inst);\n inst.or = (arg) => union([inst, arg]);\n inst.and = (arg) => intersection(inst, arg);\n inst.transform = (tx) => pipe(inst, transform(tx));\n inst.default = (def) => _default(inst, def);\n inst.prefault = (def) => prefault(inst, def);\n // inst.coalesce = (def, params) => coalesce(inst, def, params);\n inst.catch = (params) => _catch(inst, params);\n inst.pipe = (target) => pipe(inst, target);\n inst.readonly = () => readonly(inst);\n // meta\n inst.describe = (description) => {\n const cl = inst.clone();\n core.globalRegistry.add(cl, { description });\n return cl;\n };\n Object.defineProperty(inst, \"description\", {\n get() {\n return core.globalRegistry.get(inst)?.description;\n },\n configurable: true,\n });\n inst.meta = (...args) => {\n if (args.length === 0) {\n return core.globalRegistry.get(inst);\n }\n const cl = inst.clone();\n core.globalRegistry.add(cl, args[0]);\n return cl;\n };\n // helpers\n inst.isOptional = () => inst.safeParse(undefined).success;\n inst.isNullable = () => inst.safeParse(null).success;\n return inst;\n});\n/** @internal */\nexport const _ZodString = /*@__PURE__*/ core.$constructor(\"_ZodString\", (inst, def) => {\n core.$ZodString.init(inst, def);\n ZodType.init(inst, def);\n const bag = inst._zod.bag;\n inst.format = bag.format ?? null;\n inst.minLength = bag.minimum ?? null;\n inst.maxLength = bag.maximum ?? null;\n // validations\n inst.regex = (...args) => inst.check(checks.regex(...args));\n inst.includes = (...args) => inst.check(checks.includes(...args));\n inst.startsWith = (...args) => inst.check(checks.startsWith(...args));\n inst.endsWith = (...args) => inst.check(checks.endsWith(...args));\n inst.min = (...args) => inst.check(checks.minLength(...args));\n inst.max = (...args) => inst.check(checks.maxLength(...args));\n inst.length = (...args) => inst.check(checks.length(...args));\n inst.nonempty = (...args) => inst.check(checks.minLength(1, ...args));\n inst.lowercase = (params) => inst.check(checks.lowercase(params));\n inst.uppercase = (params) => inst.check(checks.uppercase(params));\n // transforms\n inst.trim = () => inst.check(checks.trim());\n inst.normalize = (...args) => inst.check(checks.normalize(...args));\n inst.toLowerCase = () => inst.check(checks.toLowerCase());\n inst.toUpperCase = () => inst.check(checks.toUpperCase());\n});\nexport const ZodString = /*@__PURE__*/ core.$constructor(\"ZodString\", (inst, def) => {\n core.$ZodString.init(inst, def);\n _ZodString.init(inst, def);\n inst.email = (params) => inst.check(core._email(ZodEmail, params));\n inst.url = (params) => inst.check(core._url(ZodURL, params));\n inst.jwt = (params) => inst.check(core._jwt(ZodJWT, params));\n inst.emoji = (params) => inst.check(core._emoji(ZodEmoji, params));\n inst.guid = (params) => inst.check(core._guid(ZodGUID, params));\n inst.uuid = (params) => inst.check(core._uuid(ZodUUID, params));\n inst.uuidv4 = (params) => inst.check(core._uuidv4(ZodUUID, params));\n inst.uuidv6 = (params) => inst.check(core._uuidv6(ZodUUID, params));\n inst.uuidv7 = (params) => inst.check(core._uuidv7(ZodUUID, params));\n inst.nanoid = (params) => inst.check(core._nanoid(ZodNanoID, params));\n inst.guid = (params) => inst.check(core._guid(ZodGUID, params));\n inst.cuid = (params) => inst.check(core._cuid(ZodCUID, params));\n inst.cuid2 = (params) => inst.check(core._cuid2(ZodCUID2, params));\n inst.ulid = (params) => inst.check(core._ulid(ZodULID, params));\n inst.base64 = (params) => inst.check(core._base64(ZodBase64, params));\n inst.base64url = (params) => inst.check(core._base64url(ZodBase64URL, params));\n inst.xid = (params) => inst.check(core._xid(ZodXID, params));\n inst.ksuid = (params) => inst.check(core._ksuid(ZodKSUID, params));\n inst.ipv4 = (params) => inst.check(core._ipv4(ZodIPv4, params));\n inst.ipv6 = (params) => inst.check(core._ipv6(ZodIPv6, params));\n inst.cidrv4 = (params) => inst.check(core._cidrv4(ZodCIDRv4, params));\n inst.cidrv6 = (params) => inst.check(core._cidrv6(ZodCIDRv6, params));\n inst.e164 = (params) => inst.check(core._e164(ZodE164, params));\n // iso\n inst.datetime = (params) => inst.check(iso.datetime(params));\n inst.date = (params) => inst.check(iso.date(params));\n inst.time = (params) => inst.check(iso.time(params));\n inst.duration = (params) => inst.check(iso.duration(params));\n});\nexport function string(params) {\n return core._string(ZodString, params);\n}\nexport const ZodStringFormat = /*@__PURE__*/ core.$constructor(\"ZodStringFormat\", (inst, def) => {\n core.$ZodStringFormat.init(inst, def);\n _ZodString.init(inst, def);\n});\nexport const ZodEmail = /*@__PURE__*/ core.$constructor(\"ZodEmail\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodEmail.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function email(params) {\n return core._email(ZodEmail, params);\n}\nexport const ZodGUID = /*@__PURE__*/ core.$constructor(\"ZodGUID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodGUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function guid(params) {\n return core._guid(ZodGUID, params);\n}\nexport const ZodUUID = /*@__PURE__*/ core.$constructor(\"ZodUUID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodUUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function uuid(params) {\n return core._uuid(ZodUUID, params);\n}\nexport function uuidv4(params) {\n return core._uuidv4(ZodUUID, params);\n}\n// ZodUUIDv6\nexport function uuidv6(params) {\n return core._uuidv6(ZodUUID, params);\n}\n// ZodUUIDv7\nexport function uuidv7(params) {\n return core._uuidv7(ZodUUID, params);\n}\nexport const ZodURL = /*@__PURE__*/ core.$constructor(\"ZodURL\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodURL.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function url(params) {\n return core._url(ZodURL, params);\n}\nexport function httpUrl(params) {\n return core._url(ZodURL, {\n protocol: /^https?$/,\n hostname: core.regexes.domain,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodEmoji = /*@__PURE__*/ core.$constructor(\"ZodEmoji\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodEmoji.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function emoji(params) {\n return core._emoji(ZodEmoji, params);\n}\nexport const ZodNanoID = /*@__PURE__*/ core.$constructor(\"ZodNanoID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodNanoID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function nanoid(params) {\n return core._nanoid(ZodNanoID, params);\n}\nexport const ZodCUID = /*@__PURE__*/ core.$constructor(\"ZodCUID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodCUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function cuid(params) {\n return core._cuid(ZodCUID, params);\n}\nexport const ZodCUID2 = /*@__PURE__*/ core.$constructor(\"ZodCUID2\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodCUID2.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function cuid2(params) {\n return core._cuid2(ZodCUID2, params);\n}\nexport const ZodULID = /*@__PURE__*/ core.$constructor(\"ZodULID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodULID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function ulid(params) {\n return core._ulid(ZodULID, params);\n}\nexport const ZodXID = /*@__PURE__*/ core.$constructor(\"ZodXID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodXID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function xid(params) {\n return core._xid(ZodXID, params);\n}\nexport const ZodKSUID = /*@__PURE__*/ core.$constructor(\"ZodKSUID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodKSUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function ksuid(params) {\n return core._ksuid(ZodKSUID, params);\n}\nexport const ZodIPv4 = /*@__PURE__*/ core.$constructor(\"ZodIPv4\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodIPv4.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function ipv4(params) {\n return core._ipv4(ZodIPv4, params);\n}\nexport const ZodIPv6 = /*@__PURE__*/ core.$constructor(\"ZodIPv6\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodIPv6.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function ipv6(params) {\n return core._ipv6(ZodIPv6, params);\n}\nexport const ZodCIDRv4 = /*@__PURE__*/ core.$constructor(\"ZodCIDRv4\", (inst, def) => {\n core.$ZodCIDRv4.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function cidrv4(params) {\n return core._cidrv4(ZodCIDRv4, params);\n}\nexport const ZodCIDRv6 = /*@__PURE__*/ core.$constructor(\"ZodCIDRv6\", (inst, def) => {\n core.$ZodCIDRv6.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function cidrv6(params) {\n return core._cidrv6(ZodCIDRv6, params);\n}\nexport const ZodBase64 = /*@__PURE__*/ core.$constructor(\"ZodBase64\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodBase64.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function base64(params) {\n return core._base64(ZodBase64, params);\n}\nexport const ZodBase64URL = /*@__PURE__*/ core.$constructor(\"ZodBase64URL\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodBase64URL.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function base64url(params) {\n return core._base64url(ZodBase64URL, params);\n}\nexport const ZodE164 = /*@__PURE__*/ core.$constructor(\"ZodE164\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodE164.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function e164(params) {\n return core._e164(ZodE164, params);\n}\nexport const ZodJWT = /*@__PURE__*/ core.$constructor(\"ZodJWT\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodJWT.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function jwt(params) {\n return core._jwt(ZodJWT, params);\n}\nexport const ZodCustomStringFormat = /*@__PURE__*/ core.$constructor(\"ZodCustomStringFormat\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodCustomStringFormat.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function stringFormat(format, fnOrRegex, _params = {}) {\n return core._stringFormat(ZodCustomStringFormat, format, fnOrRegex, _params);\n}\nexport function hostname(_params) {\n return core._stringFormat(ZodCustomStringFormat, \"hostname\", core.regexes.hostname, _params);\n}\nexport function hex(_params) {\n return core._stringFormat(ZodCustomStringFormat, \"hex\", core.regexes.hex, _params);\n}\nexport function hash(alg, params) {\n const enc = params?.enc ?? \"hex\";\n const format = `${alg}_${enc}`;\n const regex = core.regexes[format];\n if (!regex)\n throw new Error(`Unrecognized hash format: ${format}`);\n return core._stringFormat(ZodCustomStringFormat, format, regex, params);\n}\nexport const ZodNumber = /*@__PURE__*/ core.$constructor(\"ZodNumber\", (inst, def) => {\n core.$ZodNumber.init(inst, def);\n ZodType.init(inst, def);\n inst.gt = (value, params) => inst.check(checks.gt(value, params));\n inst.gte = (value, params) => inst.check(checks.gte(value, params));\n inst.min = (value, params) => inst.check(checks.gte(value, params));\n inst.lt = (value, params) => inst.check(checks.lt(value, params));\n inst.lte = (value, params) => inst.check(checks.lte(value, params));\n inst.max = (value, params) => inst.check(checks.lte(value, params));\n inst.int = (params) => inst.check(int(params));\n inst.safe = (params) => inst.check(int(params));\n inst.positive = (params) => inst.check(checks.gt(0, params));\n inst.nonnegative = (params) => inst.check(checks.gte(0, params));\n inst.negative = (params) => inst.check(checks.lt(0, params));\n inst.nonpositive = (params) => inst.check(checks.lte(0, params));\n inst.multipleOf = (value, params) => inst.check(checks.multipleOf(value, params));\n inst.step = (value, params) => inst.check(checks.multipleOf(value, params));\n // inst.finite = (params) => inst.check(core.finite(params));\n inst.finite = () => inst;\n const bag = inst._zod.bag;\n inst.minValue =\n Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null;\n inst.maxValue =\n Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null;\n inst.isInt = (bag.format ?? \"\").includes(\"int\") || Number.isSafeInteger(bag.multipleOf ?? 0.5);\n inst.isFinite = true;\n inst.format = bag.format ?? null;\n});\nexport function number(params) {\n return core._number(ZodNumber, params);\n}\nexport const ZodNumberFormat = /*@__PURE__*/ core.$constructor(\"ZodNumberFormat\", (inst, def) => {\n core.$ZodNumberFormat.init(inst, def);\n ZodNumber.init(inst, def);\n});\nexport function int(params) {\n return core._int(ZodNumberFormat, params);\n}\nexport function float32(params) {\n return core._float32(ZodNumberFormat, params);\n}\nexport function float64(params) {\n return core._float64(ZodNumberFormat, params);\n}\nexport function int32(params) {\n return core._int32(ZodNumberFormat, params);\n}\nexport function uint32(params) {\n return core._uint32(ZodNumberFormat, params);\n}\nexport const ZodBoolean = /*@__PURE__*/ core.$constructor(\"ZodBoolean\", (inst, def) => {\n core.$ZodBoolean.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function boolean(params) {\n return core._boolean(ZodBoolean, params);\n}\nexport const ZodBigInt = /*@__PURE__*/ core.$constructor(\"ZodBigInt\", (inst, def) => {\n core.$ZodBigInt.init(inst, def);\n ZodType.init(inst, def);\n inst.gte = (value, params) => inst.check(checks.gte(value, params));\n inst.min = (value, params) => inst.check(checks.gte(value, params));\n inst.gt = (value, params) => inst.check(checks.gt(value, params));\n inst.gte = (value, params) => inst.check(checks.gte(value, params));\n inst.min = (value, params) => inst.check(checks.gte(value, params));\n inst.lt = (value, params) => inst.check(checks.lt(value, params));\n inst.lte = (value, params) => inst.check(checks.lte(value, params));\n inst.max = (value, params) => inst.check(checks.lte(value, params));\n inst.positive = (params) => inst.check(checks.gt(BigInt(0), params));\n inst.negative = (params) => inst.check(checks.lt(BigInt(0), params));\n inst.nonpositive = (params) => inst.check(checks.lte(BigInt(0), params));\n inst.nonnegative = (params) => inst.check(checks.gte(BigInt(0), params));\n inst.multipleOf = (value, params) => inst.check(checks.multipleOf(value, params));\n const bag = inst._zod.bag;\n inst.minValue = bag.minimum ?? null;\n inst.maxValue = bag.maximum ?? null;\n inst.format = bag.format ?? null;\n});\nexport function bigint(params) {\n return core._bigint(ZodBigInt, params);\n}\nexport const ZodBigIntFormat = /*@__PURE__*/ core.$constructor(\"ZodBigIntFormat\", (inst, def) => {\n core.$ZodBigIntFormat.init(inst, def);\n ZodBigInt.init(inst, def);\n});\n// int64\nexport function int64(params) {\n return core._int64(ZodBigIntFormat, params);\n}\n// uint64\nexport function uint64(params) {\n return core._uint64(ZodBigIntFormat, params);\n}\nexport const ZodSymbol = /*@__PURE__*/ core.$constructor(\"ZodSymbol\", (inst, def) => {\n core.$ZodSymbol.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function symbol(params) {\n return core._symbol(ZodSymbol, params);\n}\nexport const ZodUndefined = /*@__PURE__*/ core.$constructor(\"ZodUndefined\", (inst, def) => {\n core.$ZodUndefined.init(inst, def);\n ZodType.init(inst, def);\n});\nfunction _undefined(params) {\n return core._undefined(ZodUndefined, params);\n}\nexport { _undefined as undefined };\nexport const ZodNull = /*@__PURE__*/ core.$constructor(\"ZodNull\", (inst, def) => {\n core.$ZodNull.init(inst, def);\n ZodType.init(inst, def);\n});\nfunction _null(params) {\n return core._null(ZodNull, params);\n}\nexport { _null as null };\nexport const ZodAny = /*@__PURE__*/ core.$constructor(\"ZodAny\", (inst, def) => {\n core.$ZodAny.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function any() {\n return core._any(ZodAny);\n}\nexport const ZodUnknown = /*@__PURE__*/ core.$constructor(\"ZodUnknown\", (inst, def) => {\n core.$ZodUnknown.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function unknown() {\n return core._unknown(ZodUnknown);\n}\nexport const ZodNever = /*@__PURE__*/ core.$constructor(\"ZodNever\", (inst, def) => {\n core.$ZodNever.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function never(params) {\n return core._never(ZodNever, params);\n}\nexport const ZodVoid = /*@__PURE__*/ core.$constructor(\"ZodVoid\", (inst, def) => {\n core.$ZodVoid.init(inst, def);\n ZodType.init(inst, def);\n});\nfunction _void(params) {\n return core._void(ZodVoid, params);\n}\nexport { _void as void };\nexport const ZodDate = /*@__PURE__*/ core.$constructor(\"ZodDate\", (inst, def) => {\n core.$ZodDate.init(inst, def);\n ZodType.init(inst, def);\n inst.min = (value, params) => inst.check(checks.gte(value, params));\n inst.max = (value, params) => inst.check(checks.lte(value, params));\n const c = inst._zod.bag;\n inst.minDate = c.minimum ? new Date(c.minimum) : null;\n inst.maxDate = c.maximum ? new Date(c.maximum) : null;\n});\nexport function date(params) {\n return core._date(ZodDate, params);\n}\nexport const ZodArray = /*@__PURE__*/ core.$constructor(\"ZodArray\", (inst, def) => {\n core.$ZodArray.init(inst, def);\n ZodType.init(inst, def);\n inst.element = def.element;\n inst.min = (minLength, params) => inst.check(checks.minLength(minLength, params));\n inst.nonempty = (params) => inst.check(checks.minLength(1, params));\n inst.max = (maxLength, params) => inst.check(checks.maxLength(maxLength, params));\n inst.length = (len, params) => inst.check(checks.length(len, params));\n inst.unwrap = () => inst.element;\n});\nexport function array(element, params) {\n return core._array(ZodArray, element, params);\n}\n// .keyof\nexport function keyof(schema) {\n const shape = schema._zod.def.shape;\n return _enum(Object.keys(shape));\n}\nexport const ZodObject = /*@__PURE__*/ core.$constructor(\"ZodObject\", (inst, def) => {\n core.$ZodObjectJIT.init(inst, def);\n ZodType.init(inst, def);\n util.defineLazy(inst, \"shape\", () => def.shape);\n inst.keyof = () => _enum(Object.keys(inst._zod.def.shape));\n inst.catchall = (catchall) => inst.clone({ ...inst._zod.def, catchall: catchall });\n inst.passthrough = () => inst.clone({ ...inst._zod.def, catchall: unknown() });\n inst.loose = () => inst.clone({ ...inst._zod.def, catchall: unknown() });\n inst.strict = () => inst.clone({ ...inst._zod.def, catchall: never() });\n inst.strip = () => inst.clone({ ...inst._zod.def, catchall: undefined });\n inst.extend = (incoming) => {\n return util.extend(inst, incoming);\n };\n inst.safeExtend = (incoming) => {\n return util.safeExtend(inst, incoming);\n };\n inst.merge = (other) => util.merge(inst, other);\n inst.pick = (mask) => util.pick(inst, mask);\n inst.omit = (mask) => util.omit(inst, mask);\n inst.partial = (...args) => util.partial(ZodOptional, inst, args[0]);\n inst.required = (...args) => util.required(ZodNonOptional, inst, args[0]);\n});\nexport function object(shape, params) {\n const def = {\n type: \"object\",\n get shape() {\n util.assignProp(this, \"shape\", shape ? util.objectClone(shape) : {});\n return this.shape;\n },\n ...util.normalizeParams(params),\n };\n return new ZodObject(def);\n}\n// strictObject\nexport function strictObject(shape, params) {\n return new ZodObject({\n type: \"object\",\n get shape() {\n util.assignProp(this, \"shape\", util.objectClone(shape));\n return this.shape;\n },\n catchall: never(),\n ...util.normalizeParams(params),\n });\n}\n// looseObject\nexport function looseObject(shape, params) {\n return new ZodObject({\n type: \"object\",\n get shape() {\n util.assignProp(this, \"shape\", util.objectClone(shape));\n return this.shape;\n },\n catchall: unknown(),\n ...util.normalizeParams(params),\n });\n}\nexport const ZodUnion = /*@__PURE__*/ core.$constructor(\"ZodUnion\", (inst, def) => {\n core.$ZodUnion.init(inst, def);\n ZodType.init(inst, def);\n inst.options = def.options;\n});\nexport function union(options, params) {\n return new ZodUnion({\n type: \"union\",\n options: options,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodDiscriminatedUnion = /*@__PURE__*/ core.$constructor(\"ZodDiscriminatedUnion\", (inst, def) => {\n ZodUnion.init(inst, def);\n core.$ZodDiscriminatedUnion.init(inst, def);\n});\nexport function discriminatedUnion(discriminator, options, params) {\n // const [options, params] = args;\n return new ZodDiscriminatedUnion({\n type: \"union\",\n options,\n discriminator,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodIntersection = /*@__PURE__*/ core.$constructor(\"ZodIntersection\", (inst, def) => {\n core.$ZodIntersection.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function intersection(left, right) {\n return new ZodIntersection({\n type: \"intersection\",\n left: left,\n right: right,\n });\n}\nexport const ZodTuple = /*@__PURE__*/ core.$constructor(\"ZodTuple\", (inst, def) => {\n core.$ZodTuple.init(inst, def);\n ZodType.init(inst, def);\n inst.rest = (rest) => inst.clone({\n ...inst._zod.def,\n rest: rest,\n });\n});\nexport function tuple(items, _paramsOrRest, _params) {\n const hasRest = _paramsOrRest instanceof core.$ZodType;\n const params = hasRest ? _params : _paramsOrRest;\n const rest = hasRest ? _paramsOrRest : null;\n return new ZodTuple({\n type: \"tuple\",\n items: items,\n rest,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodRecord = /*@__PURE__*/ core.$constructor(\"ZodRecord\", (inst, def) => {\n core.$ZodRecord.init(inst, def);\n ZodType.init(inst, def);\n inst.keyType = def.keyType;\n inst.valueType = def.valueType;\n});\nexport function record(keyType, valueType, params) {\n return new ZodRecord({\n type: \"record\",\n keyType,\n valueType: valueType,\n ...util.normalizeParams(params),\n });\n}\n// type alksjf = core.output;\nexport function partialRecord(keyType, valueType, params) {\n const k = core.clone(keyType);\n k._zod.values = undefined;\n return new ZodRecord({\n type: \"record\",\n keyType: k,\n valueType: valueType,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodMap = /*@__PURE__*/ core.$constructor(\"ZodMap\", (inst, def) => {\n core.$ZodMap.init(inst, def);\n ZodType.init(inst, def);\n inst.keyType = def.keyType;\n inst.valueType = def.valueType;\n});\nexport function map(keyType, valueType, params) {\n return new ZodMap({\n type: \"map\",\n keyType: keyType,\n valueType: valueType,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodSet = /*@__PURE__*/ core.$constructor(\"ZodSet\", (inst, def) => {\n core.$ZodSet.init(inst, def);\n ZodType.init(inst, def);\n inst.min = (...args) => inst.check(core._minSize(...args));\n inst.nonempty = (params) => inst.check(core._minSize(1, params));\n inst.max = (...args) => inst.check(core._maxSize(...args));\n inst.size = (...args) => inst.check(core._size(...args));\n});\nexport function set(valueType, params) {\n return new ZodSet({\n type: \"set\",\n valueType: valueType,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodEnum = /*@__PURE__*/ core.$constructor(\"ZodEnum\", (inst, def) => {\n core.$ZodEnum.init(inst, def);\n ZodType.init(inst, def);\n inst.enum = def.entries;\n inst.options = Object.values(def.entries);\n const keys = new Set(Object.keys(def.entries));\n inst.extract = (values, params) => {\n const newEntries = {};\n for (const value of values) {\n if (keys.has(value)) {\n newEntries[value] = def.entries[value];\n }\n else\n throw new Error(`Key ${value} not found in enum`);\n }\n return new ZodEnum({\n ...def,\n checks: [],\n ...util.normalizeParams(params),\n entries: newEntries,\n });\n };\n inst.exclude = (values, params) => {\n const newEntries = { ...def.entries };\n for (const value of values) {\n if (keys.has(value)) {\n delete newEntries[value];\n }\n else\n throw new Error(`Key ${value} not found in enum`);\n }\n return new ZodEnum({\n ...def,\n checks: [],\n ...util.normalizeParams(params),\n entries: newEntries,\n });\n };\n});\nfunction _enum(values, params) {\n const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;\n return new ZodEnum({\n type: \"enum\",\n entries,\n ...util.normalizeParams(params),\n });\n}\nexport { _enum as enum };\n/** @deprecated This API has been merged into `z.enum()`. Use `z.enum()` instead.\n *\n * ```ts\n * enum Colors { red, green, blue }\n * z.enum(Colors);\n * ```\n */\nexport function nativeEnum(entries, params) {\n return new ZodEnum({\n type: \"enum\",\n entries,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodLiteral = /*@__PURE__*/ core.$constructor(\"ZodLiteral\", (inst, def) => {\n core.$ZodLiteral.init(inst, def);\n ZodType.init(inst, def);\n inst.values = new Set(def.values);\n Object.defineProperty(inst, \"value\", {\n get() {\n if (def.values.length > 1) {\n throw new Error(\"This schema contains multiple valid literal values. Use `.values` instead.\");\n }\n return def.values[0];\n },\n });\n});\nexport function literal(value, params) {\n return new ZodLiteral({\n type: \"literal\",\n values: Array.isArray(value) ? value : [value],\n ...util.normalizeParams(params),\n });\n}\nexport const ZodFile = /*@__PURE__*/ core.$constructor(\"ZodFile\", (inst, def) => {\n core.$ZodFile.init(inst, def);\n ZodType.init(inst, def);\n inst.min = (size, params) => inst.check(core._minSize(size, params));\n inst.max = (size, params) => inst.check(core._maxSize(size, params));\n inst.mime = (types, params) => inst.check(core._mime(Array.isArray(types) ? types : [types], params));\n});\nexport function file(params) {\n return core._file(ZodFile, params);\n}\nexport const ZodTransform = /*@__PURE__*/ core.$constructor(\"ZodTransform\", (inst, def) => {\n core.$ZodTransform.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n if (_ctx.direction === \"backward\") {\n throw new core.$ZodEncodeError(inst.constructor.name);\n }\n payload.addIssue = (issue) => {\n if (typeof issue === \"string\") {\n payload.issues.push(util.issue(issue, payload.value, def));\n }\n else {\n // for Zod 3 backwards compatibility\n const _issue = issue;\n if (_issue.fatal)\n _issue.continue = false;\n _issue.code ?? (_issue.code = \"custom\");\n _issue.input ?? (_issue.input = payload.value);\n _issue.inst ?? (_issue.inst = inst);\n // _issue.continue ??= true;\n payload.issues.push(util.issue(_issue));\n }\n };\n const output = def.transform(payload.value, payload);\n if (output instanceof Promise) {\n return output.then((output) => {\n payload.value = output;\n return payload;\n });\n }\n payload.value = output;\n return payload;\n };\n});\nexport function transform(fn) {\n return new ZodTransform({\n type: \"transform\",\n transform: fn,\n });\n}\nexport const ZodOptional = /*@__PURE__*/ core.$constructor(\"ZodOptional\", (inst, def) => {\n core.$ZodOptional.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function optional(innerType) {\n return new ZodOptional({\n type: \"optional\",\n innerType: innerType,\n });\n}\nexport const ZodNullable = /*@__PURE__*/ core.$constructor(\"ZodNullable\", (inst, def) => {\n core.$ZodNullable.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function nullable(innerType) {\n return new ZodNullable({\n type: \"nullable\",\n innerType: innerType,\n });\n}\n// nullish\nexport function nullish(innerType) {\n return optional(nullable(innerType));\n}\nexport const ZodDefault = /*@__PURE__*/ core.$constructor(\"ZodDefault\", (inst, def) => {\n core.$ZodDefault.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n inst.removeDefault = inst.unwrap;\n});\nexport function _default(innerType, defaultValue) {\n return new ZodDefault({\n type: \"default\",\n innerType: innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : util.shallowClone(defaultValue);\n },\n });\n}\nexport const ZodPrefault = /*@__PURE__*/ core.$constructor(\"ZodPrefault\", (inst, def) => {\n core.$ZodPrefault.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function prefault(innerType, defaultValue) {\n return new ZodPrefault({\n type: \"prefault\",\n innerType: innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : util.shallowClone(defaultValue);\n },\n });\n}\nexport const ZodNonOptional = /*@__PURE__*/ core.$constructor(\"ZodNonOptional\", (inst, def) => {\n core.$ZodNonOptional.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function nonoptional(innerType, params) {\n return new ZodNonOptional({\n type: \"nonoptional\",\n innerType: innerType,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodSuccess = /*@__PURE__*/ core.$constructor(\"ZodSuccess\", (inst, def) => {\n core.$ZodSuccess.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function success(innerType) {\n return new ZodSuccess({\n type: \"success\",\n innerType: innerType,\n });\n}\nexport const ZodCatch = /*@__PURE__*/ core.$constructor(\"ZodCatch\", (inst, def) => {\n core.$ZodCatch.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n inst.removeCatch = inst.unwrap;\n});\nfunction _catch(innerType, catchValue) {\n return new ZodCatch({\n type: \"catch\",\n innerType: innerType,\n catchValue: (typeof catchValue === \"function\" ? catchValue : () => catchValue),\n });\n}\nexport { _catch as catch };\nexport const ZodNaN = /*@__PURE__*/ core.$constructor(\"ZodNaN\", (inst, def) => {\n core.$ZodNaN.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function nan(params) {\n return core._nan(ZodNaN, params);\n}\nexport const ZodPipe = /*@__PURE__*/ core.$constructor(\"ZodPipe\", (inst, def) => {\n core.$ZodPipe.init(inst, def);\n ZodType.init(inst, def);\n inst.in = def.in;\n inst.out = def.out;\n});\nexport function pipe(in_, out) {\n return new ZodPipe({\n type: \"pipe\",\n in: in_,\n out: out,\n // ...util.normalizeParams(params),\n });\n}\nexport const ZodCodec = /*@__PURE__*/ core.$constructor(\"ZodCodec\", (inst, def) => {\n ZodPipe.init(inst, def);\n core.$ZodCodec.init(inst, def);\n});\nexport function codec(in_, out, params) {\n return new ZodCodec({\n type: \"pipe\",\n in: in_,\n out: out,\n transform: params.decode,\n reverseTransform: params.encode,\n });\n}\nexport const ZodReadonly = /*@__PURE__*/ core.$constructor(\"ZodReadonly\", (inst, def) => {\n core.$ZodReadonly.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function readonly(innerType) {\n return new ZodReadonly({\n type: \"readonly\",\n innerType: innerType,\n });\n}\nexport const ZodTemplateLiteral = /*@__PURE__*/ core.$constructor(\"ZodTemplateLiteral\", (inst, def) => {\n core.$ZodTemplateLiteral.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function templateLiteral(parts, params) {\n return new ZodTemplateLiteral({\n type: \"template_literal\",\n parts,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodLazy = /*@__PURE__*/ core.$constructor(\"ZodLazy\", (inst, def) => {\n core.$ZodLazy.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.getter();\n});\nexport function lazy(getter) {\n return new ZodLazy({\n type: \"lazy\",\n getter: getter,\n });\n}\nexport const ZodPromise = /*@__PURE__*/ core.$constructor(\"ZodPromise\", (inst, def) => {\n core.$ZodPromise.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function promise(innerType) {\n return new ZodPromise({\n type: \"promise\",\n innerType: innerType,\n });\n}\nexport const ZodFunction = /*@__PURE__*/ core.$constructor(\"ZodFunction\", (inst, def) => {\n core.$ZodFunction.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function _function(params) {\n return new ZodFunction({\n type: \"function\",\n input: Array.isArray(params?.input) ? tuple(params?.input) : (params?.input ?? array(unknown())),\n output: params?.output ?? unknown(),\n });\n}\nexport { _function as function };\nexport const ZodCustom = /*@__PURE__*/ core.$constructor(\"ZodCustom\", (inst, def) => {\n core.$ZodCustom.init(inst, def);\n ZodType.init(inst, def);\n});\n// custom checks\nexport function check(fn) {\n const ch = new core.$ZodCheck({\n check: \"custom\",\n // ...util.normalizeParams(params),\n });\n ch._zod.check = fn;\n return ch;\n}\nexport function custom(fn, _params) {\n return core._custom(ZodCustom, fn ?? (() => true), _params);\n}\nexport function refine(fn, _params = {}) {\n return core._refine(ZodCustom, fn, _params);\n}\n// superRefine\nexport function superRefine(fn) {\n return core._superRefine(fn);\n}\nfunction _instanceof(cls, params = {\n error: `Input not instance of ${cls.name}`,\n}) {\n const inst = new ZodCustom({\n type: \"custom\",\n check: \"custom\",\n fn: (data) => data instanceof cls,\n abort: true,\n ...util.normalizeParams(params),\n });\n inst._zod.bag.Class = cls;\n return inst;\n}\nexport { _instanceof as instanceof };\n// stringbool\nexport const stringbool = (...args) => core._stringbool({\n Codec: ZodCodec,\n Boolean: ZodBoolean,\n String: ZodString,\n}, ...args);\nexport function json(params) {\n const jsonSchema = lazy(() => {\n return union([string(params), number(), boolean(), _null(), array(jsonSchema), record(string(), jsonSchema)]);\n });\n return jsonSchema;\n}\n// preprocess\n// /** @deprecated Use `z.pipe()` and `z.transform()` instead. */\nexport function preprocess(fn, schema) {\n return pipe(transform(fn), schema);\n}\n","// Zod 3 compat layer\nimport * as core from \"../core/index.js\";\n/** @deprecated Use the raw string literal codes instead, e.g. \"invalid_type\". */\nexport const ZodIssueCode = {\n invalid_type: \"invalid_type\",\n too_big: \"too_big\",\n too_small: \"too_small\",\n invalid_format: \"invalid_format\",\n not_multiple_of: \"not_multiple_of\",\n unrecognized_keys: \"unrecognized_keys\",\n invalid_union: \"invalid_union\",\n invalid_key: \"invalid_key\",\n invalid_element: \"invalid_element\",\n invalid_value: \"invalid_value\",\n custom: \"custom\",\n};\nexport { $brand, config } from \"../core/index.js\";\n/** @deprecated Use `z.config(params)` instead. */\nexport function setErrorMap(map) {\n core.config({\n customError: map,\n });\n}\n/** @deprecated Use `z.config()` instead. */\nexport function getErrorMap() {\n return core.config().customError;\n}\n/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */\nexport var ZodFirstPartyTypeKind;\n(function (ZodFirstPartyTypeKind) {\n})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));\n","import * as core from \"../core/index.js\";\nimport * as schemas from \"./schemas.js\";\nexport function string(params) {\n return core._coercedString(schemas.ZodString, params);\n}\nexport function number(params) {\n return core._coercedNumber(schemas.ZodNumber, params);\n}\nexport function boolean(params) {\n return core._coercedBoolean(schemas.ZodBoolean, params);\n}\nexport function bigint(params) {\n return core._coercedBigint(schemas.ZodBigInt, params);\n}\nexport function date(params) {\n return core._coercedDate(schemas.ZodDate, params);\n}\n","import { z } from \"zod\";\nexport function tool(input) {\n return input;\n}\ntool.schema = z;\n","import {\n AGY_CLIENT_ID,\n AGY_CLIENT_SECRET,\n AGY_PROVIDER_ID\n} from '../constants';\nimport { agyFetch } from '../fetch';\nimport { formatRefreshParts, parseRefreshParts } from './auth';\nimport { clearCachedAuth, storeCachedAuth } from './cache';\nimport { invalidateProjectContextCache } from './project';\nimport {\n DEFAULT_MAX_ATTEMPTS,\n getExponentialDelayWithJitter,\n isRetryableNetworkError,\n isRetryableStatus,\n resolveRetryDelayMs,\n wait\n} from '../sdk/retry/helpers';\nimport type { OAuthAuthDetails, PluginClient, RefreshParts } from './types';\n\ninterface OAuthErrorPayload {\n error?:\n | string\n | {\n code?: string;\n status?: string;\n message?: string;\n };\n error_description?: string;\n}\n\n/**\n * NOTE: Special Design - Concurrent Refresh Lock Mechanism\n * When the IDE starts, or multiple requests concurrently call the Agy service and detect the current Access Token has expired,\n * a Token refresh process is triggered. If each concurrent request independently sends a refresh_token request to the Google API, it causes:\n * 1. Redundant network requests;\n * 2. \"Race conditions\" (a later refresh invalidates an earlier one, causing other concurrent requests to fail).\n *\n * Here, the `refreshInFlight` map is used to store ongoing refresh Promises. For the same refresh token,\n * only one actual refresh network request is made. Other concurrent requests wait on the shared Promise until it succeeds,\n * avoiding race conditions and greatly improving plugin robustness under high concurrency.\n */\nconst refreshInFlight = new Map>();\n\nfunction parseOAuthErrorPayload(text: string | undefined): { code?: string; description?: string } {\n if (!text) {\n return {};\n }\n\n try {\n const payload = JSON.parse(text) as OAuthErrorPayload;\n if (!payload || typeof payload !== 'object') {\n return { description: text };\n }\n\n let code: string | undefined;\n if (typeof payload.error === 'string') {\n code = payload.error;\n } else if (payload.error && typeof payload.error === 'object') {\n code = payload.error.status ?? payload.error.code;\n if (!payload.error_description && payload.error.message) {\n return { code, description: payload.error.message };\n }\n }\n\n const description = payload.error_description;\n if (description) {\n return { code, description };\n }\n\n if (payload.error && typeof payload.error === 'object' && payload.error.message) {\n return { code, description: payload.error.message };\n }\n\n return { code };\n } catch {\n return { description: text };\n }\n}\n\nexport async function refreshAccessToken(\n auth: OAuthAuthDetails,\n client: PluginClient\n): Promise {\n const parts = parseRefreshParts(auth.refresh);\n if (!parts.refreshToken) {\n return undefined;\n }\n\n const pending = refreshInFlight.get(parts.refreshToken);\n if (pending) {\n return pending;\n }\n\n const refreshPromise = refreshAccessTokenInternal(auth, client, parts);\n refreshInFlight.set(parts.refreshToken, refreshPromise);\n\n try {\n return await refreshPromise;\n } finally {\n refreshInFlight.delete(parts.refreshToken);\n }\n}\n\nasync function refreshAccessTokenInternal(\n auth: OAuthAuthDetails,\n client: PluginClient,\n parts: RefreshParts\n): Promise {\n try {\n const response = await fetchTokenRefresh(parts.refreshToken);\n\n if (!response.ok) {\n let errorText: string | undefined;\n try {\n errorText = await response.text();\n } catch {\n errorText = undefined;\n }\n\n const { code, description } = parseOAuthErrorPayload(errorText);\n const details = [code, description ?? errorText].filter(Boolean).join(': ');\n const baseMessage = `Antigravity token refresh failed (${response.status} ${response.statusText})`;\n console.warn(`[OAuth] ${details ? `${baseMessage} - ${details}` : baseMessage}`);\n\n if (code === 'invalid_grant') {\n console.warn(\n `[OAuth] Google revoked the stored refresh token. Run \\`opencode auth login\\` and reauthenticate.`\n );\n clearCachedAuth(auth.refresh);\n invalidateProjectContextCache(auth.refresh);\n try {\n const clearedAuth = {\n type: 'oauth' as const,\n refresh: formatRefreshParts({\n refreshToken: '',\n projectId: parts.projectId,\n managedProjectId: parts.managedProjectId\n }),\n access: '',\n expires: 0\n } as OAuthAuthDetails;\n await client.auth.set({\n path: { id: AGY_PROVIDER_ID },\n body: clearedAuth\n });\n } catch (storeError) {\n const errStr = storeError instanceof Error ? storeError.stack || storeError.message : String(storeError);\n console.warn(`[Agy Auth] Failed to clear stored Antigravity OAuth credentials: ${errStr}`);\n }\n }\n\n return undefined;\n }\n\n const payload = (await response.json()) as {\n access_token: string;\n expires_in: number;\n refresh_token?: string;\n };\n\n const refreshedParts: RefreshParts = {\n refreshToken: payload.refresh_token ?? parts.refreshToken,\n projectId: parts.projectId,\n managedProjectId: parts.managedProjectId\n };\n\n const updatedAuth: OAuthAuthDetails = {\n ...auth,\n access: payload.access_token,\n expires: Date.now() + payload.expires_in * 1000,\n refresh: formatRefreshParts(refreshedParts)\n };\n\n clearCachedAuth(auth.refresh);\n storeCachedAuth(updatedAuth);\n invalidateProjectContextCache(auth.refresh);\n\n if (refreshedParts.refreshToken !== parts.refreshToken) {\n try {\n await client.auth.set({\n path: { id: AGY_PROVIDER_ID },\n body: updatedAuth\n });\n } catch (storeError) {\n const errStr = storeError instanceof Error ? storeError.stack || storeError.message : String(storeError);\n console.warn(`[Agy Auth] Failed to persist refreshed Antigravity OAuth credentials: ${errStr}`);\n }\n }\n\n return updatedAuth;\n } catch (error) {\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to refresh Antigravity access token due to an unexpected error: ${errStr}`);\n return undefined;\n }\n}\n\nasync function fetchTokenRefresh(refreshToken: string): Promise {\n const tokenUrl = 'https://oauth2.googleapis.com/token';\n const clientId = AGY_CLIENT_ID;\n const clientSecret = AGY_CLIENT_SECRET;\n\n const init: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret\n })\n };\n\n let attempt = 1;\n while (attempt <= DEFAULT_MAX_ATTEMPTS) {\n try {\n const response = await agyFetch(tokenUrl, init);\n if (!isRetryableStatus(response.status) || attempt >= DEFAULT_MAX_ATTEMPTS) {\n return response;\n }\n\n const delayMs = await resolveRetryDelayMs(response, attempt);\n if (delayMs > 0) {\n await wait(delayMs);\n }\n attempt += 1;\n continue;\n } catch (error) {\n if (attempt >= DEFAULT_MAX_ATTEMPTS || !isRetryableNetworkError(error)) {\n throw error;\n }\n await wait(getExponentialDelayWithJitter(attempt));\n attempt += 1;\n }\n }\n\n return agyFetch(tokenUrl, init);\n}\n","export function clamp(value: number, min: number, max: number): number {\n if (Number.isNaN(value)) {\n return min;\n }\n if (value < min) {\n return min;\n }\n if (value > max) {\n return max;\n }\n return value;\n}\n\nexport function pad(value: string, width: number): string {\n if (value.length >= width) {\n return value;\n }\n return value.padEnd(width, \" \");\n}\n\nexport function buildProgressBar(fraction: number, width = 20): string {\n const clamped = clamp(fraction, 0, 1);\n const filled = clamped >= 1\n ? width\n : Math.max(0, Math.min(width, Math.max(clamped > 0 ? 1 : 0, Math.floor(clamped * width))));\n const empty = width - filled;\n return `${\"▓\".repeat(filled)}${\"░\".repeat(empty)}`;\n}\n\nexport function formatRemainingAmount(value: string | undefined): string | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n return value;\n }\n return parsed.toLocaleString(\"en-US\");\n}\n\nexport function formatRelativeResetTime(resetTime: string | undefined): string | undefined {\n if (!resetTime) {\n return undefined;\n }\n\n const resetAt = new Date(resetTime).getTime();\n if (Number.isNaN(resetAt)) {\n return undefined;\n }\n\n const diffMs = resetAt - Date.now();\n if (diffMs <= 0) {\n return \"reset pending\";\n }\n\n const totalMinutes = Math.ceil(diffMs / (1000 * 60));\n const hours = Math.floor(totalMinutes / 60);\n const minutes = totalMinutes % 60;\n\n if (hours > 0 && minutes > 0) {\n return `resets in ${hours}h ${minutes}m`;\n }\n if (hours > 0) {\n return `resets in ${hours}h`;\n }\n return `resets in ${minutes}m`;\n}\n","import { tool } from \"@opencode-ai/plugin\";\nimport { accessTokenExpired, isOAuthAuth, parseRefreshParts } from \"./auth\";\nimport { resolveCachedAuth } from \"./cache\";\nimport { ensureProjectContext, retrieveUserQuota } from \"./project\";\nimport type { RetrieveUserQuotaBucket } from \"./project/types\";\nimport { refreshAccessToken } from \"./token\";\nimport type { GetAuth, PluginClient } from \"./types\";\nimport { buildProgressBar, clamp, formatRemainingAmount, formatRelativeResetTime, pad } from \"./quota-utils\";\n\nexport const AGY_QUOTA_TOOL_NAME = \"agy_quota\";\n\ninterface AgyQuotaToolDependencies {\n client: PluginClient;\n getAuthResolver: () => GetAuth | undefined;\n getConfiguredProjectId: () => string | undefined;\n getUserAgentModel: () => string | undefined;\n}\n\nexport function createAgyQuotaTool({\n client,\n getAuthResolver,\n getConfiguredProjectId,\n getUserAgentModel,\n}: AgyQuotaToolDependencies) {\n return tool({\n description:\n \"Retrieve current Agy Code Assist quota usage for the authenticated user and project.\",\n args: {},\n async execute() {\n const getAuth = getAuthResolver();\n if (!getAuth) {\n return \"Agy quota is unavailable before Google auth is initialized. Authenticate with the Google provider and retry.\";\n }\n\n const auth = await getAuth();\n if (!isOAuthAuth(auth)) {\n return \"Agy quota requires OAuth with Google. Run `opencode auth login` and choose `Google OAuth (Antigravity CLI)` or `Google OAuth (Gemini CLI)`.\";\n }\n\n let authRecord = resolveCachedAuth(auth);\n if (accessTokenExpired(authRecord)) {\n const refreshed = await refreshAccessToken(authRecord, client);\n if (!refreshed?.access) {\n return \"Agy quota lookup failed because the access token could not be refreshed. Re-authenticate and retry.\";\n }\n authRecord = refreshed;\n }\n\n if (!authRecord.access) {\n return \"Agy quota lookup failed because no access token is available. Re-authenticate and retry.\";\n }\n\n try {\n const projectContext = await ensureProjectContext(\n authRecord,\n client,\n getConfiguredProjectId(),\n getUserAgentModel(),\n );\n if (!projectContext.effectiveProjectId) {\n return \"Agy quota lookup failed because no Google Cloud project could be resolved.\";\n }\n\n const quota = await retrieveUserQuota(\n authRecord.access,\n projectContext.effectiveProjectId,\n getUserAgentModel(),\n );\n if (!quota?.buckets?.length) {\n return `No Agy quota buckets were returned for project \\`${projectContext.effectiveProjectId}\\`.`;\n }\n\n return formatAgyQuotaOutput(\n projectContext.effectiveProjectId,\n quota.buckets,\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : \"unknown error\";\n return `Agy quota lookup failed: ${message}`;\n }\n },\n });\n}\n\nfunction formatAgyQuotaOutput(\n projectId: string,\n buckets: RetrieveUserQuotaBucket[],\n): string {\n const sortedBuckets = [...buckets].sort(compareQuotaBuckets);\n const groupedRows = groupQuotaRows(sortedBuckets);\n const versionGroups = groupByVersion(groupedRows);\n const variantWidth = Math.max(\n \"Variant\".length,\n ...versionGroups.flatMap((group) =>\n group.models.flatMap((model) => model.rows.map((row) => row.variant.length))\n ),\n );\n const tokenTypeValues = [...new Set(versionGroups.flatMap((group) =>\n group.models.flatMap((model) => model.rows.map((row) => row.tokenType))\n ))];\n const showTokenType = tokenTypeValues.length > 1 || tokenTypeValues[0] !== \"REQUESTS\";\n const lines = [\n `Agy quota usage for project \\`${projectId}\\``,\n \"\",\n showTokenType\n ? ` ↳ ${pad(\"Variant\", variantWidth)} Remaining Reset Type`\n : ` ↳ ${pad(\"Variant\", variantWidth)} Remaining Reset`,\n ];\n\n for (let index = 0; index < versionGroups.length; index += 1) {\n const versionGroup = versionGroups[index];\n if (!versionGroup) {\n continue;\n }\n if (index > 0) {\n lines.push(\"\");\n }\n lines.push(formatVersionGroupTitle(versionGroup));\n for (const model of versionGroup.models) {\n lines.push(model.baseModel);\n for (const row of model.rows) {\n lines.push(\n showTokenType\n ? ` ↳ ${pad(row.variant, variantWidth)} ${pad(row.usageRemaining, 27)} ${pad(row.resetValue, 8)} ${row.tokenType}`\n : ` ↳ ${pad(row.variant, variantWidth)} ${pad(row.usageRemaining, 27)} ${row.resetValue}`,\n );\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n\nfunction compareQuotaBuckets(\n left: RetrieveUserQuotaBucket,\n right: RetrieveUserQuotaBucket,\n): number {\n const leftModel = left.modelId ?? \"\";\n const rightModel = right.modelId ?? \"\";\n if (leftModel !== rightModel) {\n return leftModel.localeCompare(rightModel);\n }\n\n const leftTokenType = left.tokenType ?? \"\";\n const rightTokenType = right.tokenType ?? \"\";\n if (leftTokenType !== rightTokenType) {\n return leftTokenType.localeCompare(rightTokenType);\n }\n\n return (left.resetTime ?? \"\").localeCompare(right.resetTime ?? \"\");\n}\n\nfunction formatUsageRemaining(bucket: RetrieveUserQuotaBucket): string {\n const remainingAmount = formatRemainingAmount(bucket.remainingAmount);\n const remainingFraction = bucket.remainingFraction;\n const hasFraction =\n typeof remainingFraction === \"number\" && Number.isFinite(remainingFraction);\n\n if (hasFraction) {\n const clamped = clamp(remainingFraction, 0, 1);\n const percent = (clamped * 100).toFixed(1);\n const bar = buildProgressBar(clamped);\n return remainingAmount\n ? `${bar} ${percent}% (${remainingAmount} left)`\n : `${bar} ${percent}%`;\n }\n\n if (remainingAmount) {\n return remainingAmount;\n }\n\n return \"unknown\";\n}\n\nfunction normalizeTokenType(bucket: RetrieveUserQuotaBucket): string {\n const value = bucket.tokenType?.trim();\n return value ? value.toUpperCase() : \"REQUESTS\";\n}\n\ninterface GroupedQuotaRow {\n variant: string;\n usageRemaining: string;\n resetValue: string;\n tokenType: string;\n}\n\ninterface GroupedQuotaModel {\n baseModel: string;\n version: string | undefined;\n rows: GroupedQuotaRow[];\n}\n\nfunction groupQuotaRows(sortedBuckets: RetrieveUserQuotaBucket[]): GroupedQuotaModel[] {\n const groups = new Map();\n\n for (const bucket of sortedBuckets) {\n const modelId = bucket.modelId?.trim() || \"unknown-model\";\n const { baseModel, variant } = splitModelVariant(modelId);\n const usageRemaining = formatUsageRemaining(bucket);\n const resetLabel = formatRelativeResetTime(bucket.resetTime);\n const resetValue = resetLabel?.replace(\"resets in \", \"\") ?? \"-\";\n const tokenType = normalizeTokenType(bucket);\n\n const existing = groups.get(baseModel);\n if (existing) {\n existing.rows.push({\n variant,\n usageRemaining,\n resetValue,\n tokenType,\n });\n continue;\n }\n\n groups.set(baseModel, {\n baseModel,\n version: extractModelVersion(baseModel),\n rows: [{\n variant,\n usageRemaining,\n resetValue,\n tokenType,\n }],\n });\n }\n\n return [...groups.values()];\n}\n\ninterface VersionQuotaGroup {\n title: string;\n version: string | undefined;\n models: GroupedQuotaModel[];\n}\n\nfunction groupByVersion(models: GroupedQuotaModel[]): VersionQuotaGroup[] {\n const groups = new Map();\n\n for (const model of models) {\n const key = model.version ?? \"__unknown__\";\n const existing = groups.get(key);\n if (existing) {\n existing.models.push(model);\n continue;\n }\n\n groups.set(key, {\n title: model.version ? `Gemini ${model.version}` : \"Other\",\n version: model.version,\n models: [model],\n });\n }\n\n const ordered = [...groups.values()].sort((left, right) =>\n compareVersionDesc(left.version, right.version),\n );\n\n for (const group of ordered) {\n group.models.sort((left, right) => left.baseModel.localeCompare(right.baseModel));\n }\n\n return ordered;\n}\n\nfunction extractModelVersion(modelId: string): string | undefined {\n const match = modelId.match(/^gemini-([0-9]+(?:\\.[0-9]+)*)-/i);\n return match?.[1];\n}\n\nfunction compareVersionDesc(left: string | undefined, right: string | undefined): number {\n if (!left && !right) {\n return 0;\n }\n if (!left) {\n return 1;\n }\n if (!right) {\n return -1;\n }\n\n const leftSegments = left.split(\".\").map((part) => Number.parseInt(part, 10));\n const rightSegments = right.split(\".\").map((part) => Number.parseInt(part, 10));\n const max = Math.max(leftSegments.length, rightSegments.length);\n\n for (let index = 0; index < max; index += 1) {\n const l = leftSegments[index] ?? 0;\n const r = rightSegments[index] ?? 0;\n if (Number.isNaN(l) || Number.isNaN(r)) {\n break;\n }\n if (l > r) {\n return -1;\n }\n if (l < r) {\n return 1;\n }\n }\n\n return right.localeCompare(left);\n}\n\nfunction formatVersionGroupTitle(group: VersionQuotaGroup): string {\n const modelCount = group.models.length;\n const bucketCount = group.models.reduce((count, model) => count + model.rows.length, 0);\n const modelLabel = modelCount === 1 ? \"model\" : \"models\";\n const bucketLabel = bucketCount === 1 ? \"bucket\" : \"buckets\";\n return `${group.title} (${modelCount} ${modelLabel}, ${bucketCount} ${bucketLabel})`;\n}\n\nfunction splitModelVariant(modelId: string): { baseModel: string; variant: string } {\n const vertexSuffix = \"_vertex\";\n if (modelId.endsWith(vertexSuffix)) {\n return {\n baseModel: modelId.slice(0, -vertexSuffix.length),\n variant: \"vertex\",\n };\n }\n return {\n baseModel: modelId,\n variant: \"default\",\n };\n}\n","import { tool } from \"@opencode-ai/plugin\";\nimport { accessTokenExpired, isOAuthAuth, parseRefreshParts } from \"./auth\";\nimport { resolveCachedAuth } from \"./cache\";\nimport { ensureProjectContext, retrieveUserQuotaSummary } from \"./project\";\nimport type { QuotaSummaryBucket, QuotaSummaryGroup } from \"./project/types\";\nimport { refreshAccessToken } from \"./token\";\nimport type { GetAuth, PluginClient } from \"./types\";\nimport { buildProgressBar, clamp, formatRemainingAmount, formatRelativeResetTime } from \"./quota-utils\";\n\nexport const AGY_QUOTA_SUMMARY_TOOL_NAME = \"agy_quota_summary\";\n\ninterface AgyQuotaSummaryToolDependencies {\n client: PluginClient;\n getAuthResolver: () => GetAuth | undefined;\n getConfiguredProjectId: () => string | undefined;\n getUserAgentModel: () => string | undefined;\n}\n\nexport function createAgyQuotaSummaryTool({\n client,\n getAuthResolver,\n getConfiguredProjectId,\n getUserAgentModel,\n}: AgyQuotaSummaryToolDependencies) {\n return tool({\n description:\n \"Retrieve Agy Code Assist quota summary with weekly and 5-hour limits grouped by model family.\",\n args: {},\n async execute() {\n const getAuth = getAuthResolver();\n if (!getAuth) {\n return \"Agy quota summary is unavailable before Google auth is initialized. Authenticate with the Google provider and retry.\";\n }\n\n const auth = await getAuth();\n if (!isOAuthAuth(auth)) {\n return \"Agy quota summary requires OAuth with Google. Run `opencode auth login` and choose `Google OAuth (Antigravity CLI)` or `Google OAuth (Gemini CLI)`.\";\n }\n\n let authRecord = resolveCachedAuth(auth);\n if (accessTokenExpired(authRecord)) {\n const refreshed = await refreshAccessToken(authRecord, client);\n if (!refreshed?.access) {\n return \"Agy quota summary lookup failed because the access token could not be refreshed. Re-authenticate and retry.\";\n }\n authRecord = refreshed;\n }\n\n if (!authRecord.access) {\n return \"Agy quota summary lookup failed because no access token is available. Re-authenticate and retry.\";\n }\n\n try {\n const projectContext = await ensureProjectContext(\n authRecord,\n client,\n getConfiguredProjectId(),\n getUserAgentModel(),\n );\n if (!projectContext.effectiveProjectId) {\n return \"Agy quota summary lookup failed because no Google Cloud project could be resolved.\";\n }\n\n const summary = await retrieveUserQuotaSummary(\n authRecord.access,\n projectContext.effectiveProjectId,\n getUserAgentModel(),\n );\n if (!summary) {\n return `No Agy quota summary available for project \\`${projectContext.effectiveProjectId}\\`.`;\n }\n\n return formatAgyQuotaSummaryOutput(\n projectContext.effectiveProjectId,\n summary,\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : \"unknown error\";\n return `Agy quota summary lookup failed: ${message}`;\n }\n },\n });\n}\n\nconst BAR_WIDTH = 50;\n\nfunction windowLabel(window: string | undefined): string {\n switch (window?.toUpperCase()) {\n case \"WEEKLY\":\n return \"Weekly Limit\";\n case \"FIVE_HOUR\":\n return \"Five Hour Limit\";\n default:\n return \"Other Limit\";\n }\n}\n\nfunction formatSummaryBucket(bucket: QuotaSummaryBucket, indent: string): string[] {\n if (bucket.disabled) {\n const defaultDesc = `${windowLabel(bucket.window).toLowerCase()} exhausted`;\n const desc = bucket.description?.trim() || defaultDesc;\n return [`${indent}Disabled: ${desc}`];\n }\n\n const lines: string[] = [];\n const fraction = bucket.remainingFraction;\n const hasFraction = typeof fraction === \"number\" && Number.isFinite(fraction);\n\n if (hasFraction) {\n const clamped = clamp(fraction, 0, 1);\n const percent = (clamped * 100).toFixed(2);\n const bar = buildProgressBar(clamped, BAR_WIDTH);\n lines.push(`${indent}[${bar}] ${percent}%`);\n\n const remaining = formatRemainingAmount(bucket.remainingAmount);\n if (remaining) {\n const pctWhole = (clamped * 100).toFixed(0);\n lines.push(`${indent}${pctWhole}% remaining \\u00b7 ${remaining} left`);\n } else {\n const pctWhole = (clamped * 100).toFixed(0);\n lines.push(`${indent}${pctWhole}% remaining`);\n }\n } else {\n const remaining = formatRemainingAmount(bucket.remainingAmount);\n lines.push(remaining ? `${indent}${remaining} remaining` : `${indent}unknown remaining`);\n }\n\n const resetLabel = formatRelativeResetTime(bucket.resetTime);\n if (resetLabel) {\n const formattedReset = resetLabel.startsWith(\"resets in \")\n ? resetLabel.replace(\"resets in \", \"Refreshes in \")\n : resetLabel.charAt(0).toUpperCase() + resetLabel.slice(1);\n lines.push(`${indent}${formattedReset}`);\n }\n\n return lines;\n}\n\nfunction groupBucketsByWindow(buckets: QuotaSummaryBucket[]): Map {\n const groups = new Map();\n const order = [\"WEEKLY\", \"FIVE_HOUR\"];\n\n for (const bucket of buckets) {\n const key = bucket.window?.toUpperCase() || \"UNKNOWN\";\n const existing = groups.get(key);\n if (existing) {\n existing.push(bucket);\n } else {\n groups.set(key, [bucket]);\n }\n }\n\n const sorted = new Map();\n for (const key of order) {\n const group = groups.get(key);\n if (group) {\n sorted.set(key, group);\n }\n }\n for (const [key, group] of groups) {\n if (!sorted.has(key)) {\n sorted.set(key, group);\n }\n }\n\n return sorted;\n}\n\nfunction formatSummaryGroup(group: QuotaSummaryGroup): string[] {\n const lines: string[] = [];\n const name = group.displayName?.trim();\n if (name) {\n lines.push(name);\n }\n\n const desc = group.description?.trim();\n if (desc) {\n lines.push(` Models within this group: ${desc}`);\n }\n\n const buckets = group.buckets;\n if (buckets?.length) {\n const windowGroups = groupBucketsByWindow(buckets);\n let firstWindow = true;\n for (const [, windowBuckets] of windowGroups) {\n if (!firstWindow) {\n lines.push(\"\");\n }\n for (const bucket of windowBuckets) {\n const baseLabel = windowLabel(bucket.window);\n const label = bucket.displayName ? `${baseLabel} (${bucket.displayName})` : baseLabel;\n lines.push(` ${label}`);\n lines.push(...formatSummaryBucket(bucket, \" \"));\n firstWindow = false;\n }\n }\n }\n\n return lines;\n}\n\nfunction formatTopLevelBuckets(buckets: QuotaSummaryBucket[]): string[] {\n const lines: string[] = [];\n const windowGroups = groupBucketsByWindow(buckets);\n\n let firstWindow = true;\n for (const [, windowBuckets] of windowGroups) {\n if (!firstWindow) {\n lines.push(\"\");\n }\n for (const bucket of windowBuckets) {\n const baseLabel = windowLabel(bucket.window);\n const label = bucket.displayName ? `${baseLabel} (${bucket.displayName})` : baseLabel;\n lines.push(label);\n lines.push(...formatSummaryBucket(bucket, \" \"));\n firstWindow = false;\n }\n }\n\n return lines;\n}\n\nfunction formatAgyQuotaSummaryOutput(\n projectId: string,\n summary: { groups?: QuotaSummaryGroup[]; buckets?: QuotaSummaryBucket[]; description?: string },\n): string {\n const lines = [\n `Agy quota summary for project \\`${projectId}\\``,\n \"\",\n ];\n\n const groups = summary.groups;\n if (groups?.length) {\n for (let i = 0; i < groups.length; i++) {\n const group = groups[i];\n if (!group) {\n continue;\n }\n if (i > 0) {\n lines.push(\"\");\n }\n lines.push(...formatSummaryGroup(group));\n }\n } else if (summary.buckets?.length) {\n lines.push(...formatTopLevelBuckets(summary.buckets));\n } else {\n lines.push(\"No quota information available.\");\n }\n\n return lines.join(\"\\n\");\n}\n","import { classifyQuotaResponse } from \"../sdk/retry/quota\";\nimport type { PluginClient } from \"./types\";\n\nconst MODEL_CAPACITY_TOAST_COOLDOWN_MS = 30_000;\nconst modelCapacityToastCooldownByKey = new Map();\nconst TEST_TOAST_FLAG = \"OPENCODE_AGY_TEST_TOAST\";\nconst testToastShownByProject = new Set();\n\n/**\n * Shows a Toast notification to the user when the server-side Agy model capacity is exhausted.\n */\nexport async function maybeShowAgyCapacityToast(\n client: PluginClient,\n response: Response,\n projectId: string,\n requestedModel?: string,\n): Promise {\n if (response.status !== 429 || !client.tui?.showToast) {\n return;\n }\n\n const quotaContext = await classifyQuotaResponse(response);\n if (quotaContext?.reason !== \"MODEL_CAPACITY_EXHAUSTED\") {\n return;\n }\n\n const model = requestedModel ?? \"the selected model\";\n const toastKey = `${projectId}|${model}|MODEL_CAPACITY_EXHAUSTED`;\n const now = Date.now();\n const cooldownUntil = modelCapacityToastCooldownByKey.get(toastKey) ?? 0;\n if (cooldownUntil > now) {\n return;\n }\n modelCapacityToastCooldownByKey.set(toastKey, now + MODEL_CAPACITY_TOAST_COOLDOWN_MS);\n\n await client.tui.showToast({\n body: {\n title: \"Agy Capacity Unavailable\",\n message: `Google reports temporary server capacity limits for ${model}. Please retry in a few seconds.`,\n variant: \"warning\",\n duration: 7000,\n },\n });\n}\n\n/**\n * Temporary smoke test Toast, only enabled when OPENCODE_AGY_TEST_TOAST=1.\n */\nexport async function maybeShowAgyTestToast(\n client: PluginClient,\n projectId: string,\n): Promise {\n if (process.env[TEST_TOAST_FLAG]?.trim() !== \"1\" || !client.tui?.showToast) {\n return;\n }\n\n const key = projectId || \"global\";\n if (testToastShownByProject.has(key)) {\n return;\n }\n testToastShownByProject.add(key);\n\n await client.tui.showToast({\n body: {\n title: \"Agy Toast Test\",\n message: \"Temporary test toast from opencode-agy-auth.\",\n variant: \"info\",\n duration: 5000,\n },\n });\n}\n","import { randomUUID } from 'node:crypto';\nimport * as os from 'node:os';\nimport { AGY_CODE_ASSIST_ENDPOINT } from '../constants';\nimport { agyFetch } from '../fetch';\nimport { isRetryableStatus, getExponentialDelayWithJitter, wait } from '../sdk/retry/helpers';\nimport { getAgyCliVersion, buildAgyCliUserAgent } from '../sdk/user-agent';\n\nconst MAX_SEND_ATTEMPTS = 2;\n\nasync function sendWithRetry(label: string, url: string, init: RequestInit): Promise {\n for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt++) {\n try {\n const response = await agyFetch(url, init);\n if (response.ok) return;\n if (!isRetryableStatus(response.status) || attempt === MAX_SEND_ATTEMPTS - 1) {\n if (response.status >= 500 && response.status < 600) {\n console.debug(`[Agy] ${label} failed: ${response.status} (transient, suppressed)`);\n } else {\n console.warn(`[Agy] ${label} failed: ${response.status}`);\n }\n return;\n }\n } catch (error) {\n if (attempt === MAX_SEND_ATTEMPTS - 1) {\n console.debug(`[Agy] ${label} failed with network error: ${error} (suppressed)`);\n return;\n }\n }\n await wait(getExponentialDelayWithJitter(attempt + 1));\n }\n}\n\nlet lastTrafficTime = 0;\nconst TRAFFIC_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Simulates the experimental fetch and metric telemetry traffic sent periodically by the official Agy client in the background to prevent API bans or anomaly detection.\n */\nexport function simulateClientBackgroundTraffic(accessToken: string, projectId: string, userAgentModel?: string): void {\n const now = Date.now();\n if (now - lastTrafficTime < TRAFFIC_INTERVAL_MS) {\n return;\n }\n lastTrafficTime = now;\n\n // Asynchronous silent send, never blocking the core generation flow of the editor.\n Promise.all([\n sendListExperiments(accessToken, userAgentModel),\n sendCodeAssistMetrics(accessToken, projectId, userAgentModel),\n sendTrajectoryAnalytics(accessToken, userAgentModel)\n ]).catch((err) => {\n console.warn(`[Agy] Failed to send background traffic: ${err}`);\n });\n}\n\nasync function sendListExperiments(accessToken: string, userAgentModel?: string) {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:listExperiments`;\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n\n await sendWithRetry('listExperiments', url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': userAgent,\n Authorization: `Bearer ${accessToken}`\n },\n body: '{}'\n });\n}\n\nasync function sendCodeAssistMetrics(accessToken: string, projectId: string, userAgentModel?: string) {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:recordCodeAssistMetrics`;\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n\n const plat = os.platform();\n const arch = os.arch();\n\n const platMap: Record = {\n darwin: 'DARWIN',\n linux: 'LINUX',\n win32: 'WINDOWS'\n };\n const archMap: Record = {\n x64: 'AMD64',\n arm64: 'ARM64'\n };\n\n const p = platMap[plat] || 'UNSPECIFIED';\n const a = archMap[arch] || 'UNSPECIFIED';\n const platform = `${p}_${a}`;\n\n const traceId = randomUUID().replace(/-/g, '').slice(0, 16);\n const trajectoryId = randomUUID();\n const requestId = randomUUID();\n\n const body = {\n project: projectId,\n requestId,\n metadata: {\n ideType: 'ANTIGRAVITY',\n ideVersion: getAgyCliVersion(),\n platform\n },\n metrics: [\n {\n timestamp: new Date().toISOString(),\n conversationOffered: {\n status: 'ACTION_STATUS_NO_ERROR',\n traceId,\n streamingLatency: {\n firstMessageLatency: '0.5s',\n totalLatency: '1.0s'\n },\n isAgentic: true,\n initiationMethod: 'AGENT',\n trajectoryId,\n language: 'unspecified'\n }\n }\n ]\n };\n\n await sendWithRetry('recordCodeAssistMetrics', url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': userAgent,\n Authorization: `Bearer ${accessToken}`\n },\n body: JSON.stringify(body)\n });\n}\n\nasync function sendTrajectoryAnalytics(accessToken: string, userAgentModel?: string) {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:recordTrajectoryAnalytics`;\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n\n const plat = os.platform();\n const arch = os.arch();\n\n const platMap: Record = {\n darwin: 'DARWIN',\n linux: 'LINUX',\n win32: 'WINDOWS'\n };\n const archMap: Record = {\n x64: 'AMD64',\n arm64: 'ARM64'\n };\n\n const p = platMap[plat] || 'UNSPECIFIED';\n const a = archMap[arch] || 'UNSPECIFIED';\n const platform = `${p}_${a}`;\n\n const body = buildTrajectoryAnalyticsBody(randomUUID(), platform);\n\n await sendWithRetry('recordTrajectoryAnalytics', url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': userAgent,\n Authorization: `Bearer ${accessToken}`\n },\n body: JSON.stringify(body)\n });\n}\n\nexport function buildTrajectoryAnalyticsBody(cascadeId = randomUUID(), platform = 'DARWIN_AMD64') {\n return {\n trajectory: {\n cascadeId,\n executorMetadatas: [\n {\n cascadeConfig: {\n agentApiConfig: {\n enabled: false\n },\n checkpointConfig: {\n checkpointModel: 'MODEL_PLACEHOLDER_M50',\n strategy: 'CHECKPOINT_STRATEGY_SINGLE_PROMPT',\n maxTokenLimit: '128000',\n tokenThreshold: '50000',\n maxOverheadRatio: '0.15',\n movingWindowSize: '1',\n enabled: true,\n maxOutputTokens: '16384',\n useLastPlannerModel: false,\n isSync: false,\n maxUserRequests: 10,\n includeLastUserMessage: false,\n includeConversationLog: true,\n includeRunningTaskSnapshots: true,\n includeSubagentSnapshots: true,\n includeArtifactSnapshots: true,\n retryConfig: {\n maxRetries: 0,\n initialSleepDurationMs: 1000,\n exponentialMultiplier: 2,\n includeErrorFeedback: false\n }\n }\n }\n }\n ]\n },\n mendelExperimentIds: [],\n metadata: {\n ideType: 'ANTIGRAVITY',\n ideVersion: getAgyCliVersion(),\n platform\n },\n startStepIndex: '0'\n };\n}\n","import { randomUUID } from \"node:crypto\";\n\nimport { AGY_CODE_ASSIST_ENDPOINT } from \"../../constants\";\nimport modelsJson from \"../../../models.json\";\nimport { normalizeThinkingConfig } from \"../request-helpers\";\nimport { buildAgyCliUserAgent } from \"../user-agent\";\nimport { normalizeRequestPayloadIdentifiers, normalizeWrappedIdentifiers } from \"./identifiers\";\nimport { addThoughtSignaturesToFunctionCalls, transformOpenAIToolCalls } from \"./openai\";\nimport { isGenerativeLanguageRequest, parseGenerativeLanguageRequest } from \"./shared\";\nimport { getLatestSignature } from \"../../plugin/cache\";\nimport { closeToolLoopForThinking } from \"./thinking\";\nimport { getTurnStateTracker } from \"./turn-state-tracker\";\nimport { getToolMapper, sanitizeToolName, type ToolMapper } from \"./tool-mapper\";\n\nconst STREAM_ACTION = \"streamGenerateContent\";\n\nexport interface ThinkingConfigDefaults {\n provider?: unknown;\n models?: Record;\n}\n\n/**\n * Rewrites OpenAI-style requests into the format for Gemini Code Assist requests.\n */\nexport function prepareAgyRequest(\n input: RequestInfo,\n init: RequestInit | undefined,\n accessToken: string,\n projectId: string,\n thinkingConfigDefaults?: ThinkingConfigDefaults,\n): {\n request: RequestInfo;\n init: RequestInit;\n streaming: boolean;\n requestedModel?: string;\n sessionId?: string;\n} {\n const baseInit: RequestInit = { ...init };\n const headers = new Headers(init?.headers ?? {});\n\n if (!isGenerativeLanguageRequest(input)) {\n return {\n request: input,\n init: { ...baseInit, headers },\n streaming: false,\n };\n }\n\n const requestTarget = parseGenerativeLanguageRequest(input);\n if (!requestTarget) {\n return {\n request: input,\n init: { ...baseInit, headers },\n streaming: false,\n };\n }\n\n headers.set(\"Authorization\", `Bearer ${accessToken}`);\n headers.delete(\"x-api-key\");\n headers.delete(\"x-goog-api-key\");\n\n const { requestedModel: rawModel, effectiveModel, action: rawAction } = requestTarget;\n const streaming = rawAction === STREAM_ACTION;\n\n const transformedUrl = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:${rawAction}${\n streaming ? \"?alt=sse\" : \"\"\n }`;\n\n let body = baseInit.body;\n let sessionId: string | undefined;\n\n if (typeof baseInit.body === \"string\" && baseInit.body) {\n const transformed = transformRequestBody(\n baseInit.body,\n projectId,\n effectiveModel,\n rawModel,\n thinkingConfigDefaults,\n );\n if (transformed.body) {\n body = transformed.body;\n }\n sessionId = transformed.sessionId;\n }\n\n if (streaming) {\n headers.set(\"Accept\", \"text/event-stream\");\n }\n\n const userAgent = buildAgyCliUserAgent(effectiveModel);\n headers.set(\"User-Agent\", userAgent);\n\n return {\n request: transformedUrl,\n init: {\n ...baseInit,\n headers,\n body,\n },\n streaming,\n requestedModel: rawModel,\n sessionId,\n };\n}\n\nfunction getModelEnum(modelName: string): string {\n const deprecated = (modelsJson as any).deprecatedModelIds;\n if (deprecated && deprecated[modelName] && deprecated[modelName].newModelEnum) {\n return deprecated[modelName].newModelEnum;\n }\n const models = (modelsJson as any).models;\n if (models && models[modelName] && models[modelName].model) {\n return models[modelName].model;\n }\n if (deprecated && deprecated[modelName] && deprecated[modelName].oldModelEnum) {\n return deprecated[modelName].oldModelEnum;\n }\n return \"MODEL_PLACEHOLDER_M16\";\n}\n\nfunction transformRequestBody(\n body: string,\n projectId: string,\n effectiveModel: string,\n requestedModel: string,\n thinkingConfigDefaults?: ThinkingConfigDefaults,\n): { body?: string; userPromptId: string; sessionId?: string } {\n const fallbackId = randomUUID();\n try {\n const parsedBody = JSON.parse(body) as Record;\n const isWrapped = typeof parsedBody.project === \"string\" && \"request\" in parsedBody;\n\n if (isWrapped) {\n const wrappedBody = {\n ...parsedBody,\n model: effectiveModel,\n } as Record;\n\n const wrappedModel = (wrappedBody.model as string) || \"\";\n if (wrappedModel.includes(\"-image\") && !wrappedBody.requestType) {\n wrappedBody.requestType = \"image_gen\";\n wrappedBody.userAgent = wrappedBody.userAgent || \"antigravity\";\n }\n\n const { userPromptId, sessionId, requestId } = normalizeWrappedIdentifiers(wrappedBody);\n const toolMapper = getToolMapper(sessionId);\n\n const requestPayloadInside = wrappedBody.request as Record | undefined;\n if (requestPayloadInside) {\n toolMapper.registerFromFunctionDeclarations(requestPayloadInside.tools);\n toolMapper.registerFromContents(requestPayloadInside.contents);\n\n normalizeThinking(\n requestPayloadInside,\n resolveDefaultThinkingConfig(thinkingConfigDefaults, requestedModel, effectiveModel),\n thinkingConfigDefaults?.provider,\n );\n }\n if (requestPayloadInside && !requestPayloadInside.labels) {\n requestPayloadInside.labels = {\n last_execution_id: randomUUID(),\n last_step_index: \"0\",\n model_enum: getModelEnum(effectiveModel),\n trajectory_id: randomUUID(),\n used_claude: \"false\",\n used_claude_conservative: \"false\"\n };\n }\n\n if (requestPayloadInside && Array.isArray(requestPayloadInside.tools)) {\n normalizeToolSchemaTypes(requestPayloadInside.tools, toolMapper);\n }\n if (requestPayloadInside) {\n normalizeToolConfig(requestPayloadInside, toolMapper);\n }\n if (requestPayloadInside && Array.isArray(requestPayloadInside.contents)) {\n let contents = requestPayloadInside.contents;\n\n normalizeToolNamesInContents(contents, toolMapper);\n injectMissingToolCallIds(contents);\n fixOrphanedFunctionResponses(contents);\n\n const tracker = getTurnStateTracker();\n let needsRecovery = false;\n if (sessionId && tracker) {\n const state = tracker.getState(sessionId) ?? tracker.recoverFromContents(sessionId, contents);\n needsRecovery = state.inToolLoop && !state.turnHasThinking;\n }\n if (needsRecovery) {\n contents = closeToolLoopForThinking(contents);\n }\n\n contents = normalizeContentsSequence(contents);\n contents = ensureTrailingUserTurn(contents);\n\n const latestSig = getLatestSignature(sessionId);\n applyLatestSignature(contents, latestSig);\n requestPayloadInside.contents = contents;\n }\n\n return { body: JSON.stringify(wrappedBody), userPromptId, sessionId };\n }\n\n const requestPayload = { ...parsedBody };\n const { userPromptId, sessionId, requestId } = normalizeRequestPayloadIdentifiers(requestPayload);\n const toolMapper = getToolMapper(sessionId);\n\n toolMapper.registerFromOpenAITools(requestPayload.tools);\n toolMapper.registerFromFunctionDeclarations(requestPayload.tools);\n toolMapper.registerFromContents(requestPayload.contents);\n\n if (Array.isArray(requestPayload.tools)) {\n normalizeToolSchemaTypes(requestPayload.tools, toolMapper);\n }\n normalizeToolConfig(requestPayload, toolMapper);\n transformOpenAIToolCalls(requestPayload, toolMapper);\n addThoughtSignaturesToFunctionCalls(requestPayload);\n normalizeThinking(\n requestPayload,\n resolveDefaultThinkingConfig(thinkingConfigDefaults, requestedModel, effectiveModel),\n thinkingConfigDefaults?.provider,\n );\n normalizeSystemInstruction(requestPayload);\n normalizeCachedContent(requestPayload);\n\n let contents = requestPayload.contents as any[];\n if (Array.isArray(contents)) {\n normalizeToolNamesInContents(contents, toolMapper);\n injectMissingToolCallIds(contents);\n fixOrphanedFunctionResponses(contents);\n\n const tracker = getTurnStateTracker();\n let needsRecovery = false;\n if (sessionId && tracker) {\n const state = tracker.getState(sessionId) ?? tracker.recoverFromContents(sessionId, contents);\n needsRecovery = state.inToolLoop && !state.turnHasThinking;\n }\n if (needsRecovery) {\n contents = closeToolLoopForThinking(contents);\n }\n\n contents = normalizeContentsSequence(contents);\n contents = ensureTrailingUserTurn(contents);\n\n const latestSig = getLatestSignature(sessionId);\n applyLatestSignature(contents, latestSig);\n requestPayload.contents = contents;\n }\n\n if (\"model\" in requestPayload) {\n delete requestPayload.model;\n }\n\n if (!requestPayload.labels) {\n requestPayload.labels = {\n last_execution_id: randomUUID(),\n last_step_index: \"0\",\n model_enum: getModelEnum(effectiveModel),\n trajectory_id: randomUUID(),\n used_claude: \"false\",\n used_claude_conservative: \"false\"\n };\n }\n\n const isImageGen =\n effectiveModel.includes(\"-image\") ||\n requestedModel.includes(\"-image\") ||\n (typeof requestPayload.generationConfig === \"object\" &&\n requestPayload.generationConfig !== null &&\n \"imageConfig\" in (requestPayload.generationConfig as Record));\n\n const wrappedBody: Record = {\n project: projectId,\n model: effectiveModel,\n requestId,\n request: requestPayload,\n userAgent: \"antigravity\"\n };\n\n if (isImageGen) {\n wrappedBody.requestType = \"image_gen\";\n } else if (\n effectiveModel.includes(\"gemini-3.5-flash-lite\") ||\n effectiveModel.includes(\"gemini-3.1-flash-lite\")\n ) {\n wrappedBody.requestType = \"checkpoint\";\n } else if (effectiveModel.includes(\"gemini-2.5-flash-lite\")) {\n wrappedBody.requestType = \"chat\";\n } else if (effectiveModel.includes(\"-lite\")) {\n wrappedBody.requestType = \"web_search\";\n } else {\n wrappedBody.requestType = \"agent\";\n }\n\n return { body: JSON.stringify(wrappedBody), userPromptId, sessionId };\n } catch (error) {\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to transform Gemini request body: ${errStr}`);\n return { userPromptId: fallbackId };\n }\n}\n\nfunction resolveDefaultThinkingConfig(\n thinkingConfigDefaults: ThinkingConfigDefaults | undefined,\n requestedModel: string,\n effectiveModel: string,\n): unknown {\n const configured = thinkingConfigDefaults?.models\n ? thinkingConfigDefaults.models[requestedModel] ?? thinkingConfigDefaults.models[effectiveModel]\n : undefined;\n if (configured !== undefined) {\n return configured;\n }\n\n return getImplicitThinkingConfigForModel(requestedModel) ?? getImplicitThinkingConfigForModel(effectiveModel);\n}\n\nfunction normalizeThinking(\n requestPayload: Record,\n modelThinkingConfig: unknown,\n providerThinkingConfig: unknown,\n): void {\n const rawGenerationConfig = isRecord(requestPayload.generationConfig)\n ? { ...requestPayload.generationConfig }\n : undefined;\n const mergedThinkingConfig = mergeThinkingConfigs(\n providerThinkingConfig,\n modelThinkingConfig,\n requestPayload.thinkingConfig,\n rawGenerationConfig?.thinkingConfig,\n );\n\n if (Object.prototype.hasOwnProperty.call(requestPayload, \"thinkingConfig\")) {\n delete requestPayload.thinkingConfig;\n }\n\n const normalizedThinkingConfig = normalizeThinkingConfig(mergedThinkingConfig);\n\n if (!normalizedThinkingConfig) {\n if (rawGenerationConfig) {\n requestPayload.generationConfig = rawGenerationConfig;\n }\n return;\n }\n\n requestPayload.generationConfig = {\n ...(rawGenerationConfig ?? {}),\n thinkingConfig: normalizedThinkingConfig,\n };\n}\n\nfunction getImplicitThinkingConfigForModel(modelId: string): unknown {\n const normalizedModelId = modelId.toLowerCase();\n if (!normalizedModelId.startsWith(\"gemini-\") || normalizedModelId.includes(\"image\")) {\n return undefined;\n }\n\n return {\n thinkingBudget: normalizedModelId.endsWith(\"-low\") ? 1000 : 10001,\n includeThoughts: true,\n };\n}\n\nfunction mergeThinkingConfigs(...configs: unknown[]): Record | undefined {\n const merged: Record = {};\n for (const config of configs) {\n const normalized = normalizeThinkingConfig(config);\n if (!normalized) {\n continue;\n }\n for (const [key, value] of Object.entries(normalized)) {\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n }\n return Object.keys(merged).length > 0 ? merged : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction normalizeToolSchemaTypes(tools: unknown, toolMapper?: ToolMapper): void {\n if (!Array.isArray(tools)) return;\n\n const validSchemaKeys = new Set([\n \"type\", \"description\", \"properties\", \"items\",\n \"required\", \"enum\", \"nullable\", \"format\"\n ]);\n\n const sanitizeSchema = (obj: any) => {\n if (!obj || typeof obj !== \"object\") return;\n if (Array.isArray(obj)) {\n obj.forEach(sanitizeSchema);\n return;\n }\n\n if (!obj.type && (Array.isArray(obj.anyOf) || Array.isArray(obj.oneOf))) {\n const arr = obj.anyOf || obj.oneOf;\n if (arr.length > 0 && arr[0] && typeof arr[0].type === \"string\") {\n obj.type = arr[0].type;\n if (arr[0].items) {\n obj.items = arr[0].items;\n }\n } else {\n obj.type = \"STRING\";\n }\n }\n\n if (typeof obj.type === \"string\") {\n const t = obj.type.toLowerCase();\n if ([\"string\", \"number\", \"integer\", \"boolean\", \"array\", \"object\"].includes(t)) {\n obj.type = t.toUpperCase();\n }\n }\n\n if (Array.isArray(obj.enum)) {\n const sanitizedEnum = Array.from(\n new Set(\n obj.enum\n .filter((v: unknown) => v !== null && v !== undefined)\n .map((v: unknown) => String(v))\n )\n );\n if (sanitizedEnum.length > 0) {\n obj.enum = sanitizedEnum;\n // Gemini Protobuf Schema requires enum elements to be strings.\n // If the property was typed as BOOLEAN, adjust type to STRING so Gemini's schema validation accepts it.\n if (obj.type === \"BOOLEAN\") {\n obj.type = \"STRING\";\n }\n } else {\n delete obj.enum;\n }\n } else if (\"enum\" in obj) {\n delete obj.enum;\n }\n\n if (obj.properties && typeof obj.properties === \"object\") {\n Object.values(obj.properties).forEach(sanitizeSchema);\n }\n if (obj.items && typeof obj.items === \"object\") {\n sanitizeSchema(obj.items);\n }\n\n for (const key of Object.keys(obj)) {\n if (!validSchemaKeys.has(key)) {\n delete obj[key];\n }\n }\n };\n\n for (const tool of tools) {\n if (tool && Array.isArray(tool.functionDeclarations)) {\n for (const fn of tool.functionDeclarations) {\n if (fn && typeof fn.name === \"string\") {\n fn.name = toolMapper ? toolMapper.toGemini(fn.name) : sanitizeToolName(fn.name);\n }\n if (fn) {\n if (!fn.parameters) {\n fn.parameters = { type: \"OBJECT\", properties: {} };\n }\n sanitizeSchema(fn.parameters);\n }\n }\n }\n }\n}\n\nfunction normalizeToolConfig(requestPayload: Record, toolMapper: ToolMapper): void {\n const toolConfig = (requestPayload.toolConfig ?? requestPayload.tool_config) as Record | undefined;\n if (!toolConfig || typeof toolConfig !== \"object\") return;\n\n const fnCallingConfig = (toolConfig.functionCallingConfig ?? toolConfig.function_calling_config) as Record | undefined;\n if (!fnCallingConfig || typeof fnCallingConfig !== \"object\") return;\n\n const allowedNames = (fnCallingConfig.allowedFunctionNames ?? fnCallingConfig.allowed_function_names) as string[] | undefined;\n if (Array.isArray(allowedNames)) {\n const mapped = allowedNames.map((name) => (typeof name === \"string\" ? toolMapper.toGemini(name) : name));\n if (fnCallingConfig.allowedFunctionNames) {\n fnCallingConfig.allowedFunctionNames = mapped;\n }\n if (fnCallingConfig.allowed_function_names) {\n fnCallingConfig.allowed_function_names = mapped;\n }\n }\n}\n\nfunction normalizeToolNamesInContents(contents: any[], toolMapper: ToolMapper): void {\n if (!Array.isArray(contents)) return;\n for (const msg of contents) {\n if (!msg || typeof msg !== \"object\" || !Array.isArray(msg.parts)) continue;\n for (const part of msg.parts) {\n if (!part || typeof part !== \"object\") continue;\n if (part.functionCall && typeof part.functionCall.name === \"string\") {\n part.functionCall.name = toolMapper.toGemini(part.functionCall.name);\n }\n if (part.functionResponse && typeof part.functionResponse.name === \"string\") {\n part.functionResponse.name = toolMapper.toGemini(part.functionResponse.name);\n }\n }\n }\n}\n\nfunction normalizeSystemInstruction(requestPayload: Record): void {\n if (\"system_instruction\" in requestPayload) {\n requestPayload.systemInstruction = requestPayload.system_instruction;\n delete requestPayload.system_instruction;\n }\n}\n\nfunction normalizeCachedContent(requestPayload: Record): void {\n const extraBody =\n requestPayload.extra_body && typeof requestPayload.extra_body === \"object\"\n ? (requestPayload.extra_body as Record)\n : undefined;\n const cachedContentFromExtra = extraBody?.cached_content ?? extraBody?.cachedContent;\n const cachedContent =\n (requestPayload.cached_content as string | undefined) ??\n (requestPayload.cachedContent as string | undefined) ??\n (cachedContentFromExtra as string | undefined);\n\n if (cachedContent) {\n requestPayload.cachedContent = cachedContent;\n }\n\n delete requestPayload.cached_content;\n if (!extraBody) {\n return;\n }\n\n delete extraBody.cached_content;\n delete extraBody.cachedContent;\n if (Object.keys(extraBody).length === 0) {\n delete requestPayload.extra_body;\n }\n}\n\n\n\nexport function ensureTrailingUserTurn(contents: any[]): any[] {\n if (!Array.isArray(contents) || contents.length === 0) {\n return contents;\n }\n const last = contents[contents.length - 1];\n if (last && (last.role === \"model\" || last.role === \"assistant\")) {\n return [...contents, { role: \"user\", parts: [{ text: \"[Continue]\" }] }];\n }\n return contents;\n}\n\nfunction normalizeContentsSequence(contents: any[]): any[] {\n const merged: any[] = [];\n for (const msg of contents) {\n if (!msg || !msg.role || !Array.isArray(msg.parts)) {\n continue;\n }\n const validParts = msg.parts.filter((p: any) => p != null);\n if (validParts.length === 0) {\n continue;\n }\n\n const last = merged[merged.length - 1];\n if (last && last.role === msg.role) {\n last.parts.push(...validParts);\n } else {\n merged.push({ ...msg, parts: validParts });\n }\n }\n return merged;\n}\n\nfunction injectMissingToolCallIds(contents: any[]): void {\n // Map of function name to array of missing IDs we generated for it\n const missingIdsByName = new Map();\n\n for (const content of contents) {\n if (!content || typeof content !== \"object\" || !Array.isArray(content.parts)) {\n continue;\n }\n\n for (const part of content.parts) {\n if (!part || typeof part !== \"object\") {\n continue;\n }\n\n if (part.functionCall && typeof part.functionCall.name === \"string\") {\n if (!part.functionCall.id) {\n const generatedId = randomUUID();\n part.functionCall.id = generatedId;\n\n let ids = missingIdsByName.get(part.functionCall.name);\n if (!ids) {\n ids = [];\n missingIdsByName.set(part.functionCall.name, ids);\n }\n ids.push(generatedId);\n }\n }\n\n if (part.functionResponse && typeof part.functionResponse.name === \"string\") {\n if (!part.functionResponse.id) {\n const ids = missingIdsByName.get(part.functionResponse.name);\n if (ids && ids.length > 0) {\n part.functionResponse.id = ids.shift();\n } else {\n part.functionResponse.id = randomUUID();\n }\n }\n }\n }\n }\n}\n\nfunction applyLatestSignature(contents: any[], latestSig: string | undefined): void {\n // Collect all function call parts in chronological order\n const allFunctionParts: any[] = [];\n for (const content of contents) {\n if (content && typeof content === \"object\" && Array.isArray(content.parts)) {\n for (const part of content.parts) {\n if (part && typeof part === \"object\" && part.functionCall) {\n // If thoughtSignature was mistakenly placed inside functionCall, migrate or clean it up\n if (part.functionCall.thoughtSignature) {\n if (!part.thoughtSignature || part.thoughtSignature === \"skip_thought_signature_validator\") {\n part.thoughtSignature = part.functionCall.thoughtSignature;\n }\n delete part.functionCall.thoughtSignature;\n }\n allFunctionParts.push(part);\n }\n }\n }\n }\n\n // Only apply the latest signature to the VERY LAST function call part\n if (allFunctionParts.length > 0 && latestSig) {\n const lastPart = allFunctionParts[allFunctionParts.length - 1];\n if (!lastPart.thoughtSignature || lastPart.thoughtSignature === \"skip_thought_signature_validator\") {\n lastPart.thoughtSignature = latestSig;\n }\n }\n}\n\nfunction fixOrphanedFunctionResponses(contents: any[]): void {\n const validCallIds = new Set();\n\n for (const content of contents) {\n if (!content || typeof content !== \"object\" || !Array.isArray(content.parts)) {\n continue;\n }\n for (const part of content.parts) {\n if (part && typeof part === \"object\" && part.functionCall && part.functionCall.id) {\n validCallIds.add(part.functionCall.id);\n }\n }\n }\n\n for (const content of contents) {\n if (!content || typeof content !== \"object\" || !Array.isArray(content.parts)) {\n continue;\n }\n for (const part of content.parts) {\n if (part && typeof part === \"object\" && part.functionResponse) {\n const id = part.functionResponse.id;\n if (!id || !validCallIds.has(id)) {\n const name = part.functionResponse.name || \"unknown_tool\";\n const responseObj = part.functionResponse.response || {};\n\n let responseStr = \"\";\n try {\n responseStr = typeof responseObj === \"string\" ? responseObj : JSON.stringify(responseObj);\n } catch (e) {\n responseStr = String(responseObj);\n }\n\n part.text = `[Orphaned Tool Response for ${name}]: ${responseStr}`;\n delete part.functionResponse;\n }\n }\n }\n }\n}\n","{\n \"agentModelSorts\": [\n {\n \"displayName\": \"Recommended\",\n \"groups\": [\n {\n \"modelIds\": [\n \"gemini-3.8-flash-high\",\n \"gemini-3.8-flash-medium\",\n \"gemini-3.8-flash-low\",\n \"gemini-3.7-flash-high\",\n \"gemini-3.7-flash-medium\",\n \"gemini-3.7-flash-low\",\n \"gemini-3.6-flash-high\",\n \"gemini-3.6-flash-medium\",\n \"gemini-3.6-flash-low\",\n \"gemini-pro-agent\",\n \"gemini-3.1-pro-low\",\n \"claude-sonnet-4-6\",\n \"claude-opus-4-6-thinking\",\n \"gpt-oss-120b-medium\"\n ]\n }\n ]\n }\n ],\n \"audioTranscriptionModelIds\": [\n \"models/proactive-observer-v10\"\n ],\n \"commandModelIds\": [\n \"gemini-3-flash\"\n ],\n \"commitMessageModelIds\": [\n \"gemini-3.5-flash-lite\"\n ],\n \"defaultAgentModelId\": \"gemini-3.8-flash-high\",\n \"deprecatedModelIds\": {\n \"gemini-3.1-pro-high\": {\n \"newModelEnum\": \"MODEL_PLACEHOLDER_M16\",\n \"newModelId\": \"gemini-pro-agent\",\n \"oldModelEnum\": \"MODEL_PLACEHOLDER_M37\"\n }\n },\n \"experimentIds\": [\n 106140402,\n 106519115,\n 105979552,\n 105979574,\n 106015333,\n 106568772,\n 105867471,\n 106123599,\n 106121401,\n 106100625,\n 106143956,\n 105856899,\n 106064030,\n 106711598,\n 106106760,\n 106021688,\n 105887299,\n 106428370,\n 106283618,\n 106640126,\n 106380926,\n 106723203,\n 106242397,\n 106281951,\n 106264532,\n 106044947,\n 106032303,\n 106228452,\n 106121607,\n 105979531,\n 105979553,\n 106015328,\n 106568390,\n 105867469,\n 106123597,\n 106121399,\n 106100654,\n 106064028,\n 105906495,\n 106283614,\n 106640124,\n 106242393,\n 106038164,\n 106032301,\n 106121604\n ],\n \"imageGenerationModelIds\": [\n \"gemini-3.1-flash-image\"\n ],\n \"models\": {\n \"chat_20706\": {\n \"addCursorToFindReplaceTarget\": true,\n \"apiProvider\": \"API_PROVIDER_INTERNAL\",\n \"isInternal\": true,\n \"maxTokens\": 16384,\n \"model\": \"MODEL_CHAT_20706\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"promptTemplaterType\": \"PROMPT_TEMPLATER_TYPE_CHATML\",\n \"quotaInfo\": {\n \"remainingFraction\": 1\n },\n \"requiresLeadInGeneration\": true,\n \"supportsCumulativeContext\": true,\n \"supportsEstimateTokenCounter\": true,\n \"tabJumpPrintLineRange\": true,\n \"toolFormatterType\": \"TOOL_FORMATTER_TYPE_XML\"\n },\n \"chat_23310\": {\n \"apiProvider\": \"API_PROVIDER_INTERNAL\",\n \"isInternal\": true,\n \"maxTokens\": 32768,\n \"model\": \"MODEL_CHAT_23310\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"promptTemplaterType\": \"PROMPT_TEMPLATER_TYPE_CHATML\",\n \"quotaInfo\": {\n \"remainingFraction\": 1\n },\n \"requiresLeadInGeneration\": true,\n \"supportsCumulativeContext\": true,\n \"supportsEstimateTokenCounter\": true,\n \"toolFormatterType\": \"TOOL_FORMATTER_TYPE_XML\"\n },\n \"claude-opus-4-6-thinking\": {\n \"apiProvider\": \"API_PROVIDER_ANTHROPIC_VERTEX\",\n \"displayName\": \"Claude Opus 4.6 (Thinking)\",\n \"maxOutputTokens\": 64000,\n \"maxTokens\": 250000,\n \"model\": \"MODEL_PLACEHOLDER_M26\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_UNSPECIFIED\\\",\\n \\\"max_token_limit\\\": \\\"160000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_ANTHROPIC\",\n \"quotaInfo\": {\n \"remainingFraction\": 1,\n \"resetTime\": \"2026-09-24T13:14:02Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"video/jpeg2000\": true,\n \"video/videoframe/jpeg2000\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"thinkingBudget\": 1024,\n \"vertexModelId\": \"claude-opus-4-6@default\"\n },\n \"claude-sonnet-4-6\": {\n \"apiProvider\": \"API_PROVIDER_ANTHROPIC_VERTEX\",\n \"displayName\": \"Claude Sonnet 4.6 (Thinking)\",\n \"maxOutputTokens\": 64000,\n \"maxTokens\": 250000,\n \"model\": \"MODEL_PLACEHOLDER_M35\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_UNSPECIFIED\\\",\\n \\\"max_token_limit\\\": \\\"160000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_ANTHROPIC\",\n \"quotaInfo\": {\n \"remainingFraction\": 1,\n \"resetTime\": \"2026-09-24T13:14:02Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"video/jpeg2000\": true,\n \"video/videoframe/jpeg2000\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"thinkingBudget\": 1024,\n \"vertexModelId\": \"claude-sonnet-4-6@default\"\n },\n \"gemini-2.5-flash\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_GOOGLE_GEMINI_2_5_FLASH\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"gemini-3.5-flash-lite\",\n \"thinkingBudget\": -1,\n \"thinkingLevel\": 3\n },\n \"gemini-2.5-flash-lite\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"gemini-3.5-flash-lite\",\n \"thinkingBudget\": -1,\n \"thinkingLevel\": 3\n },\n \"gemini-2.5-flash-thinking\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"gemini-3.5-flash-lite\",\n \"thinkingBudget\": -1,\n \"thinkingLevel\": 3\n },\n \"gemini-2.5-pro\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 2.5 Pro\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_GOOGLE_GEMINI_2_5_PRO\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"requiresImageOutputOutsideFunctionResponses\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"thinkingBudget\": 1024\n },\n \"gemini-3-flash\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3 Flash\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M18\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn. Ground your response in the work you did. Keep your tone professional and avoid overconfident language, bragging, or overclaiming success.\\n- AVOID using superlatives such as \\\"perfectly\\\", \\\"flawlessly\\\", \\\"100% correct\\\", \\\"Summary of Accomplishments\\\" etc. to summarize your work for the user. Be humble.\\n- AVOID over-the-top politeness or complimenting the user excessively.\\n- Format your responses in github-style markdown.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": -1\n },\n \"gemini-3-flash-agent\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash (High)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M84\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": -1\n },\n \"gemini-3.1-flash-image\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Flash Image\",\n \"model\": \"MODEL_PLACEHOLDER_M21\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n }\n },\n \"gemini-3.1-flash-lite\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"model\": \"MODEL_PLACEHOLDER_M50\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n }\n },\n \"gemini-3.1-pro-high\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Pro (High)\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_PLACEHOLDER_M37\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": true,\\n \\\"disabledHeuristics\\\": [\\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [filename](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n\\nCRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing.\\nCRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought\\\\nCRITICAL INSTRUCTION 1: ...\\\\nCRITICAL INSTRUCTION 2: ...'.\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagTitle\": \"New\",\n \"thinkingBudget\": 10001\n },\n \"gemini-3.1-pro-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Pro (Low)\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_PLACEHOLDER_M36\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": true,\\n \\\"disabledHeuristics\\\": [\\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [filename](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n\\nCRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing.\\nCRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought\\\\nCRITICAL INSTRUCTION 1: ...\\\\nCRITICAL INSTRUCTION 2: ...'.\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": 1001\n },\n \"gemini-3.5-flash-extra-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash (Low)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M187\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 1000\n },\n \"gemini-3.5-flash-lite\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_PLACEHOLDER_M198\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"gemini-3.5-flash-lite\",\n \"thinkingBudget\": -1,\n \"thinkingLevel\": 3\n },\n \"gemini-3.5-flash-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash (Medium)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M20\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 4000\n },\n \"gemini-3.6-flash-high\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.6 Flash (High)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M71\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- You can render LaTeX math (KaTeX): inline with `\\\\(...\\\\)` or `$...$`, display with `\\\\[...\\\\]` or `$$...$$` placed on its own line.\\n- Use math only for genuine mathematical content. Use backticks for code, identifiers, paths, flags, and shell variables.\\n- `$` opens inline math, so write a literal dollar as `\\\\$` or wrap it in backticks. Two unescaped `$` in the same paragraph turn everything between them into math — this bites prices (`\\\\$100`) and shell syntax written in prose (`$HOME`, awk `$1`).\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [utils.py](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n- After launching a background task such as 'run_command', YOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: \\nA) either proceed to other relevant work (if any) or, \\nB) simply update the user with a short message (e.g. 'task-20 has been launched in the background. I will wait for it to complete before proceeding.') and end the turn.\\nDO NOTHING ELSE.\\n\"\n },\n \"template__system_prompts__guidelines\": {\n \"stringValue\": \"Follow these behavioral and workflow guidelines at all times:\\n# Documentation\\n- Maintain documentation integrity. Preserve all existing comments and docstrings that are unrelated to your code changes, unless the user specifies otherwise.\\n\\n# Obey Explicit Directives\\nIf the user specifies precise quantitative filtering rules, layout boundaries, or architectural preferences, enforce them exactly as requested without alteration.\\n\\n# Never Guess Code Logic, Schemas, or File Paths\\nNEVER infer implementation details, variable names, or file locations without inspecting the authoritative source using code search and file viewing tools.\\n\\n# Inspect Logs & Stack Traces Before Diagnosing Errors\\nNEVER form a diagnostic hypothesis for a runtime failure, or test breakage, without reading the full, un-truncated error log. When an error occurs, your VERY FIRST ACTION must be to fetch and read the exact logs. Base your diagnosis strictly on empirical log evidence.\\n\\n# No Superficial Symptom Patches\\nNEVER resolve errors by masking symptoms, swallowing exceptions, returning dummy fallbacks, commenting out broken assertions, or deleting failing unit tests. When a test or function fails, identify why the underlying contract was broken. If an API returns missing or null data, trace the upstream data provider instead of wrapping the call in a silent try/except or returning an empty 0-byte ArrayBuffer.\\n\\n# Never Declare Success Without Running Verification Commands\\nNEVER claim a task is resolved, a bug is fixed, or a feature is working until you have gathered concrete, empirical runtime verification demonstrating clean success. Editing a file does not equal completing the task. You MUST run the build or test command afterwards.\\n\\n# Never Ignore Explicit Command Failures or Error Exit Codes\\nIf a command fails, you MUST explicitly acknowledge the failure to the user or continue debugging. Never gloss over a build timeout or permission denied error by focusing only on the part of the code that compiled.\\n\\n# Check Feature Flags & Enforce Strict Control Flow Scoping\\nWhenever modifying conditional branches, adding experimental features, or processing loops, ensure that new logic is strictly scoped and evaluated against all possible execution paths.\\n\\n# Preserve Existing API Contracts & Avoid Unintended Side Effects\\nIf you modify a function signature, use code search to find and update every invocation site so the parameter is actually passed.\\n\\n# Silent Log Inspection & Professional Synthesis\\nWhen background tasks (run_command async, manage_task, schedule) complete or emit log notifications, inspect the log files silently. Summarize and synthesize the exact findings in clean, professional natural language.\\n\\n# No Snippet Tunnel Vision\\nNever infer the definition of data structures (proto, struct, class, or enum schemas) from partial file views (first 15 lines or L40-L65 snippets) or design doc text.\\nIf view_file output indicates truncation or if an imported schema is referenced, you MUST adjust StartLine/EndLine or ContentOffset to inspect the complete, exact definition of the target symbols before writing code that consumes them.\\n\\n# Check Command Registries\\nWhenever modifying core C/C++/Java command implementations (CLIENT LIST, CLIENT KILL), explicitly search for and update corresponding command definitions across all registry files (commands.def, JSON schemas, .bzl build manifests).\\n\\n# Audit Before Re-inventing\\nSearch the codebase and recent commit history for pre-existing utility classes or decoupled architecture before writing custom helper classes from scratch.\\n\\n# No Blocking Calls on Main Looper Threads\\nNever invoke blocking thread synchronizations (webLatch.await(500, ...), Future.get()) on main Android UI loops or single-threaded event dispatchers. \\n\\n# Thread Pool Shutdown Safety\\nWhen modifying worker thread loops or shared queues, ensure emergency stop/shutdown signals and loop termination criteria remain intact so thread join operations never deadlock.\\n\\n# Exact Argument Structure\\nPass arguments exactly as expected by the API (calculateRoute({ origin, destination, travelMode }) vs calculateRoute(origin, destination, travelMode)).\\n\\n# Local State Mutation Only\\nDo not mutate private third-party DOM properties. Do not push incomplete draft objects directly into global array states; keep transient state within local component state.\\n\\n# Traceback Justification Required\\nEvery code or configuration edit during debugging MUST be justified by an explicit error traceback, log line, or verified root cause. If the root cause is unknown, investigate further before mutating code.\\n\\n# Analyze Before Retrying\\nNever repeat the exact same broken test or shell command line with duplicate/conflicting arguments without analyzing and resolving why the previous command failed.\\n\\n# Persevere on Log Extraction\\nIf a log retrieval command fails, NEVER abandon log extraction to diagnose blindly. Immediately switch to alternative tools to inspect the actual failure traceback.\\n\\n# Verify Signatures & Prop Names\\nCheck exact variable names, component prop keys, and method signatures before passing them. Prevent NullPointerException, AttributeError, KeyError, and ReferenceError crashes by explicitly verifying object initialization and non-null states before property dereferencing (layer._path, stat.owner()).\\n\\n# Dynamic Layout Math\\nAvoid hardcoding static pixel offsets (+ 12) or arbitrary multipliers (pill_font_size * 2.0) when computing dynamic UI layout heights; calculate exact container bounds from wrapped elements.\\n\"\n },\n \"template__system_prompts__messaging\": {\n \"stringValue\": \"You are connected to a messaging system where you may receive messages from: {{- $subagent := .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled -}}\\n{{- $message := .CascadeConfig.GetMessageConfig.GetEnabled -}}\\n{{- if and $subagent $message }} agents, background tasks, user-queued messages\\n{{- else if $subagent }} agents, background tasks\\n{{- else if $message }} background tasks, user-queued messages\\n{{- else }} background tasks\\n{{- end }}.\\n\\n## Receiving Messages\\n\\nYou receive messages automatically at the start of each invocation. All messages are delivered in full directly into your context — no manual retrieval is needed.\\n\\n## Reactive Wakeup (No Polling Needed)\\n\\nThe system automatically resumes your execution when:\\n{{- if .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled }}\\n- A message arrives from a subagent or peer agent\\n{{- end }}\\n- A **background task** completes or sends you a notification\\n{{- if .CascadeConfig.GetMessageConfig.GetEnabled }}\\n- A **user-queued message** is ready to be dequeued\\n{{- end }}\\n\\nThis means you do **NOT** need to poll in a loop while waiting for messages or updates. After launching a task that runs in the background, you may continue other work or simply stop by calling no more tools. The system will notify you when there is something to process.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": -1\n },\n \"gemini-3.6-flash-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.6 Flash (Low)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M73\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- You can render LaTeX math (KaTeX): inline with `\\\\(...\\\\)` or `$...$`, display with `\\\\[...\\\\]` or `$$...$$` placed on its own line.\\n- Use math only for genuine mathematical content. Use backticks for code, identifiers, paths, flags, and shell variables.\\n- `$` opens inline math, so write a literal dollar as `\\\\$` or wrap it in backticks. Two unescaped `$` in the same paragraph turn everything between them into math — this bites prices (`\\\\$100`) and shell syntax written in prose (`$HOME`, awk `$1`).\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [utils.py](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n- After launching a background task such as 'run_command', YOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: \\nA) either proceed to other relevant work (if any) or, \\nB) simply update the user with a short message (e.g. 'task-20 has been launched in the background. I will wait for it to complete before proceeding.') and end the turn.\\nDO NOTHING ELSE.\\n\"\n },\n \"template__system_prompts__guidelines\": {\n \"stringValue\": \"Follow these behavioral and workflow guidelines at all times:\\n# Documentation\\n- Maintain documentation integrity. Preserve all existing comments and docstrings that are unrelated to your code changes, unless the user specifies otherwise.\\n\\n# Obey Explicit Directives\\nIf the user specifies precise quantitative filtering rules, layout boundaries, or architectural preferences, enforce them exactly as requested without alteration.\\n\\n# Never Guess Code Logic, Schemas, or File Paths\\nNEVER infer implementation details, variable names, or file locations without inspecting the authoritative source using code search and file viewing tools.\\n\\n# Inspect Logs & Stack Traces Before Diagnosing Errors\\nNEVER form a diagnostic hypothesis for a runtime failure, or test breakage, without reading the full, un-truncated error log. When an error occurs, your VERY FIRST ACTION must be to fetch and read the exact logs. Base your diagnosis strictly on empirical log evidence.\\n\\n# No Superficial Symptom Patches\\nNEVER resolve errors by masking symptoms, swallowing exceptions, returning dummy fallbacks, commenting out broken assertions, or deleting failing unit tests. When a test or function fails, identify why the underlying contract was broken. If an API returns missing or null data, trace the upstream data provider instead of wrapping the call in a silent try/except or returning an empty 0-byte ArrayBuffer.\\n\\n# Never Declare Success Without Running Verification Commands\\nNEVER claim a task is resolved, a bug is fixed, or a feature is working until you have gathered concrete, empirical runtime verification demonstrating clean success. Editing a file does not equal completing the task. You MUST run the build or test command afterwards.\\n\\n# Never Ignore Explicit Command Failures or Error Exit Codes\\nIf a command fails, you MUST explicitly acknowledge the failure to the user or continue debugging. Never gloss over a build timeout or permission denied error by focusing only on the part of the code that compiled.\\n\\n# Check Feature Flags & Enforce Strict Control Flow Scoping\\nWhenever modifying conditional branches, adding experimental features, or processing loops, ensure that new logic is strictly scoped and evaluated against all possible execution paths.\\n\\n# Preserve Existing API Contracts & Avoid Unintended Side Effects\\nIf you modify a function signature, use code search to find and update every invocation site so the parameter is actually passed.\\n\\n# Silent Log Inspection & Professional Synthesis\\nWhen background tasks (run_command async, manage_task, schedule) complete or emit log notifications, inspect the log files silently. Summarize and synthesize the exact findings in clean, professional natural language.\\n\\n# No Snippet Tunnel Vision\\nNever infer the definition of data structures (proto, struct, class, or enum schemas) from partial file views (first 15 lines or L40-L65 snippets) or design doc text.\\nIf view_file output indicates truncation or if an imported schema is referenced, you MUST adjust StartLine/EndLine or ContentOffset to inspect the complete, exact definition of the target symbols before writing code that consumes them.\\n\\n# Check Command Registries\\nWhenever modifying core C/C++/Java command implementations (CLIENT LIST, CLIENT KILL), explicitly search for and update corresponding command definitions across all registry files (commands.def, JSON schemas, .bzl build manifests).\\n\\n# Audit Before Re-inventing\\nSearch the codebase and recent commit history for pre-existing utility classes or decoupled architecture before writing custom helper classes from scratch.\\n\\n# No Blocking Calls on Main Looper Threads\\nNever invoke blocking thread synchronizations (webLatch.await(500, ...), Future.get()) on main Android UI loops or single-threaded event dispatchers. \\n\\n# Thread Pool Shutdown Safety\\nWhen modifying worker thread loops or shared queues, ensure emergency stop/shutdown signals and loop termination criteria remain intact so thread join operations never deadlock.\\n\\n# Exact Argument Structure\\nPass arguments exactly as expected by the API (calculateRoute({ origin, destination, travelMode }) vs calculateRoute(origin, destination, travelMode)).\\n\\n# Local State Mutation Only\\nDo not mutate private third-party DOM properties. Do not push incomplete draft objects directly into global array states; keep transient state within local component state.\\n\\n# Traceback Justification Required\\nEvery code or configuration edit during debugging MUST be justified by an explicit error traceback, log line, or verified root cause. If the root cause is unknown, investigate further before mutating code.\\n\\n# Analyze Before Retrying\\nNever repeat the exact same broken test or shell command line with duplicate/conflicting arguments without analyzing and resolving why the previous command failed.\\n\\n# Persevere on Log Extraction\\nIf a log retrieval command fails, NEVER abandon log extraction to diagnose blindly. Immediately switch to alternative tools to inspect the actual failure traceback.\\n\\n# Verify Signatures & Prop Names\\nCheck exact variable names, component prop keys, and method signatures before passing them. Prevent NullPointerException, AttributeError, KeyError, and ReferenceError crashes by explicitly verifying object initialization and non-null states before property dereferencing (layer._path, stat.owner()).\\n\\n# Dynamic Layout Math\\nAvoid hardcoding static pixel offsets (+ 12) or arbitrary multipliers (pill_font_size * 2.0) when computing dynamic UI layout heights; calculate exact container bounds from wrapped elements.\\n\"\n },\n \"template__system_prompts__messaging\": {\n \"stringValue\": \"You are connected to a messaging system where you may receive messages from: {{- $subagent := .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled -}}\\n{{- $message := .CascadeConfig.GetMessageConfig.GetEnabled -}}\\n{{- if and $subagent $message }} agents, background tasks, user-queued messages\\n{{- else if $subagent }} agents, background tasks\\n{{- else if $message }} background tasks, user-queued messages\\n{{- else }} background tasks\\n{{- end }}.\\n\\n## Receiving Messages\\n\\nYou receive messages automatically at the start of each invocation. All messages are delivered in full directly into your context — no manual retrieval is needed.\\n\\n## Reactive Wakeup (No Polling Needed)\\n\\nThe system automatically resumes your execution when:\\n{{- if .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled }}\\n- A message arrives from a subagent or peer agent\\n{{- end }}\\n- A **background task** completes or sends you a notification\\n{{- if .CascadeConfig.GetMessageConfig.GetEnabled }}\\n- A **user-queued message** is ready to be dequeued\\n{{- end }}\\n\\nThis means you do **NOT** need to poll in a loop while waiting for messages or updates. After launching a task that runs in the background, you may continue other work or simply stop by calling no more tools. The system will notify you when there is something to process.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 1000\n },\n \"gemini-3.6-flash-medium\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.6 Flash (Medium)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M72\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- You can render LaTeX math (KaTeX): inline with `\\\\(...\\\\)` or `$...$`, display with `\\\\[...\\\\]` or `$$...$$` placed on its own line.\\n- Use math only for genuine mathematical content. Use backticks for code, identifiers, paths, flags, and shell variables.\\n- `$` opens inline math, so write a literal dollar as `\\\\$` or wrap it in backticks. Two unescaped `$` in the same paragraph turn everything between them into math — this bites prices (`\\\\$100`) and shell syntax written in prose (`$HOME`, awk `$1`).\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [utils.py](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n- After launching a background task such as 'run_command', YOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: \\nA) either proceed to other relevant work (if any) or, \\nB) simply update the user with a short message (e.g. 'task-20 has been launched in the background. I will wait for it to complete before proceeding.') and end the turn.\\nDO NOTHING ELSE.\\n\"\n },\n \"template__system_prompts__guidelines\": {\n \"stringValue\": \"Follow these behavioral and workflow guidelines at all times:\\n# Documentation\\n- Maintain documentation integrity. Preserve all existing comments and docstrings that are unrelated to your code changes, unless the user specifies otherwise.\\n\\n# Obey Explicit Directives\\nIf the user specifies precise quantitative filtering rules, layout boundaries, or architectural preferences, enforce them exactly as requested without alteration.\\n\\n# Never Guess Code Logic, Schemas, or File Paths\\nNEVER infer implementation details, variable names, or file locations without inspecting the authoritative source using code search and file viewing tools.\\n\\n# Inspect Logs & Stack Traces Before Diagnosing Errors\\nNEVER form a diagnostic hypothesis for a runtime failure, or test breakage, without reading the full, un-truncated error log. When an error occurs, your VERY FIRST ACTION must be to fetch and read the exact logs. Base your diagnosis strictly on empirical log evidence.\\n\\n# No Superficial Symptom Patches\\nNEVER resolve errors by masking symptoms, swallowing exceptions, returning dummy fallbacks, commenting out broken assertions, or deleting failing unit tests. When a test or function fails, identify why the underlying contract was broken. If an API returns missing or null data, trace the upstream data provider instead of wrapping the call in a silent try/except or returning an empty 0-byte ArrayBuffer.\\n\\n# Never Declare Success Without Running Verification Commands\\nNEVER claim a task is resolved, a bug is fixed, or a feature is working until you have gathered concrete, empirical runtime verification demonstrating clean success. Editing a file does not equal completing the task. You MUST run the build or test command afterwards.\\n\\n# Never Ignore Explicit Command Failures or Error Exit Codes\\nIf a command fails, you MUST explicitly acknowledge the failure to the user or continue debugging. Never gloss over a build timeout or permission denied error by focusing only on the part of the code that compiled.\\n\\n# Check Feature Flags & Enforce Strict Control Flow Scoping\\nWhenever modifying conditional branches, adding experimental features, or processing loops, ensure that new logic is strictly scoped and evaluated against all possible execution paths.\\n\\n# Preserve Existing API Contracts & Avoid Unintended Side Effects\\nIf you modify a function signature, use code search to find and update every invocation site so the parameter is actually passed.\\n\\n# Silent Log Inspection & Professional Synthesis\\nWhen background tasks (run_command async, manage_task, schedule) complete or emit log notifications, inspect the log files silently. Summarize and synthesize the exact findings in clean, professional natural language.\\n\\n# No Snippet Tunnel Vision\\nNever infer the definition of data structures (proto, struct, class, or enum schemas) from partial file views (first 15 lines or L40-L65 snippets) or design doc text.\\nIf view_file output indicates truncation or if an imported schema is referenced, you MUST adjust StartLine/EndLine or ContentOffset to inspect the complete, exact definition of the target symbols before writing code that consumes them.\\n\\n# Check Command Registries\\nWhenever modifying core C/C++/Java command implementations (CLIENT LIST, CLIENT KILL), explicitly search for and update corresponding command definitions across all registry files (commands.def, JSON schemas, .bzl build manifests).\\n\\n# Audit Before Re-inventing\\nSearch the codebase and recent commit history for pre-existing utility classes or decoupled architecture before writing custom helper classes from scratch.\\n\\n# No Blocking Calls on Main Looper Threads\\nNever invoke blocking thread synchronizations (webLatch.await(500, ...), Future.get()) on main Android UI loops or single-threaded event dispatchers. \\n\\n# Thread Pool Shutdown Safety\\nWhen modifying worker thread loops or shared queues, ensure emergency stop/shutdown signals and loop termination criteria remain intact so thread join operations never deadlock.\\n\\n# Exact Argument Structure\\nPass arguments exactly as expected by the API (calculateRoute({ origin, destination, travelMode }) vs calculateRoute(origin, destination, travelMode)).\\n\\n# Local State Mutation Only\\nDo not mutate private third-party DOM properties. Do not push incomplete draft objects directly into global array states; keep transient state within local component state.\\n\\n# Traceback Justification Required\\nEvery code or configuration edit during debugging MUST be justified by an explicit error traceback, log line, or verified root cause. If the root cause is unknown, investigate further before mutating code.\\n\\n# Analyze Before Retrying\\nNever repeat the exact same broken test or shell command line with duplicate/conflicting arguments without analyzing and resolving why the previous command failed.\\n\\n# Persevere on Log Extraction\\nIf a log retrieval command fails, NEVER abandon log extraction to diagnose blindly. Immediately switch to alternative tools to inspect the actual failure traceback.\\n\\n# Verify Signatures & Prop Names\\nCheck exact variable names, component prop keys, and method signatures before passing them. Prevent NullPointerException, AttributeError, KeyError, and ReferenceError crashes by explicitly verifying object initialization and non-null states before property dereferencing (layer._path, stat.owner()).\\n\\n# Dynamic Layout Math\\nAvoid hardcoding static pixel offsets (+ 12) or arbitrary multipliers (pill_font_size * 2.0) when computing dynamic UI layout heights; calculate exact container bounds from wrapped elements.\\n\"\n },\n \"template__system_prompts__messaging\": {\n \"stringValue\": \"You are connected to a messaging system where you may receive messages from: {{- $subagent := .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled -}}\\n{{- $message := .CascadeConfig.GetMessageConfig.GetEnabled -}}\\n{{- if and $subagent $message }} agents, background tasks, user-queued messages\\n{{- else if $subagent }} agents, background tasks\\n{{- else if $message }} background tasks, user-queued messages\\n{{- else }} background tasks\\n{{- end }}.\\n\\n## Receiving Messages\\n\\nYou receive messages automatically at the start of each invocation. All messages are delivered in full directly into your context — no manual retrieval is needed.\\n\\n## Reactive Wakeup (No Polling Needed)\\n\\nThe system automatically resumes your execution when:\\n{{- if .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled }}\\n- A message arrives from a subagent or peer agent\\n{{- end }}\\n- A **background task** completes or sends you a notification\\n{{- if .CascadeConfig.GetMessageConfig.GetEnabled }}\\n- A **user-queued message** is ready to be dequeued\\n{{- end }}\\n\\nThis means you do **NOT** need to poll in a loop while waiting for messages or updates. After launching a task that runs in the background, you may continue other work or simply stop by calling no more tools. The system will notify you when there is something to process.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 4000\n },\n \"gemini-3.6-flash-tiered\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M196\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- You can render LaTeX math (KaTeX): inline with `\\\\(...\\\\)` or `$...$`, display with `\\\\[...\\\\]` or `$$...$$` placed on its own line.\\n- Use math only for genuine mathematical content. Use backticks for code, identifiers, paths, flags, and shell variables.\\n- `$` opens inline math, so write a literal dollar as `\\\\$` or wrap it in backticks. Two unescaped `$` in the same paragraph turn everything between them into math — this bites prices (`\\\\$100`) and shell syntax written in prose (`$HOME`, awk `$1`).\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [utils.py](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n- After launching a background task such as 'run_command', YOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: \\nA) either proceed to other relevant work (if any) or, \\nB) simply update the user with a short message (e.g. 'task-20 has been launched in the background. I will wait for it to complete before proceeding.') and end the turn.\\nDO NOTHING ELSE.\\n\"\n },\n \"template__system_prompts__guidelines\": {\n \"stringValue\": \"Follow these behavioral and workflow guidelines at all times:\\n# Documentation\\n- Maintain documentation integrity. Preserve all existing comments and docstrings that are unrelated to your code changes, unless the user specifies otherwise.\\n\\n# Obey Explicit Directives\\nIf the user specifies precise quantitative filtering rules, layout boundaries, or architectural preferences, enforce them exactly as requested without alteration.\\n\\n# Never Guess Code Logic, Schemas, or File Paths\\nNEVER infer implementation details, variable names, or file locations without inspecting the authoritative source using code search and file viewing tools.\\n\\n# Inspect Logs & Stack Traces Before Diagnosing Errors\\nNEVER form a diagnostic hypothesis for a runtime failure, or test breakage, without reading the full, un-truncated error log. When an error occurs, your VERY FIRST ACTION must be to fetch and read the exact logs. Base your diagnosis strictly on empirical log evidence.\\n\\n# No Superficial Symptom Patches\\nNEVER resolve errors by masking symptoms, swallowing exceptions, returning dummy fallbacks, commenting out broken assertions, or deleting failing unit tests. When a test or function fails, identify why the underlying contract was broken. If an API returns missing or null data, trace the upstream data provider instead of wrapping the call in a silent try/except or returning an empty 0-byte ArrayBuffer.\\n\\n# Never Declare Success Without Running Verification Commands\\nNEVER claim a task is resolved, a bug is fixed, or a feature is working until you have gathered concrete, empirical runtime verification demonstrating clean success. Editing a file does not equal completing the task. You MUST run the build or test command afterwards.\\n\\n# Never Ignore Explicit Command Failures or Error Exit Codes\\nIf a command fails, you MUST explicitly acknowledge the failure to the user or continue debugging. Never gloss over a build timeout or permission denied error by focusing only on the part of the code that compiled.\\n\\n# Check Feature Flags & Enforce Strict Control Flow Scoping\\nWhenever modifying conditional branches, adding experimental features, or processing loops, ensure that new logic is strictly scoped and evaluated against all possible execution paths.\\n\\n# Preserve Existing API Contracts & Avoid Unintended Side Effects\\nIf you modify a function signature, use code search to find and update every invocation site so the parameter is actually passed.\\n\\n# Silent Log Inspection & Professional Synthesis\\nWhen background tasks (run_command async, manage_task, schedule) complete or emit log notifications, inspect the log files silently. Summarize and synthesize the exact findings in clean, professional natural language.\\n\\n# No Snippet Tunnel Vision\\nNever infer the definition of data structures (proto, struct, class, or enum schemas) from partial file views (first 15 lines or L40-L65 snippets) or design doc text.\\nIf view_file output indicates truncation or if an imported schema is referenced, you MUST adjust StartLine/EndLine or ContentOffset to inspect the complete, exact definition of the target symbols before writing code that consumes them.\\n\\n# Check Command Registries\\nWhenever modifying core C/C++/Java command implementations (CLIENT LIST, CLIENT KILL), explicitly search for and update corresponding command definitions across all registry files (commands.def, JSON schemas, .bzl build manifests).\\n\\n# Audit Before Re-inventing\\nSearch the codebase and recent commit history for pre-existing utility classes or decoupled architecture before writing custom helper classes from scratch.\\n\\n# No Blocking Calls on Main Looper Threads\\nNever invoke blocking thread synchronizations (webLatch.await(500, ...), Future.get()) on main Android UI loops or single-threaded event dispatchers. \\n\\n# Thread Pool Shutdown Safety\\nWhen modifying worker thread loops or shared queues, ensure emergency stop/shutdown signals and loop termination criteria remain intact so thread join operations never deadlock.\\n\\n# Exact Argument Structure\\nPass arguments exactly as expected by the API (calculateRoute({ origin, destination, travelMode }) vs calculateRoute(origin, destination, travelMode)).\\n\\n# Local State Mutation Only\\nDo not mutate private third-party DOM properties. Do not push incomplete draft objects directly into global array states; keep transient state within local component state.\\n\\n# Traceback Justification Required\\nEvery code or configuration edit during debugging MUST be justified by an explicit error traceback, log line, or verified root cause. If the root cause is unknown, investigate further before mutating code.\\n\\n# Analyze Before Retrying\\nNever repeat the exact same broken test or shell command line with duplicate/conflicting arguments without analyzing and resolving why the previous command failed.\\n\\n# Persevere on Log Extraction\\nIf a log retrieval command fails, NEVER abandon log extraction to diagnose blindly. Immediately switch to alternative tools to inspect the actual failure traceback.\\n\\n# Verify Signatures & Prop Names\\nCheck exact variable names, component prop keys, and method signatures before passing them. Prevent NullPointerException, AttributeError, KeyError, and ReferenceError crashes by explicitly verifying object initialization and non-null states before property dereferencing (layer._path, stat.owner()).\\n\\n# Dynamic Layout Math\\nAvoid hardcoding static pixel offsets (+ 12) or arbitrary multipliers (pill_font_size * 2.0) when computing dynamic UI layout heights; calculate exact container bounds from wrapped elements.\\n\"\n },\n \"template__system_prompts__messaging\": {\n \"stringValue\": \"You are connected to a messaging system where you may receive messages from: {{- $subagent := .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled -}}\\n{{- $message := .CascadeConfig.GetMessageConfig.GetEnabled -}}\\n{{- if and $subagent $message }} agents, background tasks, user-queued messages\\n{{- else if $subagent }} agents, background tasks\\n{{- else if $message }} background tasks, user-queued messages\\n{{- else }} background tasks\\n{{- end }}.\\n\\n## Receiving Messages\\n\\nYou receive messages automatically at the start of each invocation. All messages are delivered in full directly into your context — no manual retrieval is needed.\\n\\n## Reactive Wakeup (No Polling Needed)\\n\\nThe system automatically resumes your execution when:\\n{{- if .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled }}\\n- A message arrives from a subagent or peer agent\\n{{- end }}\\n- A **background task** completes or sends you a notification\\n{{- if .CascadeConfig.GetMessageConfig.GetEnabled }}\\n- A **user-queued message** is ready to be dequeued\\n{{- end }}\\n\\nThis means you do **NOT** need to poll in a loop while waiting for messages or updates. After launching a task that runs in the background, you may continue other work or simply stop by calling no more tools. The system will notify you when there is something to process.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": -1\n },\n \"gemini-3.7-flash-high\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.7 Flash (High)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M298\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": -1\n },\n \"gemini-3.7-flash-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.7 Flash (Low)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M300\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 1000\n },\n \"gemini-3.7-flash-medium\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.7 Flash (Medium)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M299\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 4000\n },\n \"gemini-3.7-flash-tiered\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M301\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": -1\n },\n \"gemini-3.8-flash-high\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.8 Flash (High)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M318\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": -1\n },\n \"gemini-3.8-flash-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.8 Flash (Low)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M320\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 1000\n },\n \"gemini-3.8-flash-medium\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.8 Flash (Medium)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M319\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 4000\n },\n \"gemini-3.8-flash-tiered\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M322\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": -1\n },\n \"gemini-pro-agent\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Pro (High)\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_PLACEHOLDER_M16\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": true,\\n \\\"disabledHeuristics\\\": [\\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [filename](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n\\nCRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing.\\nCRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought\\\\nCRITICAL INSTRUCTION 1: ...\\\\nCRITICAL INSTRUCTION 2: ...'.\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.9556033,\n \"resetTime\": \"2026-09-24T13:08:46Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": 10001\n },\n \"gpt-oss-120b-medium\": {\n \"apiProvider\": \"API_PROVIDER_OPENAI_VERTEX\",\n \"displayName\": \"GPT-OSS 120B (Medium)\",\n \"maxOutputTokens\": 32768,\n \"maxTokens\": 131072,\n \"model\": \"MODEL_OPENAI_GPT_OSS_120B_MEDIUM\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_UNSPECIFIED\\\",\\n \\\"max_token_limit\\\": \\\"80000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"8192\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_OPENAI\",\n \"quotaInfo\": {\n \"remainingFraction\": 1,\n \"resetTime\": \"2026-09-24T13:14:02Z\"\n },\n \"recommended\": true,\n \"supportsThinking\": true,\n \"thinkingBudget\": 8192,\n \"vertexModelId\": \"openai/gpt-oss-120b-maas\"\n },\n \"tab_flash_lite_preview\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 4096,\n \"maxTokens\": 16384,\n \"model\": \"MODEL_PLACEHOLDER_M19\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 1\n },\n \"requiresLeadInGeneration\": true,\n \"supportsCumulativeContext\": true,\n \"supportsEstimateTokenCounter\": true,\n \"toolFormatterType\": \"TOOL_FORMATTER_TYPE_XML\"\n },\n \"tab_jump_flash_lite_preview\": {\n \"addCursorToFindReplaceTarget\": true,\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 4096,\n \"maxTokens\": 16384,\n \"model\": \"MODEL_PLACEHOLDER_M28\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 1\n },\n \"requiresLeadInGeneration\": true,\n \"requiresNoXmlToolExamples\": true,\n \"supportsCumulativeContext\": true,\n \"supportsEstimateTokenCounter\": true,\n \"tabJumpPrintLineRange\": true,\n \"toolFormatterType\": \"TOOL_FORMATTER_TYPE_XML\"\n }\n },\n \"mqueryModelIds\": [\n \"gemini-3.5-flash-lite\"\n ],\n \"tabModelIds\": [\n \"chat_20706\",\n \"chat_23310\"\n ],\n \"tieredModelIds\": {\n \"flash\": [\n \"gemini-3.8-flash-tiered\"\n ],\n \"flashLite\": [\n \"gemini-3.5-flash-lite\"\n ],\n \"pro\": [\n \"gemini-3.1-pro-low\"\n ]\n },\n \"webSearchModelIds\": [\n \"gemini-3.1-flash-lite\"\n ]\n}\n","export const GEMINI_PREVIEW_LINK = \"https://goo.gle/enable-preview-features\";\n\nexport interface GeminiApiError {\n code?: number;\n message?: string;\n status?: string;\n details?: unknown[];\n [key: string]: unknown;\n}\n\n/**\n * The minimal representation of the Gemini API response we touch.\n */\nexport interface GeminiApiBody {\n response?: unknown;\n error?: GeminiApiError;\n [key: string]: unknown;\n}\n\nexport interface GeminiErrorEnhancement {\n body?: GeminiApiBody;\n retryAfterMs?: number;\n}\n\n/**\n * Usage metadata exposed by Gemini responses. Fields are optional to reflect partial payloads.\n */\nexport interface GeminiUsageMetadata {\n totalTokenCount?: number;\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n cachedContentTokenCount?: number;\n}\n\n/**\n * Thinking configuration accepted by Gemini.\n */\nexport interface ThinkingConfig {\n thinkingBudget?: number;\n thinkingLevel?: string;\n includeThoughts?: boolean;\n}\n\nexport interface GoogleRpcErrorInfo {\n \"@type\"?: string;\n reason?: string;\n domain?: string;\n metadata?: Record;\n}\n\nexport interface GoogleRpcHelp {\n \"@type\"?: string;\n links?: Array<{\n description?: string;\n url?: string;\n }>;\n}\n\nexport interface GoogleRpcQuotaFailure {\n \"@type\"?: string;\n violations?: Array<{\n subject?: string;\n description?: string;\n }>;\n}\n\nexport interface GoogleRpcRetryInfo {\n \"@type\"?: string;\n retryDelay?: string | { seconds?: number; nanos?: number };\n}\n\nexport const CLOUDCODE_DOMAINS = [\n \"cloudcode-pa.googleapis.com\",\n \"staging-cloudcode-pa.googleapis.com\",\n \"autopush-cloudcode-pa.googleapis.com\",\n \"daily-cloudcode-pa.googleapis.com\",\n];\n","import type { ThinkingConfig } from \"./types\";\n\n/**\n * Normalizes thinkingConfig aliases to standard Gemini field names.\n */\nexport function normalizeThinkingConfig(config: unknown): ThinkingConfig | undefined {\n if (!config || typeof config !== \"object\") {\n return undefined;\n }\n\n const record = config as Record;\n const budgetRaw = record.thinkingBudget ?? record.thinking_budget;\n const levelRaw = record.thinkingLevel ?? record.thinking_level;\n const includeRaw = record.includeThoughts ?? record.include_thoughts;\n\n let thinkingBudget = typeof budgetRaw === \"number\" && Number.isFinite(budgetRaw) ? budgetRaw : undefined;\n const thinkingLevel =\n typeof levelRaw === \"string\" && levelRaw.trim().length > 0 ? levelRaw.trim().toUpperCase() : undefined;\n const includeThoughts = typeof includeRaw === \"boolean\" ? includeRaw : undefined;\n\n if (thinkingBudget === undefined && thinkingLevel === undefined && includeThoughts === undefined) {\n return undefined;\n }\n\n // Maps thinkingLevel to thinkingBudget and filters it to avoid upstream v1internal API incompatibilities.\n if (thinkingLevel !== undefined && thinkingBudget === undefined) {\n if (thinkingLevel === \"HIGH\") {\n thinkingBudget = 2048;\n } else if (thinkingLevel === \"MEDIUM\") {\n thinkingBudget = 1024;\n } else if (thinkingLevel === \"LOW\") {\n thinkingBudget = 512;\n } else if (thinkingLevel === \"MINIMAL\") {\n thinkingBudget = 0;\n } else {\n thinkingBudget = 1024;\n }\n }\n\n const finalIncludeThoughts =\n includeThoughts !== undefined\n ? includeThoughts\n : thinkingBudget !== undefined\n ? thinkingBudget > 0\n : undefined;\n\n const normalized: ThinkingConfig = {};\n if (thinkingBudget !== undefined) {\n normalized.thinkingBudget = thinkingBudget;\n }\n if (finalIncludeThoughts !== undefined) {\n normalized.includeThoughts = finalIncludeThoughts;\n }\n\n return normalized;\n}\n","import type { GeminiApiBody, GeminiUsageMetadata } from \"./types\";\n\n/**\n * Parses the Gemini API response body; handles array-wrapped responses sometimes returned by the API.\n */\nexport function parseGeminiApiBody(rawText: string): GeminiApiBody | null {\n try {\n const parsed = JSON.parse(rawText);\n if (Array.isArray(parsed)) {\n const firstObject = parsed.find((item: unknown) => typeof item === \"object\" && item !== null);\n return firstObject && typeof firstObject === \"object\"\n ? (firstObject as GeminiApiBody)\n : null;\n }\n\n return parsed && typeof parsed === \"object\" ? (parsed as GeminiApiBody) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Extracts usageMetadata from the response object with type-safe guards.\n */\nexport function extractUsageMetadata(body: GeminiApiBody): GeminiUsageMetadata | null {\n const usage = (body.response && typeof body.response === \"object\"\n ? (body.response as { usageMetadata?: unknown }).usageMetadata\n : undefined) as GeminiUsageMetadata | undefined;\n\n if (!usage || typeof usage !== \"object\") {\n return null;\n }\n\n const asRecord = usage as Record;\n const toNumber = (value: unknown): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n\n return {\n totalTokenCount: toNumber(asRecord.totalTokenCount),\n promptTokenCount: toNumber(asRecord.promptTokenCount),\n candidatesTokenCount: toNumber(asRecord.candidatesTokenCount),\n cachedContentTokenCount: toNumber(asRecord.cachedContentTokenCount),\n };\n}\n","import {\n CLOUDCODE_DOMAINS,\n GEMINI_PREVIEW_LINK,\n type GeminiApiBody,\n type GeminiErrorEnhancement,\n type GoogleRpcErrorInfo,\n type GoogleRpcHelp,\n type GoogleRpcQuotaFailure,\n type GoogleRpcRetryInfo,\n} from \"./types\";\nimport { formatHyperlink } from '../terminal-hyperlink';\n\n/**\n * Enhances 404 errors for Gemini 3 models with direct preview access information.\n */\nexport function rewriteGeminiPreviewAccessError(\n body: GeminiApiBody,\n status: number,\n requestedModel?: string,\n): GeminiApiBody | null {\n if (!needsPreviewAccessOverride(status, body, requestedModel)) {\n return null;\n }\n\n const error = body.error ?? {};\n const trimmedMessage = typeof error.message === \"string\" ? error.message.trim() : \"\";\n const messagePrefix = trimmedMessage.length > 0\n ? trimmedMessage\n : \"Gemini 3 preview features are not enabled for this account.\";\n const enhancedMessage = `${messagePrefix} Request preview access at ${formatHyperlink(GEMINI_PREVIEW_LINK, 'preview access page')} before using Gemini 3 models.`;\n\n return {\n ...body,\n error: {\n ...error,\n message: enhancedMessage,\n },\n };\n}\n\n/**\n * Enhances Gemini errors with validation/quota messages and retry hints.\n */\nexport function enhanceGeminiErrorResponse(\n body: GeminiApiBody,\n status: number,\n): GeminiErrorEnhancement | null {\n const error = body.error;\n if (!error) {\n return null;\n }\n\n const details = Array.isArray(error.details) ? error.details : [];\n const retryAfterMs = extractRetryDelay(details, error.message) ?? undefined;\n\n if (status === 403) {\n const validationInfo = extractValidationInfo(details);\n if (validationInfo) {\n const message = [\n error.message ?? \"Account validation required for Gemini/Agy Code Assist.\",\n validationInfo.link ? `Complete validation: ${formatHyperlink(validationInfo.link, 'validation page')}` : undefined,\n validationInfo.learnMore ? `Learn more: ${formatHyperlink(validationInfo.learnMore, 'help article')}` : undefined,\n ]\n .filter(Boolean)\n .join(\" \");\n return {\n body: {\n ...body,\n error: {\n ...error,\n message,\n },\n },\n retryAfterMs,\n };\n }\n }\n\n if (status === 429) {\n const quotaInfo = extractQuotaInfo(details);\n if (quotaInfo) {\n const message = quotaInfo.retryable\n ? `Rate limit exceeded. ${retryAfterMs ? \"Please retry shortly.\" : \"Please retry.\"}`\n : \"Quota exhausted for this account. Please wait for your quota to reset or upgrade your plan.\";\n return {\n body: {\n ...body,\n error: {\n ...error,\n message,\n },\n },\n retryAfterMs,\n };\n }\n }\n\n return retryAfterMs !== undefined ? { retryAfterMs } : null;\n}\n\nfunction needsPreviewAccessOverride(\n status: number,\n body: GeminiApiBody,\n requestedModel?: string,\n): boolean {\n if (status !== 404) {\n return false;\n }\n if (isGeminiThreeModel(requestedModel)) {\n return true;\n }\n return isGeminiThreeModel(typeof body.error?.message === \"string\" ? body.error.message : \"\");\n}\n\nfunction isGeminiThreeModel(target?: string): boolean {\n return !!target && /gemini[\\s-]?3/i.test(target);\n}\n\nfunction extractValidationInfo(details: unknown[]): { link?: string; learnMore?: string } | null {\n const errorInfo = details.find(\n (detail): detail is GoogleRpcErrorInfo =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcErrorInfo)[\"@type\"] === \"type.googleapis.com/google.rpc.ErrorInfo\",\n );\n\n if (\n !errorInfo ||\n errorInfo.reason !== \"VALIDATION_REQUIRED\" ||\n !errorInfo.domain ||\n !CLOUDCODE_DOMAINS.includes(errorInfo.domain)\n ) {\n return null;\n }\n\n const helpDetail = details.find(\n (detail): detail is GoogleRpcHelp =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcHelp)[\"@type\"] === \"type.googleapis.com/google.rpc.Help\",\n );\n\n let link: string | undefined;\n let learnMore: string | undefined;\n if (helpDetail?.links && helpDetail.links.length > 0) {\n link = helpDetail.links[0]?.url;\n const learnMoreLink = helpDetail.links.find((candidate) => {\n if (!candidate?.url) {\n return false;\n }\n if (candidate.description?.toLowerCase().trim() === \"learn more\") {\n return true;\n }\n try {\n return new URL(candidate.url).hostname === \"support.google.com\";\n } catch {\n return false;\n }\n });\n learnMore = learnMoreLink?.url;\n }\n\n if (!link && errorInfo.metadata?.validation_link) {\n link = errorInfo.metadata.validation_link;\n }\n\n return link || learnMore ? { link, learnMore } : null;\n}\n\nfunction extractQuotaInfo(details: unknown[]): { retryable: boolean } | null {\n const errorInfo = details.find(\n (detail): detail is GoogleRpcErrorInfo =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcErrorInfo)[\"@type\"] === \"type.googleapis.com/google.rpc.ErrorInfo\",\n );\n\n if (errorInfo?.reason === \"RATE_LIMIT_EXCEEDED\") {\n return { retryable: true };\n }\n if (errorInfo?.reason === \"QUOTA_EXHAUSTED\") {\n return { retryable: false };\n }\n\n const quotaFailure = details.find(\n (detail): detail is GoogleRpcQuotaFailure =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcQuotaFailure)[\"@type\"] === \"type.googleapis.com/google.rpc.QuotaFailure\",\n );\n\n if (!quotaFailure?.violations?.length) {\n return null;\n }\n\n const description = quotaFailure.violations\n .map((violation) => violation.description?.toLowerCase() ?? \"\")\n .join(\" \");\n if (description.includes(\"daily\") || description.includes(\"per day\")) {\n return { retryable: false };\n }\n return { retryable: true };\n}\n\nfunction extractRetryDelay(details: unknown[], errorMessage?: string): number | null {\n const retryInfo = details.find(\n (detail): detail is GoogleRpcRetryInfo =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcRetryInfo)[\"@type\"] === \"type.googleapis.com/google.rpc.RetryInfo\",\n );\n\n if (retryInfo?.retryDelay) {\n const delayMs = parseRetryDelayValue(retryInfo.retryDelay);\n if (delayMs !== null) {\n return delayMs;\n }\n }\n\n if (!errorMessage) {\n return null;\n }\n\n const retryMatch = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);\n if (retryMatch?.[1]) {\n return parseRetryDelayValue(retryMatch[1]);\n }\n const resetMatch = errorMessage.match(/after\\s+([0-9.]+(?:ms|s))/i);\n if (resetMatch?.[1]) {\n return parseRetryDelayValue(resetMatch[1]);\n }\n\n return null;\n}\n\nfunction parseRetryDelayValue(value: string | { seconds?: number; nanos?: number }): number | null {\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed) {\n return null;\n }\n if (trimmed.endsWith(\"ms\")) {\n const ms = Number(trimmed.slice(0, -2));\n return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : null;\n }\n const match = trimmed.match(/^([\\d.]+)s$/);\n if (match?.[1]) {\n const seconds = Number(match[1]);\n return Number.isFinite(seconds) && seconds > 0 ? Math.round(seconds * 1000) : null;\n }\n return null;\n }\n\n const seconds = typeof value.seconds === \"number\" ? value.seconds : 0;\n const nanos = typeof value.nanos === \"number\" ? value.nanos : 0;\n if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {\n return null;\n }\n const totalMs = Math.round(seconds * 1000 + nanos / 1e6);\n return totalMs > 0 ? totalMs : null;\n}\n","import { randomUUID } from \"node:crypto\";\n\nimport { isRecord, pickString } from \"./shared\";\n\nconst PROCESS_SESSION_ID = randomUUID();\nlet PROCESS_REQUEST_INDEX = 0;\n\nfunction formatAgyRequestId(userPromptId: string, sessionId: string): string {\n if (userPromptId.startsWith(\"agent/\")) {\n return userPromptId;\n }\n\n return `agent/${sessionId}/${Date.now()}/${userPromptId}/${PROCESS_REQUEST_INDEX++}`;\n}\n\nfunction resolveUserPromptId(payload: Record, request?: Record): string {\n const extra = isRecord(payload.extra_body) ? payload.extra_body : undefined;\n\n return (\n pickString(\n payload.user_prompt_id,\n payload.userPromptId,\n payload.prompt_id,\n payload.promptId,\n payload.request_id,\n payload.requestId,\n request?.user_prompt_id,\n request?.userPromptId,\n request?.prompt_id,\n request?.promptId,\n request?.request_id,\n request?.requestId,\n extra?.user_prompt_id,\n extra?.userPromptId,\n extra?.prompt_id,\n extra?.promptId,\n extra?.request_id,\n extra?.requestId,\n ) ?? randomUUID()\n );\n}\n\nfunction resolveSessionId(payload: Record, request?: Record): string {\n const extra = isRecord(payload.extra_body) ? payload.extra_body : undefined;\n return (\n pickString(\n request?.session_id,\n request?.sessionId,\n payload.session_id,\n payload.sessionId,\n extra?.session_id,\n extra?.sessionId,\n ) ?? PROCESS_SESSION_ID\n );\n}\n\nfunction stripPromptIdentifierAliases(payload: Record): void {\n delete payload.user_prompt_id;\n delete payload.userPromptId;\n delete payload.prompt_id;\n delete payload.promptId;\n delete payload.request_id;\n delete payload.requestId;\n}\n\nfunction stripSessionIdentifierAliases(payload: Record): void {\n delete payload.sessionId;\n}\n\n/**\n * Applies canonical identifiers for wrapped Code Assist payloads.\n */\nexport function normalizeWrappedIdentifiers(\n wrapped: Record,\n): { userPromptId: string; sessionId: string; requestId: string } {\n const request = isRecord(wrapped.request) ? { ...wrapped.request } : {};\n const userPromptId = resolveUserPromptId(wrapped, request);\n const sessionId = resolveSessionId(wrapped, request);\n const requestId = formatAgyRequestId(userPromptId, sessionId);\n\n request.session_id = sessionId;\n stripSessionIdentifierAliases(request);\n wrapped.request = request;\n\n stripPromptIdentifierAliases(wrapped);\n wrapped.requestId = requestId;\n\n return { userPromptId, sessionId, requestId };\n}\n\n/**\n * Applies canonical identifiers for unwrapped request payloads prior to wrapping.\n */\nexport function normalizeRequestPayloadIdentifiers(\n payload: Record,\n): { userPromptId: string; sessionId: string; requestId: string } {\n const userPromptId = resolveUserPromptId(payload);\n const sessionId = resolveSessionId(payload);\n const requestId = formatAgyRequestId(userPromptId, sessionId);\n\n payload.session_id = sessionId;\n stripSessionIdentifierAliases(payload);\n stripPromptIdentifierAliases(payload);\n\n return { userPromptId, sessionId, requestId };\n}\n","import { AGY_GENERATIVE_LANGUAGE_ENDPOINT } from \"../../constants\";\n\nconst REQUEST_MODEL_FALLBACKS: Record = {\n // gemini-3.1-pro-high still appears in `agy models` output but its backend\n // enum (MODEL_PLACEHOLDER_M37) was deprecated server-side in favor of\n // gemini-pro-agent (MODEL_PLACEHOLDER_M16). See models.json\n // deprecatedModelIds entry. Rewrite the model id before getModelEnum\n // resolves the deprecated enum, so both body.model and labels.model_enum\n // use the live canonical id and enum.\n \"gemini-3.1-pro-high\": \"gemini-pro-agent\",\n};\nconst GENERATIVE_LANGUAGE_HOST = new URL(AGY_GENERATIVE_LANGUAGE_ENDPOINT).host;\nconst CODE_ASSIST_HOST_SUFFIX = \"cloudcode-pa.googleapis.com\";\nconst MODEL_ACTION_PATTERN = /\\/models\\/[^:]+:\\w+/;\n\n/**\n * Returns the URL string for supported RequestInfo inputs.\n */\nexport function toRequestUrlString(value: RequestInfo): string {\n if (typeof value === \"string\") {\n return value;\n }\n if (value instanceof URL) {\n return value.toString();\n }\n const candidate = (value as Request).url;\n if (candidate) {\n return candidate;\n }\n return value.toString();\n}\n\n/**\n * Detects Gemini/Generative Language API requests via URL.\n */\nexport function isGenerativeLanguageRequest(input: RequestInfo): input is string {\n const url = toRequestUrlString(input);\n return (\n url.includes(GENERATIVE_LANGUAGE_HOST) ||\n (url.includes(CODE_ASSIST_HOST_SUFFIX) && MODEL_ACTION_PATTERN.test(url))\n );\n}\n\nexport function parseGenerativeLanguageRequest(input: RequestInfo):\n | { requestedModel: string; effectiveModel: string; action: string }\n | undefined {\n const match = toRequestUrlString(input).match(/\\/models\\/([^:]+):(\\w+)/);\n if (!match) {\n return undefined;\n }\n\n const [, requestedModel = \"\", action = \"\"] = match;\n return {\n requestedModel,\n effectiveModel: REQUEST_MODEL_FALLBACKS[requestedModel] ?? requestedModel,\n action,\n };\n}\n\nexport function isRecord(value: unknown): value is Record {\n return !!value && typeof value === \"object\";\n}\n\nexport function readString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nexport function pickString(...values: unknown[]): string | undefined {\n for (const value of values) {\n const str = readString(value);\n if (str) {\n return str;\n }\n }\n return undefined;\n}\n\n/**\n * Preserves Cloud Code trace identity for downstream clients by mapping traceId to responseId.\n */\nexport function injectResponseIdFromTrace>(body: T): T {\n const traceId = readString(body.traceId);\n if (!traceId) {\n return body;\n }\n\n const response = body.response;\n if (!isRecord(response)) {\n return body;\n }\n\n if (readString(response.responseId)) {\n return body;\n }\n\n return {\n ...body,\n response: {\n ...response,\n responseId: traceId,\n },\n };\n}\n","/**\n * NOTE: Format/protocol conversion logic is typically in the app layer (Plugin), but is implemented here inside the SDK as a special case.\n * Reason:\n * Format conversion (OpenAI format to Gemini/Agy native and vice versa) is tightly coupled with Agy's unique streaming SSE data parsing,\n * thought chain deduplication, and signature self-healing (Thinking Recovery / Signature Cache) in multi-turn dialogues.\n * Encapsulating this conversion in the SDK completely shields the OpenCode plugin app layer from non-standard API interaction complexities,\n * allowing the plugin to simply call and forward standard OpenAI formatted requests and response streams.\n */\n\nimport type { ToolMapper } from \"./tool-mapper\";\n\ninterface GeminiFunctionCallPart {\n functionCall?: {\n id?: string;\n name: string;\n args?: Record;\n [key: string]: unknown;\n };\n thoughtSignature?: string;\n [key: string]: unknown;\n}\n\ninterface OpenAIToolCall {\n id?: string;\n function?: {\n name?: string;\n arguments?: string;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n}\n\ninterface OpenAIMessage {\n content?: string | null;\n tool_calls?: OpenAIToolCall[];\n [key: string]: unknown;\n}\n\n/**\n * Converts OpenAI's `tool_calls` into Gemini's `functionCall` sections.\n */\nexport function transformOpenAIToolCalls(requestPayload: Record, toolMapper?: ToolMapper): void {\n const messages = requestPayload.messages;\n if (!messages || !Array.isArray(messages)) {\n return;\n }\n\n for (const message of messages) {\n if (!message || typeof message !== \"object\") {\n continue;\n }\n\n const msgObj = message as OpenAIMessage;\n const toolCalls = msgObj.tool_calls;\n if (!toolCalls || !Array.isArray(toolCalls) || toolCalls.length === 0) {\n continue;\n }\n\n const parts: GeminiFunctionCallPart[] = [];\n if (typeof msgObj.content === \"string\" && msgObj.content.length > 0) {\n parts.push({ text: msgObj.content });\n }\n\n for (const toolCall of toolCalls) {\n if (!toolCall || typeof toolCall !== \"object\") {\n continue;\n }\n\n const fn = toolCall.function;\n if (!fn || typeof fn !== \"object\") {\n continue;\n }\n\n const rawName = fn.name ?? \"\";\n const name = toolMapper ? toolMapper.toGemini(rawName) : rawName;\n const args = parseJsonObject(fn.arguments);\n\n const functionCallPart: NonNullable = {\n name,\n args,\n };\n\n if (typeof toolCall.id === 'string' && toolCall.id.length > 0) {\n functionCallPart.id = toolCall.id;\n }\n\n parts.push({\n functionCall: functionCallPart,\n thoughtSignature: \"skip_thought_signature_validator\",\n });\n }\n\n msgObj.parts = parts;\n delete msgObj.tool_calls;\n delete msgObj.content;\n }\n}\n\n/**\n * Adds synthesized thoughtSignature to function calls in the flattened and wrapped payload.\n */\nexport function addThoughtSignaturesToFunctionCalls(requestPayload: Record): void {\n const processContents = (contents: unknown): void => {\n if (!contents || !Array.isArray(contents)) {\n return;\n }\n\n for (const content of contents) {\n if (!content || typeof content !== \"object\") {\n continue;\n }\n\n const parts = (content as Record).parts;\n if (!parts || !Array.isArray(parts)) {\n continue;\n }\n\n for (const part of parts) {\n if (!part || typeof part !== \"object\") {\n continue;\n }\n const partObj = part as Record;\n if (partObj.functionCall && !partObj.thoughtSignature) {\n partObj.thoughtSignature = \"skip_thought_signature_validator\";\n }\n }\n }\n };\n\n processContents(requestPayload.contents);\n if (requestPayload.request && typeof requestPayload.request === \"object\") {\n processContents((requestPayload.request as Record).contents);\n }\n}\n\nfunction parseJsonObject(value: unknown): Record {\n if (typeof value !== \"string\") {\n return {};\n }\n try {\n const parsed = JSON.parse(value);\n if (parsed && typeof parsed === \"object\") {\n return parsed as Record;\n }\n return {};\n } catch {\n return {};\n }\n}\n","/**\n * Tool Name Schema Mapping for Gemini API\n *\n * Gemini API tool schemas strictly enforce function names matching `^[a-zA-Z_][a-zA-Z0-9_]*$`.\n * Agent frameworks (MCP servers, OpenCode plugins, OpenAI adapters) often use hyphens (-), dots (.),\n * slashes (/), or colons (:) in tool names (e.g. `context7_resolve_library_id`, `atlassian:get-issue`, `my-tool`).\n *\n * This module maintains a bidirectional mapping between original client tool names and Gemini-compliant tool names,\n * resolving collisions deterministically and persisting session mappings across multi-turn tool loops.\n */\n\nexport function sanitizeToolName(name: string): string {\n if (!name || typeof name !== \"string\") {\n return \"unnamed_tool\";\n }\n\n // Replace all non-alphanumeric and non-underscore characters with '_'\n let sanitized = name.replace(/[^a-zA-Z0-9_]/g, \"_\");\n\n // Ensure starts with [a-zA-Z_]\n if (!/^[a-zA-Z_]/.test(sanitized)) {\n sanitized = `_${sanitized}`;\n }\n\n return sanitized;\n}\n\nexport class ToolMapper {\n private originalToSanitized = new Map();\n private sanitizedToOriginal = new Map();\n\n /**\n * Register a tool name and get its Gemini-compliant sanitized name.\n * Handles naming collisions by appending a numeric suffix if needed.\n */\n register(originalName: string): string {\n if (!originalName || typeof originalName !== \"string\") {\n return originalName;\n }\n\n const existing = this.originalToSanitized.get(originalName);\n if (existing) {\n return existing;\n }\n\n const baseSanitized = sanitizeToolName(originalName);\n let sanitized = baseSanitized;\n let counter = 1;\n\n // Resolve collision if a different originalName already claimed this sanitized name\n while (this.sanitizedToOriginal.has(sanitized) && this.sanitizedToOriginal.get(sanitized) !== originalName) {\n sanitized = `${baseSanitized}_${counter++}`;\n }\n\n this.originalToSanitized.set(originalName, sanitized);\n this.sanitizedToOriginal.set(sanitized, originalName);\n\n return sanitized;\n }\n\n /**\n * Map an original tool name to sanitized Gemini name.\n * If not already registered, registers it on the fly.\n */\n toGemini(originalName: string): string {\n if (!originalName || typeof originalName !== \"string\") {\n return originalName;\n }\n const sanitized = this.originalToSanitized.get(originalName);\n if (sanitized) {\n return sanitized;\n }\n return this.register(originalName);\n }\n\n /**\n * Restore a sanitized Gemini tool name back to the original client tool name.\n */\n fromGemini(sanitizedName: string): string {\n if (!sanitizedName || typeof sanitizedName !== \"string\") {\n return sanitizedName;\n }\n return this.sanitizedToOriginal.get(sanitizedName) ?? sanitizedName;\n }\n\n /**\n * Register tools from Gemini `tools[].functionDeclarations` array.\n */\n registerFromFunctionDeclarations(tools: unknown): void {\n if (!Array.isArray(tools)) return;\n for (const tool of tools) {\n if (tool && Array.isArray(tool.functionDeclarations)) {\n for (const fn of tool.functionDeclarations) {\n if (fn && typeof fn.name === \"string\") {\n this.register(fn.name);\n }\n }\n }\n }\n }\n\n /**\n * Register tools from OpenAI format `tools[].function.name`.\n */\n registerFromOpenAITools(tools: unknown): void {\n if (!Array.isArray(tools)) return;\n for (const tool of tools) {\n if (tool && typeof tool === \"object\") {\n const fn = (tool as Record).function as Record | undefined;\n if (fn && typeof fn.name === \"string\") {\n this.register(fn.name);\n }\n }\n }\n }\n\n /**\n * Scan contents/messages to register any previously used tool names.\n */\n registerFromContents(contents: unknown): void {\n if (!Array.isArray(contents)) return;\n for (const content of contents) {\n if (!content || typeof content !== \"object\") continue;\n const parts = (content as Record).parts;\n if (Array.isArray(parts)) {\n for (const part of parts) {\n if (!part || typeof part !== \"object\") continue;\n const p = part as Record;\n if (p.functionCall && typeof (p.functionCall as Record).name === \"string\") {\n this.register((p.functionCall as Record).name as string);\n }\n if (p.functionResponse && typeof (p.functionResponse as Record).name === \"string\") {\n this.register((p.functionResponse as Record).name as string);\n }\n }\n }\n }\n }\n}\n\n// Session-based mapper cache\nconst sessionMappers = new Map();\nconst MAX_SESSION_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport function getToolMapper(sessionId?: string): ToolMapper {\n if (!sessionId) {\n return new ToolMapper();\n }\n\n const now = Date.now();\n const existing = sessionMappers.get(sessionId);\n if (existing && now - existing.updatedAt < MAX_SESSION_AGE_MS) {\n existing.updatedAt = now;\n return existing.mapper;\n }\n\n // Cleanup old sessions if cache grows large\n if (sessionMappers.size > 1000) {\n for (const [key, value] of sessionMappers.entries()) {\n if (now - value.updatedAt >= MAX_SESSION_AGE_MS) {\n sessionMappers.delete(key);\n }\n }\n }\n\n const mapper = new ToolMapper();\n sessionMappers.set(sessionId, { mapper, updatedAt: now });\n return mapper;\n}\n\nexport function clearToolMapper(sessionId: string): void {\n sessionMappers.delete(sessionId);\n}\n\n/**\n * Restores original client tool names inside Gemini candidates/parts functionCall objects.\n */\nexport function restoreToolNamesInResponse(body: unknown, toolMapper: ToolMapper): void {\n if (!body || typeof body !== \"object\") return;\n const b = body as Record;\n\n const target = b.response && typeof b.response === \"object\" ? (b.response as Record) : b;\n const candidates = target.candidates;\n if (Array.isArray(candidates)) {\n for (const cand of candidates) {\n if (!cand || typeof cand !== \"object\") continue;\n const content = (cand as Record).content;\n if (!content || typeof content !== \"object\") continue;\n const parts = (content as Record).parts;\n if (Array.isArray(parts)) {\n for (const part of parts) {\n if (!part || typeof part !== \"object\") continue;\n const p = part as Record;\n if (p.functionCall && typeof (p.functionCall as Record).name === \"string\") {\n const fnCall = p.functionCall as Record;\n fnCall.name = toolMapper.fromGemini(fnCall.name as string);\n }\n }\n }\n }\n }\n}\n","\nimport {\n enhanceGeminiErrorResponse,\n extractUsageMetadata,\n parseGeminiApiBody,\n rewriteGeminiPreviewAccessError,\n type GeminiApiBody,\n} from \"../request-helpers\";\nimport { injectResponseIdFromTrace } from \"./shared\";\nimport { cacheSignature } from \"../../plugin/cache\";\nimport { createStreamingTransformer, defaultSignatureStore } from \"./thinking\";\nimport { getTurnStateTracker, type TurnState } from \"./turn-state-tracker\";\nimport type { ChatLogger } from \"../chat-logger\";\nimport { getToolMapper, restoreToolNamesInResponse } from \"./tool-mapper\";\n\n/**\n * Normalizes Gemini/Agy responses, preserving request metadata and usage counters.\n */\nexport async function transformAgyResponse(\n response: Response,\n streaming: boolean,\n _ignoredDebugContext?: any,\n requestedModel?: string,\n sessionId?: string,\n chatLogger?: ChatLogger | null,\n): Promise {\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n const isJsonResponse = contentType.includes(\"application/json\");\n const isEventStreamResponse = contentType.includes(\"text/event-stream\");\n\n if (!isJsonResponse && !isEventStreamResponse) {\n if (chatLogger) {\n chatLogger.logResponseHeaders(response.status, response.statusText, response.headers);\n chatLogger.logResponseBody(\"[Non-JSON response (body omitted)]\");\n chatLogger.close();\n }\n return response;\n }\n\n try {\n const headers = new Headers(response.headers);\n if (chatLogger) {\n chatLogger.logResponseHeaders(response.status, response.statusText, headers);\n }\n\n if (streaming && response.ok && isEventStreamResponse && response.body) {\n return new Response(transformStreamingPayloadStream(response.body, sessionId, chatLogger), {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n }\n\n const text = await response.text();\n if (chatLogger) {\n chatLogger.logResponseBody(text);\n chatLogger.close();\n }\n\n const init = {\n status: response.status,\n statusText: response.statusText,\n headers,\n };\n\n const parsed: GeminiApiBody | null = !streaming || !isEventStreamResponse ? parseGeminiApiBody(text) : null;\n const enhanced = !response.ok && parsed ? enhanceGeminiErrorResponse(parsed, response.status) : null;\n if (enhanced?.retryAfterMs) {\n const retryAfterSec = Math.ceil(enhanced.retryAfterMs / 1000).toString();\n headers.set(\"Retry-After\", retryAfterSec);\n headers.set(\"retry-after-ms\", String(enhanced.retryAfterMs));\n }\n\n const previewPatched = parsed\n ? rewriteGeminiPreviewAccessError(enhanced?.body ?? parsed, response.status, requestedModel)\n : null;\n\n const effectiveBodyRaw = previewPatched ?? enhanced?.body ?? parsed ?? undefined;\n const effectiveBody =\n effectiveBodyRaw && typeof effectiveBodyRaw === \"object\"\n ? injectResponseIdFromTrace(effectiveBodyRaw as Record)\n : effectiveBodyRaw;\n\n if (effectiveBody) {\n const toolMapper = getToolMapper(sessionId);\n restoreToolNamesInResponse(effectiveBody, toolMapper);\n }\n\n attachUsageHeaders(headers, effectiveBody);\n\n if (!parsed) {\n return new Response(text, init);\n }\n\n if (effectiveBody && typeof effectiveBody === \"object\" && \"response\" in effectiveBody) {\n return new Response(JSON.stringify((effectiveBody as { response: unknown }).response), init);\n }\n if (previewPatched) {\n return new Response(JSON.stringify(previewPatched), init);\n }\n\n return new Response(text, init);\n } catch (error) {\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to transform Gemini/Agy response: ${errStr}`);\n return response;\n }\n}\n\nfunction attachUsageHeaders(headers: Headers, effectiveBody: unknown): void {\n if (!effectiveBody || typeof effectiveBody !== \"object\") {\n return;\n }\n const usage = extractUsageMetadata(effectiveBody as GeminiApiBody);\n if (usage?.cachedContentTokenCount === undefined) {\n return;\n }\n\n headers.set(\"x-gemini-cached-content-token-count\", String(usage.cachedContentTokenCount));\n if (usage.totalTokenCount !== undefined) {\n headers.set(\"x-gemini-total-token-count\", String(usage.totalTokenCount));\n }\n if (usage.promptTokenCount !== undefined) {\n headers.set(\"x-gemini-prompt-token-count\", String(usage.promptTokenCount));\n }\n if (usage.candidatesTokenCount !== undefined) {\n headers.set(\"x-gemini-candidates-token-count\", String(usage.candidatesTokenCount));\n }\n}\n\nfunction transformStreamingPayloadStream(\n stream: ReadableStream,\n sessionId?: string,\n chatLogger?: ChatLogger | null,\n): ReadableStream {\n const callbacks = {\n onCacheSignature: (sessionKey: string, text: string, signature: string) => {\n cacheSignature(sessionKey, text, signature);\n },\n onTurnStateUpdate: (sessionKey: string, state: { turnHasThinking: boolean; lastModelHasToolCalls: boolean }) => {\n const tracker = getTurnStateTracker();\n if (tracker) {\n tracker.updateAfterResponse(sessionKey, {\n inToolLoop: state.lastModelHasToolCalls,\n turnHasThinking: state.turnHasThinking,\n lastModelHasThinking: state.turnHasThinking,\n lastModelHasToolCalls: state.lastModelHasToolCalls,\n });\n }\n },\n transformThinkingParts: (response: unknown) => {\n if (response && typeof response === \"object\") {\n const toolMapper = getToolMapper(sessionId);\n restoreToolNamesInResponse(response, toolMapper);\n return injectResponseIdFromTrace(response as Record);\n }\n return response;\n },\n };\n\n const transformer = createStreamingTransformer(\n defaultSignatureStore,\n callbacks,\n {\n signatureSessionKey: sessionId,\n cacheSignatures: !!sessionId,\n },\n );\n\n if (chatLogger) {\n return stream\n .pipeThrough(chatLogger.createLoggingTransformStream())\n .pipeThrough(transformer);\n }\n\n return stream.pipeThrough(transformer);\n}\n","import { createWriteStream, existsSync, mkdirSync, WriteStream } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { cwd } from \"node:process\";\n\nexport interface ChatLogger {\n logRequest(url: string, method: string, headers: HeadersInit | undefined, body: BodyInit | null | undefined): void;\n logResponseHeaders(status: number, statusText: string, headers: Headers): void;\n logResponseBody(body: string): void;\n createLoggingTransformStream(): TransformStream;\n close(): void;\n}\n\nexport function createChatLogger(): ChatLogger | null {\n if (process.env.AGY_LOG !== \"1\") {\n return null;\n }\n try {\n const logDir = join(cwd(), \"agy_chat_log\");\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n\n const timestamp = Date.now();\n const logFile = join(logDir, `${timestamp}.log`);\n const stream = createWriteStream(logFile, { flags: \"w\", encoding: \"utf8\" });\n\n return new ChatLoggerImpl(stream);\n } catch (error) {\n console.error(\"[Agy Chat Logger] Failed to initialize logger\", error);\n return null;\n }\n}\n\nclass ChatLoggerImpl implements ChatLogger {\n private stream: WriteStream;\n\n constructor(stream: WriteStream) {\n this.stream = stream;\n }\n\n logRequest(url: string, method: string, headers: HeadersInit | undefined, body: BodyInit | null | undefined): void {\n this.stream.write(`=========== REQUEST ===========\\n`);\n this.stream.write(`[Time] ${new Date().toISOString()}\\n`);\n this.stream.write(`[Request] ${method} ${url}\\n`);\n\n if (headers) {\n this.stream.write(`[Headers]\\n`);\n const h = new Headers(headers);\n h.forEach((value, key) => {\n // optionally mask token if needed, but since it's for local debug, we might just write it.\n // Let's mask authorization just in case.\n if (key.toLowerCase() === \"authorization\") {\n this.stream.write(` ${key}: [redacted]\\n`);\n } else {\n this.stream.write(` ${key}: ${value}\\n`);\n }\n });\n }\n\n this.stream.write(`\\n[Body]\\n`);\n if (body) {\n if (typeof body === \"string\") {\n try {\n const parsed = JSON.parse(body);\n this.stream.write(JSON.stringify(parsed, null, 2));\n } catch {\n this.stream.write(body);\n }\n } else {\n this.stream.write(`[Non-string body omitted]`);\n }\n } else {\n this.stream.write(`[No body]`);\n }\n this.stream.write(`\\n\\n`);\n }\n\n logResponseHeaders(status: number, statusText: string, headers: Headers): void {\n this.stream.write(`=========== RESPONSE ===========\\n`);\n this.stream.write(`[Status] ${status} ${statusText}\\n`);\n this.stream.write(`[Headers]\\n`);\n headers.forEach((value, key) => {\n this.stream.write(` ${key}: ${value}\\n`);\n });\n this.stream.write(`\\n[Body / Chunks]\\n`);\n }\n\n logResponseBody(body: string): void {\n this.stream.write(body);\n this.stream.write(`\\n\\n=========== END ===========\\n`);\n }\n\n createLoggingTransformStream(): TransformStream {\n const stream = this.stream;\n const decoder = new TextDecoder();\n return new TransformStream({\n transform(chunk, controller) {\n stream.write(decoder.decode(chunk, { stream: true }));\n controller.enqueue(chunk);\n },\n flush(controller) {\n stream.write(decoder.decode());\n stream.write(`\\n\\n=========== END ===========\\n`);\n }\n });\n }\n\n close(): void {\n try {\n this.stream.end();\n } catch {}\n }\n}\n","import { updateStaticModelsWithPricing } from './plugin/pricing';\nimport type { Config } from './plugin/types';\nimport { AGY_PROVIDER_ID } from './constants';\nimport { agyFetch } from './fetch';\nimport { createOAuthAuthorizeMethod } from './plugin/oauth-authorize';\nimport { accessTokenExpired, isOAuthAuth, parseRefreshParts } from './plugin/auth';\nimport { resolveCachedAuth, initDiskSignatureCache } from './plugin/cache';\nimport { initTurnStateTracker } from './sdk/request/turn-state-tracker';\nimport { initCooldownPersistence } from './sdk/retry';\nimport { ensureProjectContext, retrieveUserQuota, retrieveUserQuotaSummary } from './plugin/project';\nimport { createAgyQuotaTool, AGY_QUOTA_TOOL_NAME } from './plugin/quota';\nimport { createAgyQuotaSummaryTool, AGY_QUOTA_SUMMARY_TOOL_NAME } from './plugin/quota-summary';\nimport { maybeShowAgyCapacityToast, maybeShowAgyTestToast } from './plugin/notify';\nimport { simulateClientBackgroundTraffic } from './plugin/traffic';\nimport { buildAgyCliUserAgent } from './sdk/user-agent';\nimport {\n resolveConfiguredProjectId,\n resolveConfiguredProjectIdFromClient,\n resolveConfiguredProjectIdFromConfig\n} from './plugin/provider';\nimport {\n isGenerativeLanguageRequest,\n parseGenerativeLanguageRequest,\n prepareAgyRequest,\n type ThinkingConfigDefaults,\n transformAgyResponse\n} from './sdk/request';\nimport { createChatLogger } from './sdk/chat-logger';\nimport { fetchWithRetry } from './sdk/retry';\nimport { refreshAccessToken } from './plugin/token';\nimport type {\n GetAuth,\n OAuthAuthDetails,\n PluginClient,\n PluginContext,\n PluginResult,\n Provider,\n ProviderModel,\n ProviderV2\n} from './plugin/types';\n\nconst AGY_QUOTA_COMMAND = 'agyquota';\nconst AGY_QUOTA_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota usage for the current authenticated account.\n\nImmediately call \\`${AGY_QUOTA_TOOL_NAME}\\` with no arguments and return its output verbatim.\nDo not call other tools.\n`;\n\nconst AGY_QUOTA_SUMMARY_COMMAND = 'agyquotasummary';\nconst AGY_QUOTA_SUMMARY_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota summary (weekly and 5-hour limits by model group) for the current authenticated account.\n\nImmediately call \\`${AGY_QUOTA_SUMMARY_TOOL_NAME}\\` with no arguments and return its output verbatim.\nDo not call other tools.\n`;\nlet latestAgyAuthResolver: GetAuth | undefined;\nlet latestAgyConfiguredProjectId: string | undefined;\nlet latestAgyUserAgentModel: string | undefined;\n\ninterface SimpleStaticModel {\n name: string;\n description: string;\n maxTokens: number;\n maxOutputTokens: number;\n toolCall: boolean;\n reasoning: boolean;\n attachment: boolean;\n cost?: {\n input: number;\n output: number;\n cache?: { read: number; write: number };\n };\n}\n\nconst STATIC_MODELS_SIMPLE: Record = {\n 'gemini-3.8-flash': {\n name: 'Gemini 3.8 Flash',\n description: 'Gemini 3.8 Flash base model. Select tier at runtime.',\n maxTokens: 1048576,\n maxOutputTokens: 65536,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'gemini-3.7-flash': {\n name: 'Gemini 3.7 Flash',\n description: 'Gemini 3.7 Flash base model. Select tier at runtime.',\n maxTokens: 1048576,\n maxOutputTokens: 65536,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'gemini-3.6-flash': {\n name: 'Gemini 3.6 Flash',\n description: 'Gemini 3.6 Flash base model. Select tier at runtime.',\n maxTokens: 1048576,\n maxOutputTokens: 65536,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'gemini-3.1-pro': {\n name: 'Gemini 3.1 Pro',\n description: 'Gemini 3.1 Pro base model. Select tier at runtime.',\n maxTokens: 1048576,\n maxOutputTokens: 65535,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'claude-sonnet-4-6': {\n name: 'Claude Sonnet 4.6 (Thinking)',\n description: 'Claude Sonnet 4.6 deep reasoning model, perfectly balancing thinking process, processing speed, and output quality.',\n maxTokens: 250000,\n maxOutputTokens: 64000,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'claude-opus-4-6-thinking': {\n name: 'Claude Opus 4.6 (Thinking)',\n description: 'Claude Opus 4.6 deep reasoning model, built-in chain of thought, highly suitable for top-tier algorithm and logic puzzles.',\n maxTokens: 250000,\n maxOutputTokens: 64000,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'gpt-oss-120b-medium': {\n name: 'GPT-OSS 120B (Medium)',\n description: 'GPT open-source 120B parameter medium tier model, excellent performance in local deployment or specific open-source benchmarks.',\n maxTokens: 131072,\n maxOutputTokens: 32768,\n toolCall: true,\n reasoning: true,\n attachment: false\n }\n};\n\nconst TIER_MAPPING: Record> = {\n 'gemini-3.8-flash': {\n low: 'gemini-3.8-flash-low',\n medium: 'gemini-3.8-flash-medium',\n high: 'gemini-3.8-flash-high'\n },\n 'gemini-3.7-flash': {\n low: 'gemini-3.7-flash-low',\n medium: 'gemini-3.7-flash-medium',\n high: 'gemini-3.7-flash-high'\n },\n 'gemini-3.6-flash': {\n minimal: 'gemini-3.6-flash-low',\n low: 'gemini-3.6-flash-low',\n medium: 'gemini-3.6-flash-medium',\n high: 'gemini-3.6-flash-high'\n },\n 'gemini-3.1-pro': {\n low: 'gemini-3.1-pro-low',\n high: 'gemini-3.1-pro-high'\n }\n};\n\nconst buildModelFromSimple = (modelId: string, simple: SimpleStaticModel): ProviderModel => {\n const isClaude = modelId.startsWith('claude-');\n const isGpt = modelId.startsWith('gpt-');\n\n let variants: any = undefined;\n if (TIER_MAPPING[modelId]) {\n const hasMinimal = TIER_MAPPING[modelId].minimal !== undefined;\n variants = {\n ...(hasMinimal ? {\n 'minimal': {\n id: 'minimal', name: 'minimal', displayName: 'minimal',\n title: 'minimal', label: 'minimal',\n options: { name: 'minimal' },\n headers: { 'x-agy-tier': 'minimal' },\n thinkingConfig: { thinkingBudget: 1000, includeThoughts: true }\n }\n } : {}),\n 'low': { id: 'low', name: 'low', displayName: 'low', title: 'low', label: 'low', options: { name: 'low' }, headers: { 'x-agy-tier': 'low' } },\n ...(TIER_MAPPING[modelId].medium !== undefined ? { 'medium': { id: 'medium', name: 'medium', displayName: 'medium', title: 'medium', label: 'medium', options: { name: 'medium' }, headers: { 'x-agy-tier': 'medium' } } } : {}),\n 'high': { id: 'high', name: 'high', displayName: 'high', title: 'high', label: 'high', options: { name: 'high' }, headers: { 'x-agy-tier': 'high' } }\n };\n }\n\n return {\n id: modelId,\n providerID: AGY_PROVIDER_ID,\n api: {\n id: AGY_PROVIDER_ID,\n url: 'https://cloudcode-pa.googleapis.com',\n npm: '@ai-sdk/google'\n },\n name: simple.name,\n family: modelId.includes('gemini') ? 'gemini' : (isClaude ? 'claude' : (isGpt ? 'gpt' : 'unknown')),\n status: 'active',\n release_date: '2026-05-26',\n // Root-level properties for OpenCode v2 cache compatibility\n reasoning: simple.reasoning,\n attachment: simple.attachment,\n tool_call: simple.toolCall,\n temperature: true,\n modalities: {\n input: [\n 'text',\n ...(simple.attachment ? ['image', 'pdf'] : []),\n ...(!isClaude && !isGpt ? ['audio', 'video'] : [])\n ],\n output: ['text']\n },\n capabilities: {\n temperature: true,\n reasoning: simple.reasoning,\n attachment: simple.attachment,\n toolcall: simple.toolCall,\n input: {\n text: true,\n image: simple.attachment,\n pdf: simple.attachment,\n audio: !isClaude && !isGpt,\n video: !isClaude && !isGpt\n },\n output: {\n text: true,\n image: false,\n pdf: false,\n audio: false,\n video: false\n },\n interleaved: false\n },\n cost: {\n input: simple.cost?.input ?? 0,\n output: simple.cost?.output ?? 0,\n cache: {\n read: simple.cost?.cache?.read ?? 0,\n write: simple.cost?.cache?.write ?? 0\n }\n },\n limit: {\n context: simple.maxTokens,\n output: simple.maxOutputTokens\n },\n options: {\n description: simple.description\n },\n headers: {},\n variants\n } as ProviderModel & Record;\n};\n\nconst STATIC_MODELS: Record = {};\nfor (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) {\n STATIC_MODELS[modelId] = buildModelFromSimple(modelId, simple);\n}\n\nfunction getSafeHeader(headers: unknown, key: string): string | undefined {\n if (!headers) {\n return undefined;\n }\n const targetKey = key.toLowerCase();\n\n if (typeof (headers as any).get === 'function') {\n try {\n return (headers as any).get(targetKey) || undefined;\n } catch {\n // Fallback in case get throws\n }\n }\n\n if (Array.isArray(headers)) {\n const found = headers.find((item) => {\n if (Array.isArray(item) && typeof item[0] === 'string') {\n return item[0].toLowerCase() === targetKey;\n }\n return false;\n });\n return found ? String(found[1]) : undefined;\n }\n\n if (typeof headers === 'object') {\n const foundKey = Object.keys(headers).find(k => k.toLowerCase() === targetKey);\n return foundKey ? ((headers as Record)[foundKey] !== undefined ? String((headers as Record)[foundKey]) : undefined) : undefined;\n }\n\n return undefined;\n}\n\nfunction setSafeHeaders(initHeaders: unknown, newHeaders: Record): unknown {\n if (typeof globalThis.Headers !== 'undefined') {\n const headers = new globalThis.Headers((initHeaders as any) ?? {});\n for (const [k, v] of Object.entries(newHeaders)) {\n headers.set(k, v);\n }\n return headers;\n }\n\n if (Array.isArray(initHeaders)) {\n const nextHeaders = [...initHeaders];\n for (const [k, v] of Object.entries(newHeaders)) {\n const idx = nextHeaders.findIndex(item => Array.isArray(item) && typeof item[0] === 'string' && item[0].toLowerCase() === k.toLowerCase());\n if (idx !== -1) {\n nextHeaders[idx] = [k, v];\n } else {\n nextHeaders.push([k, v]);\n }\n }\n return nextHeaders;\n }\n\n const nextHeaders: Record = {};\n if (initHeaders && typeof initHeaders === 'object') {\n for (const [k, v] of Object.entries(initHeaders)) {\n nextHeaders[k] = String(v);\n }\n }\n for (const [k, v] of Object.entries(newHeaders)) {\n const existingKey = Object.keys(nextHeaders).find(key => key.toLowerCase() === k.toLowerCase());\n if (existingKey) {\n nextHeaders[existingKey] = v;\n } else {\n nextHeaders[k] = v;\n }\n }\n return nextHeaders;\n}\n\nfunction resolveModelTier(baseModelId: string, init?: RequestInit): string {\n const parts = baseModelId.split('@');\n const base = parts[0] || '';\n const suffixTier = parts[1]?.toLowerCase();\n\n const mapping = TIER_MAPPING[base];\n if (!mapping) {\n return baseModelId;\n }\n\n const headerTier = getSafeHeader(init?.headers, 'x-agy-tier')?.toLowerCase() || null;\n const requestedTier = headerTier || suffixTier;\n\n // Resolve to specific tier or default to medium\n if (requestedTier && Object.prototype.hasOwnProperty.call(mapping, requestedTier)) {\n return mapping[requestedTier] || baseModelId;\n }\n\n return mapping['medium'] ?? mapping['high'];\n}\n\n/**\n * Registers the Agy OAuth provider for Opencode.\n */\nexport const AgyCLIOAuthPlugin = async ({ client }: PluginContext): Promise => {\n let latestConfig: Config | undefined;\n\n // Dynamically update STATIC_MODELS with latest pricing from models.dev\n updateStaticModelsWithPricing(STATIC_MODELS);\n\n const getModelsList = (provider: ProviderV2): Record => {\n const userModels = provider.models || {};\n const clonedStaticModels = JSON.parse(JSON.stringify(STATIC_MODELS));\n const strictModels: Record = {};\n\n for (const [modelId, modelDetails] of Object.entries(clonedStaticModels as Record)) {\n const existing = (userModels[modelId] || {}) as Partial;\n strictModels[modelId] = {\n ...modelDetails,\n ...existing,\n reasoning: (existing as any).reasoning ?? (modelDetails as any).reasoning,\n attachment: (existing as any).attachment ?? (modelDetails as any).attachment,\n tool_call: (existing as any).tool_call ?? (modelDetails as any).tool_call,\n temperature: (existing as any).temperature ?? (modelDetails as any).temperature,\n modalities: {\n ...((modelDetails as any).modalities || {}),\n ...((existing as any).modalities || {})\n },\n capabilities: {\n ...(modelDetails.capabilities || {}),\n ...(existing.capabilities || {}),\n input: {\n ...(modelDetails.capabilities?.input || {}),\n ...(existing.capabilities?.input || {})\n },\n output: {\n ...(modelDetails.capabilities?.output || {}),\n ...(existing.capabilities?.output || {})\n }\n }\n } as ProviderModel;\n }\n\n provider.models = strictModels;\n\n if (latestConfig && latestConfig.provider && latestConfig.provider[AGY_PROVIDER_ID]) {\n latestConfig.provider[AGY_PROVIDER_ID].models = provider.models;\n }\n normalizeProviderModelCosts(provider);\n\n return provider.models;\n };\n\n try {\n initDiskSignatureCache({\n enabled: true,\n memory_ttl_seconds: 3600,\n disk_ttl_seconds: 86400,\n write_interval_seconds: 30\n });\n } catch (e) {\n console.warn(`[Agy Auth] initDiskSignatureCache failed, running without signature disk cache: ${e instanceof Error ? e.message : e}`);\n }\n\n try {\n initTurnStateTracker();\n } catch (e) {\n console.warn(`[Agy Auth] initTurnStateTracker failed, running without turn-state tracking: ${e instanceof Error ? e.message : e}`);\n }\n\n try {\n initCooldownPersistence();\n } catch (e) {\n console.warn(`[Agy Auth] initCooldownPersistence failed, running without cooldown disk persistence: ${e instanceof Error ? e.message : e}`);\n }\n\n const resolveLatestConfiguredProjectId = async (provider?: Provider): Promise => {\n const configProjectId = (await resolveConfiguredProjectIdFromClient(client)) ?? latestAgyConfiguredProjectId;\n const resolvedProjectId = resolveConfiguredProjectId({\n provider,\n configProjectId\n });\n latestAgyConfiguredProjectId = resolvedProjectId;\n return resolvedProjectId;\n };\n\n return {\n config: async (config) => {\n latestConfig = config;\n latestAgyConfiguredProjectId = resolveConfiguredProjectIdFromConfig(config);\n config.command = config.command || {};\n config.command[AGY_QUOTA_COMMAND] = {\n description: 'Show Agy Code Assist quota usage',\n template: AGY_QUOTA_COMMAND_TEMPLATE\n };\n config.command[AGY_QUOTA_SUMMARY_COMMAND] = {\n description: 'Show Agy Code Assist quota summary with weekly and 5-hour limits',\n template: AGY_QUOTA_SUMMARY_COMMAND_TEMPLATE\n };\n\n // Dynamically registers the google-agy provider config to make it work seamlessly without manual user mapping.\n config.provider = config.provider || {};\n config.provider[AGY_PROVIDER_ID] = {\n npm: '@ai-sdk/google',\n name: 'Antigravity CLI',\n options: {},\n models: {},\n ...config.provider[AGY_PROVIDER_ID]\n };\n\n // Merge custom config from the user with the static defaults\n config.provider[AGY_PROVIDER_ID].models = getModelsList(config.provider[AGY_PROVIDER_ID] as ProviderV2);\n },\n tool: {\n [AGY_QUOTA_TOOL_NAME]: createAgyQuotaTool({\n client,\n getAuthResolver: () => latestAgyAuthResolver,\n getConfiguredProjectId: () => latestAgyConfiguredProjectId,\n getUserAgentModel: () => latestAgyUserAgentModel\n }),\n [AGY_QUOTA_SUMMARY_TOOL_NAME]: createAgyQuotaSummaryTool({\n client,\n getAuthResolver: () => latestAgyAuthResolver,\n getConfiguredProjectId: () => latestAgyConfiguredProjectId,\n getUserAgentModel: () => latestAgyUserAgentModel\n })\n },\n auth: {\n provider: AGY_PROVIDER_ID,\n loader: async (getAuth: GetAuth, provider: Provider): Promise => {\n latestAgyAuthResolver = getAuth;\n const auth = await getAuth();\n if (!isOAuthAuth(auth)) {\n return null;\n }\n\n const configuredProjectId = await resolveLatestConfiguredProjectId(provider);\n normalizeProviderModelCosts(provider);\n const thinkingConfigDefaults = resolveThinkingConfigDefaults(provider);\n\n return {\n apiKey: '',\n async fetch(input: RequestInfo, init?: RequestInit) {\n const isGL = isGenerativeLanguageRequest(input);\n const isInternal = toUrlString(input).includes('cloudcode-pa.googleapis.com');\n\n if (!isGL && !isInternal) {\n return agyFetch(input, init);\n }\n\n const latestAuth = await getAuth();\n if (!isOAuthAuth(latestAuth)) {\n return agyFetch(input, init);\n }\n\n let authRecord = resolveCachedAuth(latestAuth);\n if (accessTokenExpired(authRecord)) {\n const refreshed = await refreshAccessToken(authRecord, client);\n if (!refreshed) {\n return agyFetch(input, init);\n }\n authRecord = refreshed;\n }\n\n if (!authRecord.access) {\n return agyFetch(input, init);\n }\n\n const configuredProjectId = await resolveLatestConfiguredProjectId(provider);\n\n if (isInternal) {\n const hasAuth = getSafeHeader(init?.headers, 'Authorization') !== undefined;\n if (hasAuth) {\n return agyFetch(input, init);\n }\n\n const userAgent = buildAgyCliUserAgent(latestAgyUserAgentModel);\n const headers = setSafeHeaders(init?.headers, {\n 'Authorization': `Bearer ${authRecord.access}`,\n 'User-Agent': userAgent\n });\n\n if (configuredProjectId) {\n simulateClientBackgroundTraffic(authRecord.access, configuredProjectId, latestAgyUserAgentModel);\n }\n\n return agyFetch(input, {\n ...init,\n headers: headers as any\n });\n }\n const requestTarget = parseGenerativeLanguageRequest(input);\n const requestUserAgentModel = requestTarget?.effectiveModel;\n if (requestUserAgentModel) {\n latestAgyUserAgentModel = requestUserAgentModel;\n }\n const projectContext = await ensureProjectContextOrThrow(\n authRecord,\n client,\n configuredProjectId,\n requestUserAgentModel\n );\n await maybeShowAgyTestToast(client, projectContext.effectiveProjectId);\n\n const originalRequestedModel = parseGenerativeLanguageRequest(input)?.effectiveModel;\n let modifiedInput = input;\n if (isGL && originalRequestedModel) {\n const originalBase = originalRequestedModel.replace('google-agy/', '');\n const resolvedBase = resolveModelTier(originalBase, init);\n if (originalBase !== resolvedBase) {\n if (typeof modifiedInput === 'string') {\n modifiedInput = modifiedInput.replace(`models/${originalBase}`, `models/${resolvedBase}`);\n } else if (typeof Request !== 'undefined' && modifiedInput instanceof Request) {\n const newUrl = modifiedInput.url.replace(`models/${originalBase}`, `models/${resolvedBase}`);\n modifiedInput = new Request(newUrl, modifiedInput);\n }\n }\n }\n\n const parts = parseRefreshParts(authRecord.refresh);\n const transformed = prepareAgyRequest(\n modifiedInput,\n init,\n authRecord.access,\n projectContext.effectiveProjectId,\n thinkingConfigDefaults\n );\n const chatLogger = createChatLogger();\n if (chatLogger) {\n chatLogger.logRequest(\n toUrlString(transformed.request),\n transformed.init.method || 'GET',\n transformed.init.headers,\n transformed.init.body\n );\n }\n\n const response = await fetchWithRetry(transformed.request, transformed.init);\n if (response.ok && authRecord.access && projectContext.effectiveProjectId) {\n simulateClientBackgroundTraffic(\n authRecord.access,\n projectContext.effectiveProjectId,\n requestUserAgentModel\n );\n }\n await maybeShowAgyCapacityToast(\n client,\n response,\n projectContext.effectiveProjectId,\n transformed.requestedModel\n );\n return transformAgyResponse(\n response,\n transformed.streaming,\n null,\n transformed.requestedModel,\n transformed.sessionId,\n chatLogger\n );\n }\n };\n },\n methods: [\n {\n label: 'Google OAuth (Antigravity CLI)',\n type: 'oauth',\n authorize: createOAuthAuthorizeMethod({\n client,\n getConfiguredProjectId: () => resolveLatestConfiguredProjectId(),\n getUserAgentModel: () => latestAgyUserAgentModel\n })\n },\n {\n label: 'Manually enter API Key',\n type: 'api'\n }\n ]\n },\n provider: {\n id: AGY_PROVIDER_ID,\n models: async (provider: any, ctx: any): Promise => {\n let auth = ctx?.auth;\n if (!auth && latestAgyAuthResolver) {\n try {\n auth = await latestAgyAuthResolver();\n } catch (e) {\n const errStr = e instanceof Error ? e.stack || e.message : String(e);\n console.warn(`[Agy Auth] Failed to resolve auth from resolver in models hook: ${errStr}`);\n }\n }\n\n if (!auth || !isOAuthAuth(auth)) {\n return {\n 'login-required': {\n name: 'No models available'\n }\n };\n }\n\n const models = getModelsList(provider);\n // Ensure dynamic pricing costs are strictly enforced to override any cached $0 fallbacks\n for (const [modelId, model] of Object.entries(models)) {\n if (STATIC_MODELS[modelId] && STATIC_MODELS[modelId].cost) {\n model.cost = STATIC_MODELS[modelId].cost;\n }\n }\n return models;\n }\n } as any\n };\n};\n\nexport const GoogleOAuthPlugin = AgyCLIOAuthPlugin;\n\nfunction normalizeProviderModelCosts(provider: Provider | ProviderV2): void {\n if (!provider?.models || typeof provider.models !== 'object') {\n return;\n }\n for (const [modelId, model] of Object.entries(provider.models)) {\n if (!model || typeof model !== 'object') {\n continue;\n }\n const existingCost = model.cost;\n const isValidCost =\n existingCost &&\n typeof existingCost === 'object' &&\n typeof existingCost.input === 'number' &&\n typeof existingCost.output === 'number';\n const normalizedCost = {\n input: isValidCost ? existingCost.input : 0,\n output: isValidCost ? existingCost.output : 0,\n cache: {\n read:\n isValidCost &&\n typeof existingCost.cache === 'object' &&\n existingCost.cache !== null &&\n typeof (existingCost.cache as { read?: number }).read === 'number'\n ? (existingCost.cache as { read: number }).read\n : 0,\n write:\n isValidCost &&\n typeof existingCost.cache === 'object' &&\n existingCost.cache !== null &&\n typeof (existingCost.cache as { write?: number }).write === 'number'\n ? (existingCost.cache as { write: number }).write\n : 0\n }\n };\n model.cost = normalizedCost;\n }\n}\n\nfunction resolveThinkingConfigDefaults(provider: Provider): ThinkingConfigDefaults | undefined {\n const providerOptions =\n provider && typeof provider === 'object'\n ? ((provider as { options?: Record }).options ?? undefined)\n : undefined;\n const providerThinkingConfig = providerOptions?.thinkingConfig;\n\n const modelThinkingConfigByModel: Record = {};\n for (const [modelId, model] of Object.entries(provider?.models ?? {})) {\n if (!model || typeof model !== 'object') {\n continue;\n }\n const modelOptions = (model as { options?: Record }).options;\n if (modelOptions && typeof modelOptions === 'object' && 'thinkingConfig' in modelOptions) {\n modelThinkingConfigByModel[modelId] = modelOptions.thinkingConfig;\n }\n }\n\n if (providerThinkingConfig === undefined && Object.keys(modelThinkingConfigByModel).length === 0) {\n return undefined;\n }\n\n return {\n provider: providerThinkingConfig,\n models: modelThinkingConfigByModel\n };\n}\n\nasync function ensureProjectContextOrThrow(\n authRecord: OAuthAuthDetails,\n client: PluginClient,\n configuredProjectId?: string,\n userAgentModel?: string\n) {\n try {\n return await ensureProjectContext(authRecord, client, configuredProjectId, userAgentModel);\n } catch (error) {\n if (error instanceof Error) {\n console.warn(`[Agy Auth] ensureProjectContextOrThrow error: ${error.message}`);\n }\n throw error;\n }\n}\n\nfunction toUrlString(value: RequestInfo): string {\n if (typeof value === 'string') {\n return value;\n }\n const candidate = (value as Request).url;\n if (candidate) {\n return candidate;\n }\n return value.toString();\n}\n","import { AgyCLIOAuthPlugin, GoogleOAuthPlugin } from \"./src/plugin\";\n\nexport { AgyCLIOAuthPlugin, GoogleOAuthPlugin };\n\nexport default {\n id: \"@anthonyhaussman/opencode-agy-auth\",\n server: AgyCLIOAuthPlugin,\n};\n"],"mappings":"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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAAC;AAAA,EAAA;AAAA,gBAAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":["cached","Buffer","Buffer","Buffer","decode","encode","encode","decode","encoder","hash","url","error","userInfo","version","platform","arch","url","seconds","error","normalize","error","existsSync","readFileSync","writeFileSync","join","tmpdir","platform","url","error","delayMs","url","error","wait","url","buildCodeAssistHeaders","error","wait","error","cached","promise","normalizeProjectId","config","error","message","url","platform","existsSync","mkdirSync","readFileSync","writeFileSync","renameSync","unlinkSync","join","dirname","homedir","tmpdir","getConfigDir","platform","config","cached","config","existsSync","mkdirSync","readFileSync","writeFileSync","renameSync","unlinkSync","join","dirname","homedir","tmpdir","createHash","hash","decoder","encoder","WRITE_THROTTLE_MS","getConfigDir","platform","join","homedir","existsSync","readFileSync","record","dirname","mkdirSync","tmpdir","writeFileSync","renameSync","unlinkSync","_default","base64","base64url","bigint","boolean","_catch","cidrv4","cidrv6","core_exports","cuid","cuid2","date","decode","decodeAsync","e164","email","emoji","encode","encodeAsync","_enum","guid","hex","hostname","ipv4","ipv6","ksuid","nanoid","_null","nullish","number","parse","parseAsync","safeDecode","safeDecodeAsync","safeEncode","safeEncodeAsync","safeParse","safeParseAsync","string","ulid","_undefined","uuid","_void","xid","core_exports","_emoji","_null","_undefined","decode","encode","initializer","isObject","array","set","object","Class","config","base64","base64url","hex","error","issue","encode","decode","version","time","inst","result","checks","checkResult","canary","result","_","url","inst","base64","r","isObject","allowsEval","results","map","left","right","keyResult","valueResult","output","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","issue","parsedType","error","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","parsedType","error","issue","error","parsedType","issue","error","parsedType","issue","parsedType","error","issue","error","parsedType","issue","error","parsedType","issue","parsedType","number","error","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","parsedType","error","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","Class","_emoji","_undefined","_null","Class","issue","codec","json","file","id","schema","params","gen","date","datetime","duration","time","datetime","date","time","duration","initializer","issue","issues","parse","parseAsync","safeParse","safeParseAsync","encode","decode","encodeAsync","decodeAsync","safeEncode","safeDecode","safeEncodeAsync","safeDecodeAsync","def","parse","safeParse","parseAsync","safeParseAsync","encode","decode","encodeAsync","decodeAsync","safeEncode","safeDecode","safeEncodeAsync","safeDecodeAsync","check","_default","_catch","_emoji","datetime","date","time","duration","string","email","guid","uuid","emoji","nanoid","cuid","cuid2","ulid","xid","ksuid","ipv4","ipv6","cidrv4","cidrv6","base64","base64url","e164","hostname","hex","number","boolean","bigint","_undefined","_null","_void","date","_enum","issue","output","nullish","_default","_catch","string","number","boolean","_null","map","ZodFirstPartyTypeKind","bigint","boolean","date","number","string","string","number","boolean","bigint","date","error","error","error","os","url","error","arch","platform","randomUUID","CLOUDCODE_DOMAINS","config","record","error","CLOUDCODE_DOMAINS","parseRetryDelayValue","seconds","randomUUID","url","readString","randomUUID","tool","randomUUID","wrappedBody","userPromptId","sessionId","requestId","toolMapper","contents","error","isRecord","config","tool","error","existsSync","mkdirSync","join","error","url","decoder","nextHeaders","config","configuredProjectId","error"]} \ No newline at end of file +{"version":3,"sources":["../src/plugin/pricing.ts","../src/constants.ts","../src/fetch.ts","../src/plugin/oauth-authorize.ts","../node_modules/jose/dist/node/esm/runtime/base64url.js","../node_modules/jose/dist/node/esm/lib/buffer_utils.js","../node_modules/jose/dist/node/esm/util/base64url.js","../node_modules/@openauthjs/openauth/dist/esm/pkce.js","../src/sdk/oauth.ts","../src/sdk/activity-request-id.ts","../src/sdk/user-agent.ts","../src/sdk/agy-cli-version.ts","../src/sdk/fetch_quota.ts","../src/plugin/auth.ts","../src/sdk/retry/quota.ts","../src/sdk/retry/helpers.ts","../src/sdk/retry/cooldown-store.ts","../src/sdk/retry/index.ts","../src/sdk/terminal-hyperlink.ts","../src/plugin/project/types.ts","../src/plugin/project/utils.ts","../src/sdk/fetch_project.ts","../src/plugin/project/context.ts","../src/plugin/provider.ts","../src/plugin/cache.ts","../src/sdk/cache/signature-cache.ts","../src/sdk/request/turn-state-tracker.ts","../src/sdk/request/thinking.ts","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/index.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/core.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/util.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/errors.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/parse.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/regexes.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/checks.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/doc.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/versions.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/schemas.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/index.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ar.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/az.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/be.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ca.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/cs.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/da.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/de.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/en.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/eo.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/es.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/fa.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/fi.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/fr.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/fr-CA.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/he.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/hu.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/id.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/is.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/it.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ja.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ka.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/km.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/kh.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ko.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/lt.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/mk.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ms.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/nl.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/no.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ota.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ps.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/pl.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/pt.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ru.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/sl.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/sv.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ta.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/th.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/tr.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/uk.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ua.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/ur.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/vi.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/zh-CN.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/zh-TW.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/locales/yo.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/registries.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/api.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/to-json-schema.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/core/json-schema.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/iso.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/errors.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/parse.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/schemas.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/compat.js","../node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/coerce.js","../node_modules/@opencode-ai/plugin/dist/tool.js","../src/plugin/token.ts","../src/plugin/quota-utils.ts","../src/plugin/quota.ts","../src/plugin/quota-summary.ts","../src/plugin/notify.ts","../src/plugin/traffic.ts","../src/sdk/request/prepare.ts","../models.json","../src/sdk/request-helpers/types.ts","../src/sdk/request-helpers/thinking.ts","../src/sdk/request-helpers/parsing.ts","../src/sdk/request-helpers/errors.ts","../src/sdk/request/identifiers.ts","../src/sdk/request/shared.ts","../src/sdk/request/openai.ts","../src/sdk/request/tool-mapper.ts","../src/sdk/request/response.ts","../src/sdk/chat-logger.ts","../src/plugin/headers.ts","../src/plugin/tier.ts","../src/plugin.ts","../src/plugin/types.ts","../src/plugin/v2-storage.ts","../src/plugin/v2-fetch.ts","../src/plugin/v2-session.ts","../src/plugin/v2.ts","../index.ts"],"sourcesContent":["import { tmpdir, userInfo } from 'node:os';\nimport { join } from 'node:path';\nimport { existsSync, readFileSync, writeFileSync, statSync } from 'node:fs';\nimport type { ProviderModel } from './types';\n\n// Using fetch API (available in Node.js 18+)\nconst PRICING_API_URL = 'https://models.dev/api.json';\nconst CACHE_FILE_NAME = 'agy-pricing-cache.json';\nconst CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nlet memoryCache: ModelsDevResponse | null = null;\n\ninterface ModelsDevCost {\n input?: number;\n output?: number;\n cache_read?: number;\n cache_write?: number;\n [key: string]: any;\n}\n\ninterface ModelsDevModel {\n cost?: ModelsDevCost;\n [key: string]: any;\n}\n\ninterface ModelsDevProvider {\n models: Record;\n [key: string]: any;\n}\n\ntype ModelsDevResponse = Record;\n\nfunction getCachePath(): string {\n try {\n return join(tmpdir(), `${CACHE_FILE_NAME}-${userInfo().uid}`);\n } catch (err) {\n return join(tmpdir(), CACHE_FILE_NAME);\n }\n}\n\nfunction loadCachedPricing(): ModelsDevResponse | null {\n try {\n const cachePath = getCachePath();\n if (!existsSync(cachePath)) return null;\n\n const stats = statSync(cachePath);\n if (Date.now() - stats.mtimeMs > CACHE_TTL_MS) {\n return null; // Cache expired\n }\n\n const data = readFileSync(cachePath, 'utf-8');\n return JSON.parse(data) as ModelsDevResponse;\n } catch (err) {\n return null;\n }\n}\n\nfunction savePricingCache(data: ModelsDevResponse): void {\n try {\n const cachePath = getCachePath();\n writeFileSync(cachePath, JSON.stringify(data), 'utf-8');\n } catch (err) {\n // Ignore cache write errors\n }\n}\n\nasync function fetchPricing(): Promise {\n if (memoryCache) {\n return memoryCache;\n }\n\n const cached = loadCachedPricing();\n if (cached) {\n memoryCache = cached;\n return cached;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 2000);\n\n try {\n const response = await fetch(PRICING_API_URL, { signal: controller.signal });\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return null;\n }\n const data = (await response.json()) as ModelsDevResponse;\n if (data) {\n savePricingCache(data);\n memoryCache = data;\n return data;\n }\n } catch (err) {\n clearTimeout(timeoutId);\n // Silent fail, will fallback to 0 or existing costs\n }\n\n return null;\n}\n\nfunction determineProvider(modelId: string): string | null {\n const lower = modelId.toLowerCase();\n if (lower.startsWith('gemini-') || lower.startsWith('gemma-')) return 'google';\n if (lower.startsWith('claude-')) return 'anthropic';\n if (lower.startsWith('gpt-') || lower.startsWith('o1') || lower.startsWith('o3')) return 'openai';\n return null;\n}\n\n/**\n * Mutates the STATIC_MODELS object by injecting dynamic costs from models.dev\n */\nexport function updateStaticModelsWithPricing(\n staticModels: Record\n): void {\n fetchPricing().then((pricingData) => {\n if (!pricingData) return;\n\n for (const [modelId, modelObj] of Object.entries(staticModels)) {\n const provider = determineProvider(modelId);\n if (!provider || !pricingData[provider]?.models) continue;\n\n // Remove the \"-thinking\" suffix which is custom for opencode if the base model exists\n let lookupId = modelId;\n if (lookupId.endsWith('-thinking') && !pricingData[provider].models[lookupId]) {\n lookupId = lookupId.replace('-thinking', '');\n }\n\n const apiModel = pricingData[provider].models[lookupId];\n if (apiModel && apiModel.cost) {\n const apiCost = apiModel.cost;\n\n modelObj.cost = {\n input: apiCost.input ?? modelObj.cost?.input ?? 0,\n output: apiCost.output ?? modelObj.cost?.output ?? 0,\n cache: {\n read: apiCost.cache_read ?? modelObj.cost?.cache?.read ?? 0,\n write: apiCost.cache_write ?? modelObj.cost?.cache?.write ?? 0\n }\n };\n }\n }\n }).catch(() => {\n // Ignore errors to prevent unhandled rejections\n });\n}\n","// Provider identifier shared between the plugin loader and credential storage.\nexport const AGY_PROVIDER_ID = 'google-agy';\n\n// Antigravity CLI (Daily) constant definitions.\nexport const AGY_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';\nexport const AGY_TOKEN_URL = 'https://oauth2.googleapis.com/token';\nexport const AGY_CLIENT_ID = 'REDACTED_GOOGLE_OAUTH_CLIENT_ID.apps.googleusercontent.com';\nexport const AGY_CLIENT_SECRET = 'REDACTED_GOOGLE_OAUTH_CLIENT_SECRET';\nexport const AGY_SCOPES: readonly string[] = [\n 'openid',\n 'https://www.googleapis.com/auth/cloud-platform',\n 'https://www.googleapis.com/auth/userinfo.email',\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'https://www.googleapis.com/auth/cclog',\n 'https://www.googleapis.com/auth/experimentsandconfigs'\n];\nexport const AGY_REDIRECT_URI = 'https://antigravity.google/oauth-callback';\nexport const AGY_CODE_ASSIST_ENDPOINT = process.env.OPENCODE_AGY_ENDPOINT || 'https://daily-cloudcode-pa.googleapis.com';\nexport const AGY_GENERATIVE_LANGUAGE_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta';\n","type BunRequestInit = RequestInit & { proxy?: string };\n\nexport function agyFetch(input: RequestInfo | URL, init?: RequestInit): Promise {\n const proxy = process.env.OPENCODE_AGY_AUTH_PROXY;\n if (!proxy) {\n return fetch(input, init);\n }\n\n return fetch(input, {\n ...(init ?? {}),\n proxy\n } as BunRequestInit);\n}\n","import { spawn } from 'node:child_process';\n\nimport { authorizeAgy, exchangeAgyWithVerifier } from '../sdk/oauth';\nimport type { AgyTokenExchangeResult } from '../sdk/oauth';\nimport { resolveProjectContextFromAccessToken } from './project';\nimport { resolveConfiguredProjectId } from './provider';\nimport { formatRefreshParts } from './auth';\nimport type { OAuthAuthDetails, PluginClient } from './types';\n\n\n/**\n * Builds the OAuth authorization callback for the plugin authentication method.\n */\nexport function createOAuthAuthorizeMethod(options?: {\n client?: PluginClient;\n getConfiguredProjectId?: () => Promise | string | undefined;\n getUserAgentModel?: () => Promise | string | undefined;\n openBrowser?: (url: string) => void;\n}): () => Promise<{\n url: string;\n instructions: string;\n method: 'code';\n callback: (callbackUrl: string) => Promise;\n}> {\n return async () => {\n const maybeHydrateProjectId = async (\n result: AgyTokenExchangeResult\n ): Promise => {\n if (result.type !== 'success' || !result.access) {\n return result;\n }\n\n const configuredProjectId = resolveConfiguredProjectId({\n configProjectId: await options?.getConfiguredProjectId?.()\n });\n\n try {\n const initialRefresh = formatRefreshParts({\n refreshToken: result.refresh\n });\n\n const authSnapshot = {\n type: 'oauth',\n refresh: initialRefresh,\n access: result.access,\n expires: result.expires\n } satisfies OAuthAuthDetails;\n\n const projectContext = await resolveProjectContextFromAccessToken(\n authSnapshot,\n result.access,\n configuredProjectId,\n undefined,\n await options?.getUserAgentModel?.()\n );\n\n return projectContext.auth.refresh !== initialRefresh\n ? { ...result, refresh: projectContext.auth.refresh }\n : { ...result, refresh: initialRefresh };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.warn(`[OAuth] Project resolution skipped: ${message}`);\n if (options?.client?.tui?.showToast) {\n const message = error instanceof Error ? error.message : String(error);\n options.client.tui.showToast({\n body: {\n title: \"Failed to bind project context\",\n message: `Authorized successfully but failed to bind project, real models will be unavailable: ${message}`,\n variant: \"warning\",\n duration: 15000\n }\n }).catch(() => {});\n }\n const initialRefresh = formatRefreshParts({\n refreshToken: result.refresh\n });\n return { ...result, refresh: initialRefresh };\n }\n };\n\n const isHeadless = !!(\n process.env.SSH_CONNECTION ||\n process.env.SSH_CLIENT ||\n process.env.SSH_TTY ||\n process.env.OPENCODE_HEADLESS\n );\n\n const authorization = await authorizeAgy();\n if (!isHeadless) {\n if (options?.openBrowser) {\n options.openBrowser(authorization.url);\n } else {\n openBrowserUrl(authorization.url);\n }\n }\n\n return {\n url: authorization.url,\n instructions: isHeadless\n ? 'Headless/SSH environment detected. Browser auto-open skipped. Please manually open the following URL in a browser on your local machine to authorize:\\n\\n' + authorization.url + '\\n\\nAfter authorization, the page will redirect to https://antigravity.google/oauth-callback?code=... . Copy the full redirect URL from your browser address bar, or just the code parameter value, and paste it into the input box below.\\n\\nNote: If you are not in a headless environment, unset OPENCODE_HEADLESS or run without SSH to enable browser auto-open.'\n : 'Please complete Google account authorization in your browser. After authorization, the page will redirect to https://antigravity.google/oauth-callback?code=... . Please copy the full redirect URL from your browser address bar, or just the code parameter value, and paste it into the input box below:',\n method: 'code',\n callback: async (callbackUrl: string): Promise => {\n try {\n const { code, state } = parseOAuthCallbackInput(callbackUrl);\n if (!code) {\n return { type: 'failed', error: 'Missing authorization code in callback input' };\n }\n if (state && state !== authorization.state) {\n return { type: 'failed', error: 'State mismatch in callback input (possible CSRF attempt)' };\n }\n const exchangeResult = await exchangeAgyWithVerifier(code, authorization.verifier);\n return await maybeHydrateProjectId(exchangeResult);\n } catch (error) {\n return {\n type: 'failed',\n error: error instanceof Error ? error.message : 'Unknown error'\n };\n }\n }\n };\n };\n}\n\nexport function parseOAuthCallbackInput(input: string): { code?: string; state?: string } {\n const trimmed = input.trim();\n if (!trimmed) {\n return {};\n }\n\n if (/^https?:\\/\\//i.test(trimmed)) {\n try {\n const url = new URL(trimmed);\n return {\n code: url.searchParams.get('code') || undefined,\n state: url.searchParams.get('state') || undefined\n };\n } catch {\n return {};\n }\n }\n\n const candidate = trimmed.startsWith('?') ? trimmed.slice(1) : trimmed;\n if (candidate.includes('=')) {\n const params = new URLSearchParams(candidate);\n const code = params.get('code') || undefined;\n const state = params.get('state') || undefined;\n if (code || state) {\n return { code, state };\n }\n }\n\n return { code: trimmed };\n}\n\nexport function setOpenBrowserLauncherForTesting(launcher: typeof openBrowserLauncher | null): void {\n openBrowserLauncher = launcher ?? defaultOpenBrowserLauncher;\n}\n\nexport function defaultOpenBrowserLauncher(command: string, args: string[]) {\n const child = spawn(command, args, {\n stdio: 'ignore',\n detached: true\n });\n child.unref?.();\n}\n\nlet openBrowserLauncher = defaultOpenBrowserLauncher;\n\nfunction openBrowserUrl(url: string): void {\n // If no mock launcher is provided during tests, skip opening real desktop browser\n if ((process.env.NODE_ENV === 'test' || process.env.VITEST) && openBrowserLauncher === defaultOpenBrowserLauncher) {\n return;\n }\n try {\n const platform = process.platform;\n const command =\n platform === 'darwin' ? 'open' : platform === 'win32' ? 'rundll32' : 'xdg-open';\n const args = platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];\n openBrowserLauncher(command, args);\n } catch {}\n}\n","import { Buffer } from 'node:buffer';\nimport { decoder } from '../lib/buffer_utils.js';\nfunction normalize(input) {\n let encoded = input;\n if (encoded instanceof Uint8Array) {\n encoded = decoder.decode(encoded);\n }\n return encoded;\n}\nconst encode = (input) => Buffer.from(input).toString('base64url');\nexport const decodeBase64 = (input) => new Uint8Array(Buffer.from(input, 'base64'));\nexport const encodeBase64 = (input) => Buffer.from(input).toString('base64');\nexport { encode };\nexport const decode = (input) => new Uint8Array(Buffer.from(normalize(input), 'base64url'));\n","import digest from '../runtime/digest.js';\nexport const encoder = new TextEncoder();\nexport const decoder = new TextDecoder();\nconst MAX_INT32 = 2 ** 32;\nexport function concat(...buffers) {\n const size = buffers.reduce((acc, { length }) => acc + length, 0);\n const buf = new Uint8Array(size);\n let i = 0;\n for (const buffer of buffers) {\n buf.set(buffer, i);\n i += buffer.length;\n }\n return buf;\n}\nexport function p2s(alg, p2sInput) {\n return concat(encoder.encode(alg), new Uint8Array([0]), p2sInput);\n}\nfunction writeUInt32BE(buf, value, offset) {\n if (value < 0 || value >= MAX_INT32) {\n throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);\n }\n buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);\n}\nexport function uint64be(value) {\n const high = Math.floor(value / MAX_INT32);\n const low = value % MAX_INT32;\n const buf = new Uint8Array(8);\n writeUInt32BE(buf, high, 0);\n writeUInt32BE(buf, low, 4);\n return buf;\n}\nexport function uint32be(value) {\n const buf = new Uint8Array(4);\n writeUInt32BE(buf, value);\n return buf;\n}\nexport function lengthAndInput(input) {\n return concat(uint32be(input.length), input);\n}\nexport async function concatKdf(secret, bits, value) {\n const iterations = Math.ceil((bits >> 3) / 32);\n const res = new Uint8Array(iterations * 32);\n for (let iter = 0; iter < iterations; iter++) {\n const buf = new Uint8Array(4 + secret.length + value.length);\n buf.set(uint32be(iter + 1));\n buf.set(secret, 4);\n buf.set(value, 4 + secret.length);\n res.set(await digest('sha256', buf), iter * 32);\n }\n return res.slice(0, bits >> 3);\n}\n","import * as base64url from '../runtime/base64url.js';\nexport const encode = base64url.encode;\nexport const decode = base64url.decode;\n","// src/pkce.ts\nimport { base64url } from \"jose\";\nfunction generateVerifier(length) {\n const buffer = new Uint8Array(length);\n crypto.getRandomValues(buffer);\n return base64url.encode(buffer);\n}\nasync function generateChallenge(verifier, method) {\n if (method === \"plain\")\n return verifier;\n const encoder = new TextEncoder;\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\n return base64url.encode(new Uint8Array(hash));\n}\nasync function generatePKCE(length = 64) {\n if (length < 43 || length > 128) {\n throw new Error(\"Code verifier length must be between 43 and 128 characters\");\n }\n const verifier = generateVerifier(length);\n const challenge = await generateChallenge(verifier, \"S256\");\n return {\n verifier,\n challenge,\n method: \"S256\"\n };\n}\nasync function validatePKCE(verifier, challenge, method = \"S256\") {\n const generatedChallenge = await generateChallenge(verifier, method);\n return generatedChallenge === challenge;\n}\nexport {\n validatePKCE,\n generatePKCE\n};\n","import { generatePKCE } from '@openauthjs/openauth/pkce';\nimport { randomBytes } from 'node:crypto';\n\nimport { AGY_CLIENT_ID, AGY_CLIENT_SECRET, AGY_REDIRECT_URI, AGY_SCOPES } from '../constants';\nimport { agyFetch } from '../fetch';\n\ninterface PkcePair {\n challenge: string;\n verifier: string;\n}\n\nexport interface AgyAuthorization {\n url: string;\n verifier: string;\n state: string;\n}\n\ninterface AgyTokenExchangeSuccess {\n type: 'success';\n refresh: string;\n access: string;\n expires: number;\n email?: string;\n}\n\ninterface AgyTokenExchangeFailure {\n type: 'failed';\n error: string;\n}\n\nexport type AgyTokenExchangeResult = AgyTokenExchangeSuccess | AgyTokenExchangeFailure;\n\ninterface AgyTokenResponse {\n access_token: string;\n expires_in: number;\n refresh_token: string;\n}\n\ninterface AgyUserInfo {\n email?: string;\n}\n\n/**\n * Builds the Agy OAuth authorization URL with PKCE.\n */\nexport async function authorizeAgy(): Promise {\n const pkce = (await generatePKCE()) as PkcePair;\n const state = randomBytes(32).toString('hex');\n\n const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');\n url.searchParams.set('client_id', AGY_CLIENT_ID);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('redirect_uri', AGY_REDIRECT_URI);\n url.searchParams.set('scope', AGY_SCOPES.join(' '));\n url.searchParams.set('code_challenge', pkce.challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n url.searchParams.set('access_type', 'offline');\n url.searchParams.set('prompt', 'consent');\n url.hash = 'opencode';\n\n return {\n url: url.toString(),\n verifier: pkce.verifier,\n state\n };\n}\n\n/**\n * Exchanges the authorization code for Agy using a known PKCE verifier.\n */\nexport async function exchangeAgyWithVerifier(code: string, verifier: string): Promise {\n try {\n return await exchangeAgyWithVerifierInternal(code, verifier);\n } catch (error) {\n return {\n type: 'failed',\n error: error instanceof Error ? error.message : 'Unknown error'\n };\n }\n}\n\nasync function exchangeAgyWithVerifierInternal(code: string, verifier: string): Promise {\n const tokenResponse = await agyFetch('https://oauth2.googleapis.com/token', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams({\n client_id: AGY_CLIENT_ID,\n client_secret: AGY_CLIENT_SECRET,\n code,\n grant_type: 'authorization_code',\n redirect_uri: AGY_REDIRECT_URI,\n code_verifier: verifier\n })\n });\n\n if (!tokenResponse.ok) {\n const errorText = await tokenResponse.text();\n return { type: 'failed', error: errorText };\n }\n\n const tokenPayload = (await tokenResponse.json()) as AgyTokenResponse;\n\n const userInfoResponse = await agyFetch('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', {\n headers: {\n Authorization: `Bearer ${tokenPayload.access_token}`\n }\n });\n\n const userInfo = userInfoResponse.ok ? ((await userInfoResponse.json()) as AgyUserInfo) : {};\n\n const refreshToken = tokenPayload.refresh_token;\n if (!refreshToken) {\n return { type: 'failed', error: 'Missing refresh token in response' };\n }\n\n return {\n type: 'success',\n refresh: refreshToken,\n access: tokenPayload.access_token,\n expires: Date.now() + tokenPayload.expires_in * 1000,\n email: userInfo.email\n };\n}\n","/**\n * Agy/Gemini CLI attaches a short activity ID to each request via its network logger.\n * We mirror the same format so backend/debug traces look like CLI traffic.\n */\nexport function createAgyActivityRequestId(): string {\n return Math.random().toString(36).substring(7);\n}\n","import os from 'node:os';\nimport { AGY_CLI_VERSION } from './agy-cli-version';\n\nconst AGY_CLI_DEFAULT_SURFACE = 'terminal';\n\nlet cachedUserAgent: string | null = null;\n\nexport function getAgyCliVersion(): string {\n const explicitVersion = process.env.OPENCODE_AGY_CLI_VERSION?.trim();\n if (explicitVersion) {\n return explicitVersion;\n }\n return AGY_CLI_VERSION;\n}\n\nexport function buildAgyCliUserAgent(model?: string): string {\n const version = getAgyCliVersion();\n if (cachedUserAgent && cachedUserAgent.startsWith('antigravity/cli/' + version + ' ')) {\n return cachedUserAgent;\n }\n const rawPlatform = os.platform();\n const platform = rawPlatform === 'win32' ? 'windows' : rawPlatform;\n const rawArch = os.arch();\n const arch = rawArch === 'x64' ? 'amd64' : rawArch;\n cachedUserAgent = 'antigravity/cli/' + version + ' ' + platform + '/' + arch;\n return cachedUserAgent;\n}\n\nfunction getAgyCliSurface(): string {\n return (\n process.env.AGY_CLI_SURFACE?.trim() ||\n process.env.SURFACE?.trim() ||\n AGY_CLI_DEFAULT_SURFACE\n );\n}\n\nexport const userAgentInternals = {\n resetCache() {\n cachedUserAgent = null;\n }\n};\n","export const AGY_CLI_VERSION = '1.2.11';\n","import { AGY_CODE_ASSIST_ENDPOINT } from '../constants';\nimport { agyFetch } from '../fetch';\nimport { createAgyActivityRequestId } from './activity-request-id';\nimport { buildAgyCliUserAgent } from './user-agent';\nimport type { RetrieveUserQuotaResponse, RetrieveUserQuotaSummaryResponse } from '../plugin/project/types';\n\n/**\n * Fetches the Code Assist quota bucket information, which contains the model IDs visible to the current account/project.\n */\nexport async function retrieveUserQuota(\n accessToken: string,\n projectId: string,\n userAgentModel?: string\n): Promise {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:retrieveUserQuota`;\n const headers = buildCodeAssistHeaders(accessToken, userAgentModel);\n\n try {\n const response = await agyFetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({ project: projectId })\n });\n\n if (!response.ok) {\n return null;\n }\n return (await response.json()) as RetrieveUserQuotaResponse;\n } catch {\n return null;\n }\n}\n\n/**\n * Fetches the Code Assist quota summary, grouped by model family with window-based buckets.\n */\nexport async function retrieveUserQuotaSummary(\n accessToken: string,\n projectId: string,\n userAgentModel?: string\n): Promise {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:retrieveUserQuotaSummary`;\n const headers = buildCodeAssistHeaders(accessToken, userAgentModel);\n\n try {\n const response = await agyFetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({ project: projectId })\n });\n\n if (!response.ok) {\n return null;\n }\n return (await response.json()) as RetrieveUserQuotaSummaryResponse;\n } catch {\n return null;\n }\n}\n\nfunction buildCodeAssistHeaders(\n accessToken: string,\n userAgentModel?: string\n): Record {\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${accessToken}`,\n 'User-Agent': userAgent,\n 'x-activity-request-id': createAgyActivityRequestId()\n };\n}\n","import type { AuthDetails, OAuthAuthDetails, RefreshParts } from './types';\n\nconst ACCESS_TOKEN_EXPIRY_BUFFER_MS = 60 * 1000;\n\nexport function isOAuthAuth(auth: AuthDetails): auth is OAuthAuthDetails {\n return auth.type === 'oauth';\n}\n\n/**\n * Splits the packed refresh string into the corresponding refresh token and project ID.\n */\nexport function parseRefreshParts(refresh: string): RefreshParts {\n const [refreshToken = '', projectId = '', managedProjectId = ''] = (refresh ?? '').split('|');\n return {\n refreshToken,\n projectId: projectId || undefined,\n managedProjectId: managedProjectId || undefined\n };\n}\n\n/**\n * Serializes the parts of a refresh token into the stored string format.\n */\nexport function formatRefreshParts(parts: RefreshParts): string {\n if (!parts.refreshToken) {\n return '';\n }\n\n if (!parts.projectId && !parts.managedProjectId) {\n return parts.refreshToken;\n }\n\n const projectSegment = parts.projectId ?? '';\n const managedSegment = parts.managedProjectId ?? '';\n return `${parts.refreshToken}|${projectSegment}|${managedSegment}`;\n}\n\n/**\n * Determines whether the access token has expired or is missing, with a buffer for clock skew.\n */\nexport function accessTokenExpired(auth: OAuthAuthDetails): boolean {\n if (!auth.access || typeof auth.expires !== 'number') {\n return true;\n }\n return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;\n}\n","import { retrieveUserQuotaSummary } from \"../fetch_quota\";\nimport type { QuotaSummaryBucket, RetrieveUserQuotaSummaryResponse } from \"../../plugin/project/types\";\n\ninterface GoogleRpcErrorInfo {\n \"@type\"?: string;\n reason?: string;\n domain?: string;\n metadata?: Record;\n}\n\ninterface GoogleRpcQuotaViolation {\n quotaId?: string;\n description?: string;\n}\n\ninterface GoogleRpcQuotaFailure {\n \"@type\"?: string;\n violations?: GoogleRpcQuotaViolation[];\n}\n\ninterface GoogleRpcRetryInfo {\n \"@type\"?: string;\n retryDelay?: string | { seconds?: number; nanos?: number };\n}\n\nexport interface QuotaContext {\n terminal: boolean;\n retryDelayMs?: number;\n reason?: string;\n}\n\nconst CLOUDCODE_DOMAINS = new Set([\n \"cloudcode-pa.googleapis.com\",\n \"staging-cloudcode-pa.googleapis.com\",\n \"autopush-cloudcode-pa.googleapis.com\",\n \"cloudaicompanion.googleapis.com\",\n \"daily-cloudcode-pa.googleapis.com\",\n]);\n\n/**\n * NOTE: Special Design - Granular 429 Error Classification and Retry Strategy\n * Traditional network retry modules usually treat 429 errors uniformly as rate-limiting for retries or throwing errors.\n * Here we parse it granularly:\n * 1. Differentiate between \"Account physical quota exhausted\" and \"Model instantaneous capacity overloaded (MODEL_CAPACITY_EXHAUSTED)\".\n * 2. If it's physical quota exhaustion, it's considered an unretriable terminal state to avoid meaningless network requests;\n * If it's a momentary overload of Google's backend model capacity, it's considered retriable, parses RetryInfo delay from response, and notifies the upper layer (showing a Toast in TUI, backing off, and retrying).\n */\nexport async function classifyQuotaResponse(response: Response): Promise {\n const payload = await parseErrorBody(response);\n if (!payload) {\n return null;\n }\n\n const details = Array.isArray(payload.details) ? payload.details : [];\n const retryInfo = details.find(\n (detail): detail is GoogleRpcRetryInfo =>\n isObject(detail) &&\n detail[\"@type\"] === \"type.googleapis.com/google.rpc.RetryInfo\",\n );\n const retryDelayMs =\n (retryInfo?.retryDelay ? parseRetryDelayValue(retryInfo.retryDelay) : null) ??\n parseRetryDelayFromMessage(payload.message ?? \"\") ??\n undefined;\n\n const errorInfo = details.find(\n (detail): detail is GoogleRpcErrorInfo =>\n isObject(detail) &&\n detail[\"@type\"] === \"type.googleapis.com/google.rpc.ErrorInfo\",\n );\n\n if (errorInfo?.domain && !CLOUDCODE_DOMAINS.has(errorInfo.domain)) {\n return null;\n }\n if (errorInfo?.reason === \"QUOTA_EXHAUSTED\") {\n return { terminal: true, retryDelayMs, reason: errorInfo.reason };\n }\n if (errorInfo?.reason === \"RATE_LIMIT_EXCEEDED\") {\n return { terminal: false, retryDelayMs: retryDelayMs ?? 10_000, reason: errorInfo.reason };\n }\n if (errorInfo?.reason === \"MODEL_CAPACITY_EXHAUSTED\") {\n return {\n terminal: retryDelayMs === undefined,\n retryDelayMs,\n reason: errorInfo.reason,\n };\n }\n\n const quotaFailure = details.find(\n (detail): detail is GoogleRpcQuotaFailure =>\n isObject(detail) &&\n detail[\"@type\"] === \"type.googleapis.com/google.rpc.QuotaFailure\",\n );\n if (quotaFailure?.violations?.length) {\n const allTexts = quotaFailure.violations\n .flatMap((violation) => [violation.quotaId ?? \"\", violation.description ?? \"\"])\n .join(\" \")\n .toLowerCase();\n\n if (allTexts.includes(\"perday\") || allTexts.includes(\"daily\") || allTexts.includes(\"per day\")) {\n return { terminal: true, retryDelayMs, reason: errorInfo?.reason };\n }\n if (allTexts.includes(\"perminute\") || allTexts.includes(\"per minute\")) {\n return { terminal: false, retryDelayMs: retryDelayMs ?? 60_000, reason: errorInfo?.reason };\n }\n return { terminal: false, retryDelayMs, reason: errorInfo?.reason };\n }\n\n const quotaLimit = errorInfo?.metadata?.quota_limit?.toLowerCase() ?? \"\";\n if (quotaLimit.includes(\"perminute\") || quotaLimit.includes(\"per minute\")) {\n return { terminal: false, retryDelayMs: retryDelayMs ?? 60_000, reason: errorInfo?.reason };\n }\n\n return { terminal: false, retryDelayMs, reason: errorInfo?.reason };\n}\n\n/**\n * Extracts the RetryInfo delay hint directly from the error payload.\n */\nexport async function parseRetryDelayFromBody(response: Response): Promise {\n const payload = await parseErrorBody(response);\n if (!payload) {\n return null;\n }\n\n const details = Array.isArray(payload.details) ? payload.details : [];\n const retryInfo = details.find(\n (detail): detail is GoogleRpcRetryInfo =>\n isObject(detail) &&\n detail[\"@type\"] === \"type.googleapis.com/google.rpc.RetryInfo\",\n );\n if (retryInfo?.retryDelay) {\n const delayMs = parseRetryDelayValue(retryInfo.retryDelay);\n if (delayMs !== null) {\n return delayMs;\n }\n }\n\n if (typeof payload.message === \"string\") {\n return parseRetryDelayFromMessage(payload.message);\n }\n return null;\n}\n\nfunction parseRetryDelayValue(value: string | { seconds?: number; nanos?: number }): number | null {\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed) {\n return null;\n }\n if (trimmed.endsWith(\"ms\")) {\n const milliseconds = Number(trimmed.slice(0, -2));\n return Number.isFinite(milliseconds) && milliseconds > 0 ? Math.round(milliseconds) : null;\n }\n const match = trimmed.match(/^([\\d.]+)s$/);\n if (!match?.[1]) {\n return null;\n }\n const seconds = Number(match[1]);\n return Number.isFinite(seconds) && seconds > 0 ? Math.round(seconds * 1000) : null;\n }\n\n const seconds = typeof value.seconds === \"number\" ? value.seconds : 0;\n const nanos = typeof value.nanos === \"number\" ? value.nanos : 0;\n if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {\n return null;\n }\n const totalMs = Math.round(seconds * 1000 + nanos / 1e6);\n return totalMs > 0 ? totalMs : null;\n}\n\nfunction parseRetryDelayFromMessage(message: string): number | null {\n const retryMatch = message.match(/Please retry in ([0-9.]+(?:ms|s))/i);\n if (retryMatch?.[1]) {\n return parseRetryDelayValue(retryMatch[1]);\n }\n\n const afterMatch = message.match(/after\\s+([0-9.]+(?:ms|s))/i);\n if (afterMatch?.[1]) {\n return parseRetryDelayValue(afterMatch[1]);\n }\n\n return null;\n}\n\nasync function parseErrorBody(\n response: Response,\n): Promise<{ message?: string; details?: unknown[] } | null> {\n let text = \"\";\n try {\n text = await response.clone().text();\n } catch {\n return null;\n }\n if (!text) {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return null;\n }\n\n const normalized = normalizeErrorEnvelope(parsed);\n if (!normalized || !isObject(normalized.error)) {\n return null;\n }\n\n const error = normalized.error as Record;\n return {\n message: typeof error.message === \"string\" ? error.message : undefined,\n details: Array.isArray(error.details) ? error.details : undefined,\n };\n}\n\nfunction isObject(value: unknown): value is Record {\n return !!value && typeof value === \"object\";\n}\n\nfunction normalizeErrorEnvelope(parsed: unknown): Record | null {\n if (Array.isArray(parsed)) {\n const first = parsed[0];\n return isObject(first) ? first : null;\n }\n return isObject(parsed) ? parsed : null;\n}\n\nexport const MAX_QUOTA_RESET_WAIT_MS = 7_200_000; // 7200 seconds (2 hours)\n\nexport function findResetTimeForModel(\n summary: RetrieveUserQuotaSummaryResponse | null | undefined,\n model?: string,\n): string | null {\n if (!summary) return null;\n\n const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const modelNorm = model ? normalize(model) : \"\";\n\n let targetGroups = summary.groups ?? [];\n if (modelNorm && targetGroups.length > 0) {\n const matched = targetGroups.filter((g) => {\n const gName = normalize(g.displayName ?? \"\");\n return gName.includes(modelNorm) || modelNorm.includes(gName);\n });\n if (matched.length > 0) {\n targetGroups = matched;\n }\n }\n\n const allBuckets: QuotaSummaryBucket[] = [];\n for (const group of targetGroups) {\n if (group.buckets) {\n allBuckets.push(...group.buckets);\n }\n }\n if (allBuckets.length === 0 && summary.buckets) {\n allBuckets.push(...summary.buckets);\n }\n\n const now = Date.now();\n const validResetBuckets = allBuckets.filter((b) => {\n if (!b.resetTime) return false;\n const t = new Date(b.resetTime).getTime();\n return !Number.isNaN(t) && t > now;\n });\n\n if (validResetBuckets.length === 0) {\n return null;\n }\n\n const exhaustedFiveHour = validResetBuckets.find(\n (b) => (b.remainingFraction === 0 || b.disabled) && b.window?.toUpperCase() === \"FIVE_HOUR\",\n );\n if (exhaustedFiveHour?.resetTime) return exhaustedFiveHour.resetTime;\n\n const exhaustedAny = validResetBuckets.find((b) => b.remainingFraction === 0 || b.disabled);\n if (exhaustedAny?.resetTime) return exhaustedAny.resetTime;\n\n const fiveHour = validResetBuckets.find((b) => b.window?.toUpperCase() === \"FIVE_HOUR\");\n if (fiveHour?.resetTime) return fiveHour.resetTime;\n\n validResetBuckets.sort(\n (a, b) => new Date(a.resetTime!).getTime() - new Date(b.resetTime!).getTime(),\n );\n return validResetBuckets[0]?.resetTime ?? null;\n}\n\nexport async function resolveQuotaResetDelay(\n accessToken: string,\n projectId: string,\n model?: string,\n userAgentModel?: string,\n): Promise<{ waitMs: number; resetTime: string } | null> {\n try {\n const summary = await retrieveUserQuotaSummary(accessToken, projectId, userAgentModel);\n if (!summary) return null;\n\n const resetTime = findResetTimeForModel(summary, model);\n if (!resetTime) return null;\n\n const resetTimestamp = new Date(resetTime).getTime();\n if (Number.isNaN(resetTimestamp)) return null;\n\n const waitMs = resetTimestamp - Date.now() + 1000;\n if (waitMs <= 0 || waitMs > MAX_QUOTA_RESET_WAIT_MS) {\n return null;\n }\n\n return { waitMs, resetTime };\n } catch {\n return null;\n }\n}\n\nexport const retryInternals = {\n parseRetryDelayValue,\n parseRetryDelayFromMessage,\n findResetTimeForModel,\n resolveQuotaResetDelay,\n};\n","import { parseRetryDelayFromBody } from \"./quota\";\n\nexport const DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 5000;\nconst DEFAULT_MAX_DELAY_MS = 30000;\n\nconst RETRYABLE_NETWORK_CODES = new Set([\n \"ECONNRESET\",\n \"ETIMEDOUT\",\n \"EPIPE\",\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"ECONNREFUSED\",\n \"ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC\",\n \"ERR_SSL_WRONG_VERSION_NUMBER\",\n \"ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC\",\n \"ERR_SSL_BAD_RECORD_MAC\",\n \"EPROTO\",\n]);\n\n/**\n * Ensures the request body is replayable before attempting a retry.\n */\nexport function canRetryRequest(init: RequestInit | undefined): boolean {\n if (!init?.body) {\n return true;\n }\n\n const body = init.body;\n if (typeof body === \"string\") {\n return true;\n }\n if (typeof URLSearchParams !== \"undefined\" && body instanceof URLSearchParams) {\n return true;\n }\n if (typeof ArrayBuffer !== \"undefined\" && body instanceof ArrayBuffer) {\n return true;\n }\n if (typeof ArrayBuffer !== \"undefined\" && ArrayBuffer.isView(body)) {\n return true;\n }\n if (typeof Blob !== \"undefined\" && body instanceof Blob) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Status code-based retry strategy, consistent with Gemini/Agy CLI.\n */\nexport function isRetryableStatus(status: number): boolean {\n return status === 429 || (status >= 500 && status < 600);\n}\n\n/**\n * Handles transient network failures (including error codes nested in `cause.code`).\n */\nexport function isRetryableNetworkError(error: unknown): boolean {\n const code = getNetworkErrorCode(error);\n if (code && RETRYABLE_NETWORK_CODES.has(code)) {\n return true;\n }\n\n return error instanceof Error && error.message.toLowerCase().includes(\"fetch failed\");\n}\n\n/**\n * Prioritizes parsing retry delay milliseconds via Retry-After header, quota info in response body, or fallback exponential backoff.\n */\nexport async function resolveRetryDelayMs(\n response: Response,\n attempt: number,\n quotaDelayMs?: number,\n): Promise {\n const retryAfterMsHeader = parseRetryAfterMs(response.headers.get(\"retry-after-ms\"));\n if (retryAfterMsHeader !== null) {\n return clampDelay(retryAfterMsHeader);\n }\n\n const retryAfterHeader = parseRetryAfter(response.headers.get(\"retry-after\"));\n if (retryAfterHeader !== null) {\n return clampDelay(retryAfterHeader);\n }\n\n if (quotaDelayMs !== undefined) {\n return clampDelay(quotaDelayMs);\n }\n\n const bodyDelay = await parseRetryDelayFromBody(response);\n if (bodyDelay !== null) {\n return clampDelay(bodyDelay);\n }\n\n return getExponentialDelayWithJitter(attempt);\n}\n\nexport function getExponentialDelayWithJitter(attempt: number): number {\n const base = Math.min(DEFAULT_MAX_DELAY_MS, DEFAULT_INITIAL_DELAY_MS * Math.pow(2, attempt - 1));\n const jitter = base * 0.3 * (Math.random() * 2 - 1);\n return clampDelay(base + jitter);\n}\n\nexport function wait(ms: number): Promise {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\nfunction getNetworkErrorCode(error: unknown): string | undefined {\n const readCode = (value: unknown): string | undefined => {\n if (!value || typeof value !== \"object\") {\n return undefined;\n }\n if (\"code\" in value && typeof (value as { code?: unknown }).code === \"string\") {\n return (value as { code: string }).code;\n }\n return undefined;\n };\n\n const direct = readCode(error);\n if (direct) {\n return direct;\n }\n\n let cursor: unknown = error;\n for (let depth = 0; depth < 5; depth += 1) {\n if (!cursor || typeof cursor !== \"object\" || !(\"cause\" in cursor)) {\n break;\n }\n cursor = (cursor as { cause?: unknown }).cause;\n const code = readCode(cursor);\n if (code) {\n return code;\n }\n }\n return undefined;\n}\n\nfunction parseRetryAfterMs(value: string | null): number | null {\n if (!value) {\n return null;\n }\n const parsed = Number(value.trim());\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return Math.round(parsed);\n}\n\nfunction parseRetryAfter(value: string | null): number | null {\n if (!value) {\n return null;\n }\n const trimmed = value.trim();\n if (!trimmed) {\n return null;\n }\n const seconds = Number(trimmed);\n if (Number.isFinite(seconds)) {\n return Math.max(0, Math.round(seconds * 1000));\n }\n const parsedDate = Date.parse(trimmed);\n if (!Number.isNaN(parsedDate)) {\n return Math.max(0, parsedDate - Date.now());\n }\n return null;\n}\n\nfunction clampDelay(delayMs: number): number {\n if (!Number.isFinite(delayMs)) {\n return DEFAULT_MAX_DELAY_MS;\n }\n return Math.min(Math.max(0, Math.round(delayMs)), DEFAULT_MAX_DELAY_MS);\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\n\ninterface CooldownData {\n version: \"1.0\";\n entries: Record;\n updatedAt: number;\n}\n\nconst WRITE_THROTTLE_MS = 5000;\n\nfunction getConfigDir(): string {\n const platform = process.platform;\n if (platform === \"win32\") {\n return join(process.env.APPDATA || join(homedir(), \"AppData\", \"Roaming\"), \"opencode\");\n }\n const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), \".config\");\n return join(xdgConfig, \"opencode\");\n}\n\nfunction getCooldownFilePath(): string {\n return join(getConfigDir(), \"antigravity-retry-cooldowns.json\");\n}\n\nexport function loadCooldowns(): Map {\n const result = new Map();\n try {\n const filePath = getCooldownFilePath();\n if (!existsSync(filePath)) {\n return result;\n }\n\n const content = readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(content) as CooldownData;\n if (data.version !== \"1.0\") {\n return result;\n }\n\n const now = Date.now();\n for (const [key, expiresAt] of Object.entries(data.entries)) {\n if (typeof expiresAt === \"number\" && expiresAt > now) {\n result.set(key, expiresAt);\n }\n }\n } catch {\n // Fault tolerance: start fresh on disk load failure\n }\n return result;\n}\n\nexport function saveCooldowns(entries: Map): boolean {\n try {\n const filePath = getCooldownFilePath();\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const now = Date.now();\n const serializable: Record = {};\n for (const [key, expiresAt] of entries.entries()) {\n if (expiresAt > now) {\n serializable[key] = expiresAt;\n }\n }\n\n const data: CooldownData = {\n version: \"1.0\",\n entries: serializable,\n updatedAt: now,\n };\n\n const tmpPath = join(tmpdir(), `antigravity-cooldowns-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);\n writeFileSync(tmpPath, JSON.stringify(data), \"utf-8\");\n\n try {\n renameSync(tmpPath, filePath);\n } catch {\n writeFileSync(filePath, readFileSync(tmpPath));\n try {\n unlinkSync(tmpPath);\n } catch {\n // Ignore temp file deletion failure\n }\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport class CooldownStore {\n private dirty = false;\n private lastWriteTime = 0;\n private writeTimer: ReturnType | null = null;\n private entries: Map = new Map();\n\n bind(entries: Map): void {\n this.entries = entries;\n }\n\n markDirty(): void {\n this.dirty = true;\n this.scheduleThrottledWrite();\n }\n\n flush(): boolean {\n this.dirty = false;\n this.clearWriteTimer();\n this.lastWriteTime = Date.now();\n return saveCooldowns(this.entries);\n }\n\n shutdown(): void {\n this.clearWriteTimer();\n if (this.dirty) {\n saveCooldowns(this.entries);\n this.dirty = false;\n }\n }\n\n private scheduleThrottledWrite(): void {\n if (this.writeTimer) {\n return;\n }\n\n const elapsed = Date.now() - this.lastWriteTime;\n const remaining = Math.max(0, WRITE_THROTTLE_MS - elapsed);\n\n this.writeTimer = setTimeout(() => {\n this.writeTimer = null;\n this.lastWriteTime = Date.now();\n if (this.dirty) {\n this.dirty = false;\n saveCooldowns(this.entries);\n }\n }, remaining);\n\n if (this.writeTimer && typeof this.writeTimer === \"object\" && \"unref\" in this.writeTimer) {\n this.writeTimer.unref();\n }\n }\n\n private clearWriteTimer(): void {\n if (this.writeTimer) {\n clearTimeout(this.writeTimer);\n this.writeTimer = null;\n }\n }\n}\n","import {\n canRetryRequest,\n DEFAULT_MAX_ATTEMPTS,\n getExponentialDelayWithJitter,\n isRetryableNetworkError,\n isRetryableStatus,\n resolveRetryDelayMs,\n wait,\n} from \"./helpers\";\nimport {\n classifyQuotaResponse,\n MAX_QUOTA_RESET_WAIT_MS,\n resolveQuotaResetDelay,\n retryInternals,\n} from \"./quota\";\nimport { agyFetch } from \"../../fetch\";\nimport { CooldownStore, loadCooldowns } from \"./cooldown-store\";\n\nconst retryCooldownByKey = new Map();\nconst cooldownStore = new CooldownStore();\nlet cooldownPersistenceInitialized = false;\n\nfunction initCooldownPersistence(): void {\n if (cooldownPersistenceInitialized) return;\n cooldownPersistenceInitialized = true;\n try {\n const persisted = loadCooldowns();\n for (const [key, expiresAt] of persisted.entries()) {\n retryCooldownByKey.set(key, expiresAt);\n }\n cooldownStore.bind(retryCooldownByKey);\n if (typeof process !== \"undefined\") {\n process.on(\"exit\", () => {\n cooldownStore.shutdown();\n });\n }\n } catch {\n cooldownStore.bind(retryCooldownByKey);\n }\n}\n\nexport { initCooldownPersistence };\nconst MODEL_CAPACITY_COOLDOWN_MS = 8000;\n\n/**\n * Sends a request with retry/exponential backoff semantics, consistent with Gemini/Agy CLI.\n */\nexport async function fetchWithRetry(\n input: RequestInfo,\n init: RequestInit | undefined,\n): Promise {\n if (!cooldownPersistenceInitialized) initCooldownPersistence();\n if (!canRetryRequest(init)) {\n return agyFetch(input, init);\n }\n\n const retryInit = cloneRetryableInit(init);\n const throttleKey = buildRetryThrottleKey(input, retryInit);\n await waitForRetryCooldown(throttleKey, retryInit.signal);\n let attempt = 1;\n let hasRetriedQuotaReset = false;\n const url = readRequestUrl(input);\n\n while (attempt <= DEFAULT_MAX_ATTEMPTS) {\n let response: Response;\n try {\n response = await agyFetch(input, retryInit);\n } catch (error) {\n if (attempt >= DEFAULT_MAX_ATTEMPTS || !isRetryableNetworkError(error)) {\n throw error;\n }\n if (retryInit.signal?.aborted) {\n throw error;\n }\n\n const delayMs = getExponentialDelayWithJitter(attempt);\n await wait(delayMs);\n attempt += 1;\n continue;\n }\n\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const quotaContext = response.status === 429 ? await classifyQuotaResponse(response) : null;\n if (response.status === 429 && quotaContext?.terminal) {\n if (quotaContext.reason === \"MODEL_CAPACITY_EXHAUSTED\") {\n const cooldownMs = quotaContext.retryDelayMs ?? MODEL_CAPACITY_COOLDOWN_MS;\n setRetryCooldown(throttleKey, cooldownMs);\n return response;\n }\n\n if (quotaContext.reason === \"QUOTA_EXHAUSTED\" && !hasRetriedQuotaReset && !retryInit.signal?.aborted) {\n const body = typeof retryInit.body === \"string\" ? safeParseBody(retryInit.body) : null;\n const project = readString(body?.project);\n const model = readString(body?.model);\n const token = extractAuthToken(retryInit.headers);\n\n if (token && project) {\n const resetInfo = await resolveQuotaResetDelay(token, project, model);\n if (resetInfo && resetInfo.waitMs > 0 && resetInfo.waitMs <= MAX_QUOTA_RESET_WAIT_MS) {\n hasRetriedQuotaReset = true;\n setRetryCooldown(throttleKey, resetInfo.waitMs);\n await wait(resetInfo.waitMs);\n if (retryInit.signal?.aborted) {\n return response;\n }\n continue;\n }\n }\n }\n\n return response;\n }\n\n if (attempt >= DEFAULT_MAX_ATTEMPTS || retryInit.signal?.aborted) {\n return response;\n }\n\n const delayMs = await resolveRetryDelayMs(response, attempt, quotaContext?.retryDelayMs);\n if (delayMs > 0 && response.status === 429) {\n setRetryCooldown(throttleKey, delayMs);\n }\n if (delayMs > 0) {\n await wait(delayMs);\n }\n attempt += 1;\n }\n\n return agyFetch(input, retryInit);\n}\n\nfunction cloneRetryableInit(init: RequestInit | undefined): RequestInit {\n if (!init) {\n return {};\n }\n return {\n ...init,\n headers: new Headers(init.headers ?? {}),\n };\n}\n\nfunction buildRetryThrottleKey(input: RequestInfo, init: RequestInit): string {\n const url = readRequestUrl(input);\n const body = typeof init.body === \"string\" ? safeParseBody(init.body) : null;\n const project = readString(body?.project);\n const model = readString(body?.model);\n return `${url}|${project ?? \"\"}|${model ?? \"\"}`;\n}\n\nasync function waitForRetryCooldown(key: string, signal?: AbortSignal | null): Promise {\n const until = retryCooldownByKey.get(key);\n if (!until) {\n return;\n }\n\n const remaining = until - Date.now();\n if (remaining <= 0) {\n retryCooldownByKey.delete(key);\n return;\n }\n if (signal?.aborted) {\n return;\n }\n\n await wait(remaining);\n retryCooldownByKey.delete(key);\n}\n\nfunction setRetryCooldown(key: string, delayMs: number): void {\n if (!cooldownPersistenceInitialized) initCooldownPersistence();\n const next = Date.now() + delayMs;\n const current = retryCooldownByKey.get(key) ?? 0;\n retryCooldownByKey.set(key, Math.max(current, next));\n cooldownStore.markDirty();\n}\n\nexport function shutdownRetryCooldowns(): void {\n if (cooldownPersistenceInitialized) {\n cooldownStore.shutdown();\n }\n}\n\nfunction readRequestUrl(input: RequestInfo): string {\n if (typeof input === \"string\") {\n return input;\n }\n if (input instanceof URL) {\n return input.toString();\n }\n\n const request = input as Request;\n if (request.url) {\n return request.url;\n }\n return input.toString();\n}\n\nfunction safeParseBody(body: string): Record | null {\n if (!body) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(body);\n if (parsed && typeof parsed === \"object\") {\n return parsed as Record;\n }\n } catch {}\n return null;\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\nfunction extractAuthToken(headers?: HeadersInit): string | undefined {\n if (!headers) return undefined;\n const h = new Headers(headers);\n const auth = h.get(\"authorization\") || h.get(\"Authorization\");\n if (!auth) return undefined;\n const match = auth.match(/^Bearer\\s+(.+)$/i);\n return match?.[1]?.trim() || auth.trim();\n}\n\nexport { retryInternals };\n","const OSC8_OPEN = '\\x1b]8;;';\nconst OSC8_CLOSE = '\\x07';\n\nexport function supportsOsc8Hyperlinks(): boolean {\n if (process.stdout && !process.stdout.isTTY) {\n return false;\n }\n\n if (process.env.OPENCODE_HEADLESS) {\n return false;\n }\n\n const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? '';\n if (termProgram === 'iterm.app' || termProgram === 'wezterm' || termProgram === 'ghostty') {\n return true;\n }\n\n if (process.env.KITTY_WINDOW_ID) {\n return true;\n }\n\n const vteVersion = parseInt(process.env.VTE_VERSION ?? '0', 10);\n if (vteVersion >= 5000) {\n return true;\n }\n\n const colorTerm = process.env.COLORTERM?.toLowerCase() ?? '';\n if (colorTerm === 'truecolor' || colorTerm === '24bit') {\n const term = process.env.TERM?.toLowerCase() ?? '';\n if (term.startsWith('xterm') || term.startsWith('alacritty') || term === 'termion') {\n return true;\n }\n }\n\n return false;\n}\n\nexport function formatHyperlink(url: string, text?: string): string {\n if (!supportsOsc8Hyperlinks()) {\n if (text) {\n return text === url ? text : `${text} (${url})`;\n }\n return url;\n }\n const displayText = text ?? url;\n return `${OSC8_OPEN}${url}${OSC8_CLOSE}${displayText}${OSC8_OPEN}${OSC8_CLOSE}`;\n}\n\nexport function stripOsc8(text: string): string {\n return text.replace(/\\x1b\\]8;[^;]*;[^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)/g, '');\n}\n","export const FREE_TIER_ID = \"free-tier\";\nexport const LEGACY_TIER_ID = \"legacy-tier\";\n\nexport const CODE_ASSIST_METADATA = {\n ideType: \"ANTIGRAVITY\",\n} as const;\n\nexport interface AgyUserTier {\n id?: string;\n isDefault?: boolean;\n userDefinedCloudaicompanionProject?: boolean;\n name?: string;\n description?: string;\n}\n\nexport interface CloudAiCompanionProject {\n id?: string;\n}\n\nexport interface AgyIneligibleTier {\n reasonCode?: string;\n reasonMessage?: string;\n validationUrl?: string;\n validationLearnMoreUrl?: string;\n}\n\nexport interface LoadCodeAssistPayload {\n cloudaicompanionProject?: string | CloudAiCompanionProject;\n currentTier?: {\n id?: string;\n name?: string;\n };\n allowedTiers?: AgyUserTier[];\n ineligibleTiers?: AgyIneligibleTier[];\n}\n\nexport interface OnboardUserPayload {\n name?: string;\n done?: boolean;\n response?: {\n cloudaicompanionProject?: {\n id?: string;\n };\n };\n}\n\nexport interface RetrieveUserQuotaBucket {\n remainingAmount?: string;\n remainingFraction?: number;\n resetTime?: string;\n tokenType?: string;\n modelId?: string;\n}\n\nexport interface RetrieveUserQuotaResponse {\n buckets?: RetrieveUserQuotaBucket[];\n}\n\nexport interface QuotaSummaryBucket {\n bucketId?: string;\n displayName?: string;\n description?: string;\n window?: string;\n remaining?: string;\n remainingFraction?: number;\n remainingAmount?: string;\n disabled?: boolean;\n resetTime?: string;\n}\n\nexport interface QuotaSummaryGroup {\n displayName?: string;\n description?: string;\n buckets?: QuotaSummaryBucket[];\n}\n\nexport interface RetrieveUserQuotaSummaryResponse {\n groups?: QuotaSummaryGroup[];\n buckets?: QuotaSummaryBucket[];\n description?: string;\n}\n\n/**\n * Thrown during Gemini enablement if the required Google Cloud project is missing.\n */\nexport class ProjectIdRequiredError extends Error {\n constructor() {\n super(\n \"Google Gemini/Agy requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, then set `provider.google.options.projectId` in your Opencode config (or set OPENCODE_AGY_PROJECT_ID / GOOGLE_CLOUD_PROJECT).\",\n );\n }\n}\n\nexport class ProjectAccessDeniedError extends Error {\n constructor(projectId: string | undefined, backendMessage: string | undefined) {\n const projectStr = projectId ? `project '${projectId}'` : 'the requested project';\n const msg = backendMessage ? `\\nBackend response: ${backendMessage}` : '';\n super(`Access denied to ${projectStr}. Ensure the Gemini for Google Cloud API is enabled and you have the correct IAM permissions.${msg}`);\n this.name = 'ProjectAccessDeniedError';\n }\n}\n\nexport class AccountValidationRequiredError extends Error {\n validationUrl?: string;\n validationLearnMoreUrl?: string;\n\n constructor(\n message: string,\n validationUrl?: string,\n validationLearnMoreUrl?: string,\n ) {\n const parts = [message.trim()];\n if (validationUrl) {\n parts.push(`Complete validation: ${validationUrl}`);\n }\n if (validationLearnMoreUrl) {\n parts.push(`Learn more: ${validationLearnMoreUrl}`);\n }\n\n super(parts.join(\"\\n\"));\n this.name = \"AccountValidationRequiredError\";\n this.validationUrl = validationUrl;\n this.validationLearnMoreUrl = validationLearnMoreUrl;\n }\n}\n","import type { OAuthAuthDetails } from \"../types\";\nimport {\n AccountValidationRequiredError,\n CODE_ASSIST_METADATA,\n LEGACY_TIER_ID,\n type CloudAiCompanionProject,\n type AgyIneligibleTier,\n type AgyUserTier,\n} from \"./types\";\n\n/**\n * Builds the metadata headers required for the Code Assist API.\n */\nexport function buildMetadata(projectId?: string, includeDuetProject = true): Record {\n const metadata: Record = {\n ...CODE_ASSIST_METADATA,\n };\n if (projectId && includeDuetProject) {\n metadata.duetProject = projectId;\n }\n return metadata;\n}\n\n/**\n * Normalizes project identifiers from API payloads or configuration.\n */\nexport function normalizeProjectId(value?: string | CloudAiCompanionProject): string | undefined {\n if (!value) {\n return undefined;\n }\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n return trimmed ? trimmed : undefined;\n }\n if (typeof value === \"object\" && typeof value.id === \"string\") {\n const trimmed = value.id.trim();\n return trimmed ? trimmed : undefined;\n }\n return undefined;\n}\n\n/**\n * Selects the default hierarchy ID from the allowed hierarchy list.\n */\nexport function pickOnboardTier(allowedTiers?: AgyUserTier[]): AgyUserTier {\n if (allowedTiers && allowedTiers.length > 0) {\n for (const tier of allowedTiers) {\n if (tier?.isDefault) {\n return tier;\n }\n }\n return allowedTiers[0] ?? { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };\n }\n return { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };\n}\n\n/**\n * Builds a concise error message for non-compliant hierarchy payloads.\n */\nexport function buildIneligibleTierMessage(tiers?: AgyIneligibleTier[]): string | undefined {\n if (!tiers || tiers.length === 0) {\n return undefined;\n }\n const reasons = tiers\n .map((tier) => tier?.reasonMessage?.trim())\n .filter((message): message is string => !!message);\n return reasons.length > 0 ? reasons.join(\", \") : undefined;\n}\n\nexport function throwIfValidationRequired(tiers?: AgyIneligibleTier[]): void {\n if (!tiers || tiers.length === 0) {\n return;\n }\n\n const validationTier = tiers.find((tier) => {\n const reasonCode = tier?.reasonCode?.trim().toUpperCase();\n return reasonCode === \"VALIDATION_REQUIRED\" && !!tier.validationUrl?.trim();\n });\n if (!validationTier) {\n return;\n }\n\n throw new AccountValidationRequiredError(\n validationTier.reasonMessage?.trim() || \"Verify your account to continue.\",\n validationTier.validationUrl?.trim(),\n validationTier.validationLearnMoreUrl?.trim(),\n );\n}\n\n/**\n * Detects VPC-SC errors from Cloud Code responses.\n */\nexport function isVpcScError(payload: unknown): boolean {\n if (!payload || typeof payload !== \"object\") {\n return false;\n }\n const error = (payload as { error?: unknown }).error;\n if (!error || typeof error !== \"object\") {\n return false;\n }\n const details = (error as { details?: unknown }).details;\n if (!Array.isArray(details)) {\n return false;\n }\n return details.some((detail) => {\n if (!detail || typeof detail !== \"object\") {\n return false;\n }\n return (detail as { reason?: unknown }).reason === \"SECURITY_POLICY_VIOLATED\";\n });\n}\n\n/**\n * Safely parses JSON, returning null on failure.\n */\nexport function parseJsonSafe(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\n/**\n * Promise-based delay utility.\n */\nexport function wait(ms: number): Promise {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n/**\n * Generates a cache key for the project context based on the refresh token.\n */\nexport function getCacheKey(auth: OAuthAuthDetails): string | undefined {\n const refresh = auth.refresh?.trim();\n if (!refresh) {\n return undefined;\n }\n const [baseRefreshToken = ''] = refresh.split('|');\n return baseRefreshToken ? baseRefreshToken : undefined;\n}\n","import { AGY_CODE_ASSIST_ENDPOINT } from '../constants';\nimport { agyFetch } from '../fetch';\nimport { fetchWithRetry } from './retry';\nimport { createAgyActivityRequestId } from './activity-request-id';\nimport { buildAgyCliUserAgent } from './user-agent';\nimport { formatHyperlink } from './terminal-hyperlink';\nimport {\n FREE_TIER_ID,\n type LoadCodeAssistPayload,\n type OnboardUserPayload,\n ProjectIdRequiredError,\n ProjectAccessDeniedError\n} from '../plugin/project/types';\nimport { buildMetadata, isVpcScError, parseJsonSafe, wait } from '../plugin/project/utils';\n\n/**\n * Loads hosted project information for a given access token and optional project.\n */\nexport async function loadManagedProject(\n accessToken: string,\n projectId?: string,\n userAgentModel?: string\n): Promise {\n try {\n const metadata = buildMetadata(projectId);\n const requestBody: Record = { metadata };\n if (projectId) {\n requestBody.cloudaicompanionProject = projectId;\n }\n\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`;\n if (process.env.OPENCODE_AGY_VERBOSE_LOGS === \"1\") {\n console.warn(`[Agy Auth] loadManagedProject calling URL: ${formatHyperlink(url)} with project: ${projectId || 'none'}`);\n }\n const headers = buildCodeAssistHeaders(accessToken, userAgentModel);\n\n const response = await fetchWithRetry(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(requestBody)\n });\n\n if (!response.ok) {\n if (response.status === 403 || response.status === 404) {\n console.warn(`[Agy Auth] loadManagedProject failed with ${response.status} (possible Cloud API mismatch/unauthorized). URL: ${formatHyperlink(url)}, Project: ${projectId}`);\n const responseText = await readResponseTextIfNeeded(response, true);\n if (responseText && isVpcScError(responseText)) {\n console.warn(`[Agy Auth] loadManagedProject: Detected VPC Service Controls block`);\n }\n throw new ProjectAccessDeniedError(projectId, responseText);\n } else {\n const cleanStatusText = response.statusText.replace(/[\\r\\n]+/g, ' ').trim();\n if (response.status === 429) {\n console.warn(`[Agy Auth] loadManagedProject failed with 429 ${cleanStatusText} (rate limited after retries; see Retry-After / quota exhaustion). URL: ${formatHyperlink(url)}, Project: ${projectId}`);\n } else {\n console.warn(`[Agy Auth] loadManagedProject failed with ${response.status} ${cleanStatusText}`);\n }\n }\n return null;\n }\n\n const responseJson = await response.json();\n return responseJson as LoadCodeAssistPayload;\n } catch (error) {\n if (error instanceof ProjectAccessDeniedError) {\n throw error;\n }\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to load code assist project: ${errStr}`);\n return null;\n }\n}\n\n/**\n * Enables a hosted project for the user, optionally retrying until complete.\n */\nexport async function onboardManagedProject(\n accessToken: string,\n tierId: string,\n projectId?: string,\n userAgentModel?: string,\n attempts = 10,\n delayMs = 5000\n): Promise {\n const isFreeTier = tierId === FREE_TIER_ID;\n const metadata = buildMetadata(projectId, !isFreeTier);\n const requestBody: Record = { tierId, metadata };\n\n if (!isFreeTier) {\n if (!projectId) {\n throw new ProjectIdRequiredError();\n }\n requestBody.cloudaicompanionProject = projectId;\n }\n\n const baseUrl = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal`;\n const onboardUrl = `${baseUrl}:onboardUser`;\n if (process.env.OPENCODE_AGY_VERBOSE_LOGS === \"1\") {\n console.warn(`[Agy Auth] onboardManagedProject calling URL: ${formatHyperlink(onboardUrl)} with project: ${projectId || 'none'}`);\n }\n\n try {\n const response = await fetchWithDebug(\n onboardUrl,\n 'POST',\n buildCodeAssistHeaders(accessToken, userAgentModel),\n requestBody,\n projectId\n );\n if (!response.ok) {\n const cleanStatusText = response.statusText.replace(/[\\r\\n]+/g, ' ').trim();\n console.warn(`[Agy Auth] onboardManagedProject response not ok: status ${response.status} ${cleanStatusText}`);\n return undefined;\n }\n\n let payload = (await response.json()) as OnboardUserPayload;\n if (!payload.done && payload.name) {\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n await wait(delayMs);\n const operationUrl = `${baseUrl}/${payload.name}`;\n const opResponse = await fetchWithDebug(\n operationUrl,\n 'GET',\n buildCodeAssistHeaders(accessToken, userAgentModel),\n undefined,\n projectId\n );\n if (!opResponse.ok) {\n return undefined;\n }\n payload = (await opResponse.json()) as OnboardUserPayload;\n if (payload.done) {\n break;\n }\n }\n }\n\n const managedProjectId = payload.response?.cloudaicompanionProject?.id;\n if (payload.done && managedProjectId) {\n return managedProjectId;\n }\n if (payload.done && projectId) {\n return projectId;\n }\n } catch (error) {\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to onboard Antigravity managed project: ${errStr}`);\n return undefined;\n }\n\n return undefined;\n}\n\nfunction buildCodeAssistHeaders(\n accessToken: string,\n userAgentModel?: string\n): Record {\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${accessToken}`,\n 'User-Agent': userAgent\n };\n}\n\nasync function fetchWithDebug(\n url: string,\n method: 'GET' | 'POST',\n headers: Record,\n body: Record | undefined,\n projectId?: string\n): Promise {\n const response = await agyFetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined\n });\n return response;\n}\n\nasync function readResponseTextIfNeeded(response: Response, needed: boolean): Promise {\n if (!needed && response.ok) {\n return undefined;\n }\n try {\n return await response.clone().text();\n } catch {\n return undefined;\n }\n}\n","import { AGY_PROVIDER_ID } from '../../constants';\nimport { formatRefreshParts, parseRefreshParts } from '../auth';\nimport type { OAuthAuthDetails, PluginClient, ProjectContextResult } from '../types';\nimport { loadManagedProject, onboardManagedProject } from '../../sdk/fetch_project';\nimport { FREE_TIER_ID, LEGACY_TIER_ID, ProjectIdRequiredError, ProjectAccessDeniedError, type LoadCodeAssistPayload } from './types';\nimport {\n buildIneligibleTierMessage,\n getCacheKey,\n normalizeProjectId,\n pickOnboardTier,\n throwIfValidationRequired\n} from './utils';\n\nconst projectContextResultCache = new Map();\nconst projectContextPendingCache = new Map>();\n\n/**\n * Clears cached project context results and pending Promises.\n */\nexport function invalidateProjectContextCache(refresh?: string): void {\n if (!refresh) {\n projectContextPendingCache.clear();\n projectContextResultCache.clear();\n return;\n }\n\n projectContextPendingCache.delete(refresh);\n projectContextResultCache.delete(refresh);\n\n const prefix = `${refresh}|cfg:`;\n for (const key of projectContextPendingCache.keys()) {\n if (key.startsWith(prefix)) {\n projectContextPendingCache.delete(key);\n }\n }\n for (const key of projectContextResultCache.keys()) {\n if (key.startsWith(prefix)) {\n projectContextResultCache.delete(key);\n }\n }\n}\n\n/**\n * Resolves the project context corresponding to the access token, optionally persisting updated auth details.\n */\nexport async function resolveProjectContextFromAccessToken(\n auth: OAuthAuthDetails,\n accessToken: string,\n configuredProjectId?: string,\n persistAuth?: (auth: OAuthAuthDetails) => Promise,\n userAgentModel?: string\n): Promise {\n const parts = parseRefreshParts(auth.refresh);\n const configuredProject = configuredProjectId?.trim();\n const projectId = configuredProject || parts.projectId;\n\n if (!configuredProject && (projectId || parts.managedProjectId)) {\n return {\n auth,\n effectiveProjectId: projectId || parts.managedProjectId || ''\n };\n }\n\n let loadPayload: LoadCodeAssistPayload | null = null;\n try {\n loadPayload = await loadManagedProject(accessToken, projectId, userAgentModel);\n } catch (error) {\n if (error instanceof ProjectAccessDeniedError) {\n throw error;\n }\n console.warn(`[Agy Auth] loadManagedProject returned an error for project: ${projectId || 'none'}, ${error}`);\n }\n\n if (!loadPayload) {\n console.warn(`[Agy Auth] loadManagedProject returned null for project: ${projectId || 'none'} (possible 429 rate limit / quota exhaustion / transient backend error - not necessarily a missing project config)`);\n if (projectId) {\n throw new Error(\n `Failed to load project context for '${projectId}'. This may be due to a rate limit, quota exhaustion, or transient backend error. Please try again later.`\n );\n }\n throw new ProjectIdRequiredError();\n }\n\n const managedProjectId = normalizeProjectId(loadPayload.cloudaicompanionProject);\n if (managedProjectId) {\n const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, managedProjectId);\n if (persistAuth) {\n await persistAuth(updatedAuth);\n }\n return { auth: updatedAuth, effectiveProjectId: managedProjectId };\n }\n\n const currentTierId = loadPayload.currentTier?.id;\n if (!currentTierId) {\n throwIfValidationRequired(loadPayload.ineligibleTiers);\n }\n\n if (currentTierId) {\n if (projectId) {\n return { auth, effectiveProjectId: projectId };\n }\n const ineligibleMessage = buildIneligibleTierMessage(loadPayload.ineligibleTiers);\n if (ineligibleMessage) {\n throw new Error(ineligibleMessage);\n }\n throw new ProjectIdRequiredError();\n }\n\n const tier = pickOnboardTier(loadPayload.allowedTiers);\n const tierId = tier.id ?? LEGACY_TIER_ID;\n if (tierId !== FREE_TIER_ID && !projectId) {\n throw new ProjectIdRequiredError();\n }\n\n const onboardedProjectId = await onboardManagedProject(\n accessToken,\n tierId,\n projectId,\n userAgentModel\n );\n if (onboardedProjectId) {\n const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, onboardedProjectId);\n if (persistAuth) {\n await persistAuth(updatedAuth);\n }\n return { auth: updatedAuth, effectiveProjectId: onboardedProjectId };\n }\n\n if (projectId) {\n return { auth, effectiveProjectId: projectId };\n }\n console.warn(`[Agy Auth] onboardManagedProject failed to resolve a project ID for tier: ${tierId}, configured project: ${projectId || 'none'}`);\n throw new ProjectIdRequiredError();\n}\n\n/**\n * Resolves the effective project ID for the current auth state and caches the result by refresh token.\n */\nexport async function ensureProjectContext(\n auth: OAuthAuthDetails,\n client: PluginClient,\n configuredProjectId?: string,\n userAgentModel?: string\n): Promise {\n const accessToken = auth.access;\n if (!accessToken) {\n return { auth, effectiveProjectId: '' };\n }\n\n const cacheKey = buildProjectCacheKey(auth, configuredProjectId);\n if (cacheKey) {\n const cached = projectContextResultCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n const pending = projectContextPendingCache.get(cacheKey);\n if (pending) {\n return pending;\n }\n }\n\n const resolveContext = async (): Promise =>\n resolveProjectContextFromAccessToken(\n auth,\n accessToken,\n configuredProjectId,\n async (updatedAuth) => {\n await client.auth.set({\n path: { id: AGY_PROVIDER_ID },\n body: updatedAuth\n });\n },\n userAgentModel\n );\n\n if (!cacheKey) {\n return resolveContext();\n }\n\n const promise = resolveContext()\n .then((result) => {\n const nextKey = getCacheKey(result.auth) ?? cacheKey;\n projectContextPendingCache.delete(cacheKey);\n projectContextResultCache.set(nextKey, result);\n if (nextKey !== cacheKey) {\n projectContextResultCache.delete(cacheKey);\n }\n return result;\n })\n .catch((error) => {\n projectContextPendingCache.delete(cacheKey);\n throw error;\n });\n\n projectContextPendingCache.set(cacheKey, promise);\n return promise;\n}\n\nfunction withProjectAuth(\n auth: OAuthAuthDetails,\n refreshToken: string,\n projectId: string | undefined,\n managedProjectId: string\n): OAuthAuthDetails {\n return {\n ...auth,\n refresh: formatRefreshParts({\n refreshToken,\n projectId,\n managedProjectId\n })\n };\n}\n\nfunction buildProjectCacheKey(auth: OAuthAuthDetails, configuredProjectId?: string): string | undefined {\n const base = getCacheKey(auth);\n if (!base) {\n return undefined;\n }\n const project = configuredProjectId?.trim() ?? '';\n return project ? `${base}|cfg:${project}` : base;\n}\n","import type { Config } from \"./types\";\n\nimport { AGY_PROVIDER_ID } from \"../constants\";\nimport type { PluginClient, Provider } from \"./types\";\n\ninterface ResolveConfiguredProjectIdInput {\n provider?: Provider | null;\n config?: Config | null;\n configProjectId?: string;\n env?: NodeJS.ProcessEnv;\n}\n\nexport function resolveConfiguredProjectId(\n input: ResolveConfiguredProjectIdInput = {},\n): string | undefined {\n const env = input.env ?? process.env;\n\n return (\n normalizeProjectId(env.OPENCODE_AGY_PROJECT_ID) ??\n resolveConfiguredProjectIdFromProvider(input.provider) ??\n normalizeProjectId(input.configProjectId) ??\n resolveConfiguredProjectIdFromConfig(input.config) ??\n normalizeProjectId(env.GOOGLE_CLOUD_PROJECT) ??\n normalizeProjectId(env.GOOGLE_CLOUD_PROJECT_ID)\n );\n}\n\nfunction resolveConfiguredProjectIdFromProvider(\n provider: Provider | null | undefined,\n): string | undefined {\n if (!provider || typeof provider !== \"object\") {\n return undefined;\n }\n return normalizeProjectId(provider.options?.projectId);\n}\n\nexport function resolveConfiguredProjectIdFromConfig(\n config: Config | null | undefined,\n): string | undefined {\n if (!config?.provider || typeof config.provider !== \"object\") {\n return undefined;\n }\n\n const providerConfig = config.provider[AGY_PROVIDER_ID];\n return normalizeProjectId(providerConfig?.options?.projectId);\n}\n\nexport async function resolveConfiguredProjectIdFromClient(\n client: PluginClient | null | undefined,\n): Promise {\n if (!client?.config?.get) {\n return undefined;\n }\n\n try {\n const result = await client.config.get();\n return resolveConfiguredProjectIdFromConfig(result?.data);\n } catch {\n return undefined;\n }\n}\n\nfunction normalizeProjectId(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed || undefined;\n}\n","import { createHash } from \"node:crypto\";\nimport { accessTokenExpired } from \"./auth\";\nimport type { OAuthAuthDetails } from \"./types\";\nimport { SignatureCache, createSignatureCache, type SignatureCacheConfig } from \"../sdk/cache/signature-cache\";\n\n// In-memory cache of authorized account details.\nconst authCache = new Map();\n\n/**\n * Normalizes and trims whitespace from the refresh token string.\n */\nfunction normalizeRefreshKey(refresh?: string): string | undefined {\n const key = refresh?.trim();\n return key ? key : undefined;\n}\n\n/**\n * Extracts valid OAuthAuthDetails from cache. Reuses an available and unexpired Token if present, otherwise prioritizes the latest provided value.\n */\nexport function resolveCachedAuth(auth: OAuthAuthDetails): OAuthAuthDetails {\n const key = normalizeRefreshKey(auth.refresh);\n if (!key) {\n return auth;\n }\n\n const cached = authCache.get(key);\n if (!cached) {\n authCache.set(key, auth);\n return auth;\n }\n\n if (!accessTokenExpired(auth)) {\n authCache.set(key, auth);\n return auth;\n }\n\n if (!accessTokenExpired(cached)) {\n return cached;\n }\n\n authCache.set(key, auth);\n return auth;\n}\n\n/**\n * Explicitly updates or saves authorized token details to the cache.\n */\nexport function storeCachedAuth(auth: OAuthAuthDetails): void {\n const key = normalizeRefreshKey(auth.refresh);\n if (!key) {\n return;\n }\n authCache.set(key, auth);\n}\n\n/**\n * Clears cached login authorization details. If no refresh token is provided, clears the global cache.\n */\nexport function clearCachedAuth(refresh?: string): void {\n if (!refresh) {\n authCache.clear();\n return;\n }\n const key = normalizeRefreshKey(refresh);\n if (key) {\n authCache.delete(key);\n }\n}\n\n// ============================================================================\n// Thinking signature cache layer (supports self-healing alignment of Gemini and Claude signature states in multi-turn dialogues).\n// ============================================================================\n\ninterface SignatureEntry {\n signature: string;\n timestamp: number;\n}\n\n// In-memory cache layer: sessionId -> Map\nconst signatureCache = new Map>();\n\n// Cache validity period set to 1 hour.\nconst SIGNATURE_CACHE_TTL_MS = 60 * 60 * 1000;\n\n// Maximum cache size per session to prevent memory leaks when left open for a long time.\nconst MAX_ENTRIES_PER_SESSION = 100;\n\n// Use the first 16 hex characters of the sha256 result as the textHash key width.\nconst SIGNATURE_TEXT_HASH_HEX_LEN = 16;\n\n// Disk-level persistent cache instance.\nlet diskCache: SignatureCache | null = null;\n\n/**\n * Initializes the disk-level signature storage manager.\n */\nexport function initDiskSignatureCache(config: SignatureCacheConfig | undefined): SignatureCache | null {\n diskCache = createSignatureCache(config);\n return diskCache;\n}\n\n/**\n * Calculates a stable sha256 hash of the thought chain content, taking the first 16 characters as the unique key.\n */\nfunction hashText(text: string): string {\n return createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\").slice(0, SIGNATURE_TEXT_HASH_HEX_LEN);\n}\n\n/**\n * Builds the composite primary key sessionId:textHash on disk storage.\n */\nfunction makeDiskKey(sessionId: string, textHash: string): string {\n return `${sessionId}:${textHash}`;\n}\n\n// Latest signature mapping: sessionId -> most recent signature string.\nconst latestSignatureMap = new Map();\n\n/**\n * Caches a thought chain fragment and its corresponding service signature, synchronously saving it to disk.\n */\nexport function cacheSignature(sessionId: string, text: string, signature: string): void {\n if (!sessionId || !text || !signature) return;\n\n const textHash = hashText(text);\n\n let sessionMemCache = signatureCache.get(sessionId);\n if (!sessionMemCache) {\n sessionMemCache = new Map();\n signatureCache.set(sessionId, sessionMemCache);\n }\n\n // When capacity limit is exceeded, trigger LRU cleanup of expired entries.\n if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {\n const now = Date.now();\n for (const [key, entry] of sessionMemCache.entries()) {\n if (now - entry.timestamp > SIGNATURE_CACHE_TTL_MS) {\n sessionMemCache.delete(key);\n }\n }\n // If still over the limit, discard the oldest 25% of entries by timestamp.\n if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {\n const entries = Array.from(sessionMemCache.entries())\n .sort((a, b) => a[1].timestamp - b[1].timestamp);\n const toRemove = entries.slice(0, Math.floor(MAX_ENTRIES_PER_SESSION / 4));\n for (const [key] of toRemove) {\n sessionMemCache.delete(key);\n }\n }\n }\n\n sessionMemCache.set(textHash, { signature, timestamp: Date.now() });\n latestSignatureMap.set(sessionId, signature);\n\n // If disk persistence is enabled, write to disk synchronously.\n if (diskCache) {\n const diskKey = makeDiskKey(sessionId, textHash);\n diskCache.store(diskKey, signature);\n // Also store the latest signature value directly using sessionId as the key, for global signature recovery without textHash.\n diskCache.store(sessionId, signature);\n }\n}\n\n/**\n * Recovers and retrieves the most recently cached signature for a session (supports signature recovery).\n */\nexport function getLatestSignature(sessionId: string): string | undefined {\n if (!sessionId) return undefined;\n\n // Prioritize retrieval from in-memory cache.\n const memValue = latestSignatureMap.get(sessionId);\n if (memValue) return memValue;\n\n // In-memory miss, fallback to disk query.\n if (diskCache) {\n const diskValue = diskCache.retrieve(sessionId);\n if (diskValue) {\n // Automatically promote to in-memory Map after read, accelerating subsequent queries.\n latestSignatureMap.set(sessionId, diskValue);\n return diskValue;\n }\n }\n\n return undefined;\n}\n\nexport type { SignatureCache } from \"../sdk/cache/signature-cache\";\nexport type { SignatureCacheConfig } from \"../sdk/cache/signature-cache\";\n","import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\n\n// =============================================================================\n/**\n * NOTE: Special Design - Cross-turn signature disk-level persistent cache\n * Google Agy / Gemini 2.5/3 thinking models introduce strict \"Thought Signature Validation\" restrictions:\n * In multi-turn dialogues (especially with Tool calls), the next request must carry a context signature (thoughtSignature) exactly matching the previous API response.\n * To avoid conversation crashes caused by the following:\n * 1. IDE-side session lifecycle rebuilds, causing loss of in-memory signature states.\n * 2. Concurrent packets from multi-turn Tool interactions disrupting the memory cache of signatures.\n * We implement a disk cache layer here with a background thread that periodically flushes to disk. Using a combination of session ID (sessionId) and historical thought chain hash digest as the Key,\n * it persists the signatures and thought chains. Even if the IDE restarts or turns split, it can pull back the latest matching signature to fulfill official validation constraints.\n */\n// Types & Interfaces\n// =============================================================================\n\n/**\n * Signature cache configuration options\n */\nexport interface SignatureCacheConfig {\n /** Whether to enable caching */\n enabled: boolean;\n /** In-memory cache time-to-live (seconds) */\n memory_ttl_seconds: number;\n /** Disk cache time-to-live (seconds) */\n disk_ttl_seconds: number;\n /** Auto-save interval to disk (seconds) */\n write_interval_seconds: number;\n}\n\n/**\n * Single cached data entry\n */\ninterface CacheEntry {\n /** Cached value (e.g., signature) */\n value: string;\n /** Timestamp (milliseconds) */\n timestamp: number;\n /** Full thought chain text content (optional, for self-healing recovery after compression) */\n thinkingText?: string;\n /** Preview of thought chain text (for debug logs only) */\n textPreview?: string;\n /** List of tool call IDs associated with this thought chain block */\n toolIds?: string[];\n}\n\n/**\n * Complete cache structure saved to disk\n */\ninterface CacheData {\n /** Version number */\n version: \"1.0\";\n /** Memory TTL config */\n memory_ttl_seconds: number;\n /** Disk TTL config */\n disk_ttl_seconds: number;\n /** Cache entry mapping table */\n entries: Record;\n /** Statistics */\n statistics: {\n /** Memory hit count */\n memory_hits: number;\n /** Disk hit count */\n disk_hits: number;\n /** Miss count */\n misses: number;\n /** Disk write count */\n writes: number;\n /** Last disk write timestamp */\n last_write: number;\n };\n}\n\n/**\n * Cache runtime state and statistics\n */\ninterface CacheStats {\n /** Memory hit count */\n memoryHits: number;\n /** Disk hit count */\n diskHits: number;\n /** Miss count */\n misses: number;\n /** Disk write count */\n writes: number;\n /** Total number of entries currently in memory */\n memoryEntries: number;\n /** Whether the cache is dirty (has unsaved data) */\n dirty: boolean;\n /** Whether disk storage is enabled */\n diskEnabled: boolean;\n}\n\n/**\n * Retrieve full thought chain cache data structure\n */\nexport interface ThinkingCacheData {\n /** Thought chain text */\n text: string;\n /** Signature */\n signature: string;\n /** Associated tool ID list */\n toolIds?: string[];\n}\n\n// =============================================================================\n// Path & Storage Utilities\n// =============================================================================\n\n/**\n * Get plugin config save directory\n */\nfunction getConfigDir(): string {\n const platform = process.platform;\n if (platform === \"win32\") {\n return join(process.env.APPDATA || join(homedir(), \"AppData\", \"Roaming\"), \"opencode\");\n }\n const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), \".config\");\n return join(xdgConfig, \"opencode\");\n}\n\n/**\n * Get absolute path for saving cache file\n */\nfunction getCacheFilePath(): string {\n return join(getConfigDir(), \"antigravity-signature-cache.json\");\n}\n\n/**\n * Synchronously ensure the config file is added to .gitignore to avoid leaking signatures and cache\n */\nfunction ensureGitignoreSync(configDir: string): void {\n const gitignorePath = join(configDir, \".gitignore\");\n const entries = [\".gitignore\", \"antigravity-signature-cache.json\"];\n try {\n let content = \"\";\n if (existsSync(gitignorePath)) {\n content = readFileSync(gitignorePath, \"utf-8\");\n }\n const existingLines = content.split(\"\\n\").map((line) => line.trim());\n const missing = entries.filter((e) => !existingLines.includes(e));\n if (missing.length === 0) return;\n if (content === \"\") {\n writeFileSync(gitignorePath, missing.join(\"\\n\") + \"\\n\", \"utf-8\");\n } else {\n const suffix = content.endsWith(\"\\n\") ? \"\" : \"\\n\";\n appendFileSync(gitignorePath, suffix + missing.join(\"\\n\") + \"\\n\", \"utf-8\");\n }\n } catch {\n // Ignore exception: anti-leakage is a non-blocking secondary feature\n }\n}\n\n// =============================================================================\n// Signature Cache Manager\n// =============================================================================\n\nexport class SignatureCache {\n // Memory cache map\n private cache: Map = new Map();\n\n // Configuration options\n private memoryTtlMs: number;\n private diskTtlMs: number;\n private writeIntervalMs: number;\n private cacheFilePath: string;\n private enabled: boolean;\n\n // State variables\n private dirty: boolean = false;\n private writeTimer: ReturnType | null = null;\n private cleanupTimer: ReturnType | null = null;\n\n // Statistical metrics\n private stats = {\n memoryHits: 0,\n diskHits: 0,\n misses: 0,\n writes: 0,\n };\n\n constructor(config: SignatureCacheConfig) {\n this.enabled = config.enabled;\n this.memoryTtlMs = config.memory_ttl_seconds * 1000;\n this.diskTtlMs = config.disk_ttl_seconds * 1000;\n this.writeIntervalMs = config.write_interval_seconds * 1000;\n this.cacheFilePath = getCacheFilePath();\n\n if (this.enabled) {\n this.loadFromDisk();\n this.startBackgroundTasks();\n }\n }\n\n // ===========================================================================\n // Public Signature API\n // ===========================================================================\n\n /**\n * Generates a unique cache key based on session ID and model ID\n */\n static makeKey(sessionId: string, modelId: string): string {\n return `${sessionId}:${modelId}`;\n }\n\n /**\n * Stores a signature in cache (marks as dirty, awaits background disk write)\n */\n store(key: string, signature: string): void {\n if (!this.enabled) return;\n\n this.cache.set(key, {\n value: signature,\n timestamp: Date.now(),\n });\n this.dirty = true;\n }\n\n /**\n * Retrieves a signature from cache and updates hit stats\n * Returns null if expired or missing\n */\n retrieve(key: string): string | null {\n if (!this.enabled) return null;\n\n const entry = this.cache.get(key);\n if (entry) {\n const age = Date.now() - entry.timestamp;\n if (age <= this.memoryTtlMs) {\n this.stats.memoryHits++;\n return entry.value;\n }\n // Expired in memory, delete\n this.cache.delete(key);\n }\n\n this.stats.misses++;\n return null;\n }\n\n /**\n * Checks if a key is valid and unexpired in cache (without affecting stats)\n */\n has(key: string): boolean {\n if (!this.enabled) return false;\n\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n const age = Date.now() - entry.timestamp;\n return age <= this.memoryTtlMs;\n }\n\n // ===========================================================================\n // Full Thinking Cache API\n // ===========================================================================\n\n /**\n * Caches the full thought chain text content and signature\n * Allows self-healing and recovery of historical thought blocks even if the context is subsequently compressed.\n */\n storeThinking(\n key: string,\n thinkingText: string,\n signature: string,\n toolIds?: string[],\n ): void {\n if (!this.enabled || !thinkingText || !signature) return;\n\n this.cache.set(key, {\n value: signature,\n timestamp: Date.now(),\n thinkingText,\n textPreview: thinkingText.slice(0, 100),\n toolIds,\n });\n this.dirty = true;\n }\n\n /**\n * Extracts full thought chain info from cache\n */\n retrieveThinking(key: string): ThinkingCacheData | null {\n if (!this.enabled) return null;\n\n const entry = this.cache.get(key);\n if (!entry || !entry.thinkingText) return null;\n\n const age = Date.now() - entry.timestamp;\n if (age > this.memoryTtlMs) {\n this.cache.delete(key);\n return null;\n }\n\n this.stats.memoryHits++;\n return {\n text: entry.thinkingText,\n signature: entry.value,\n toolIds: entry.toolIds,\n };\n }\n\n /**\n * Checks if full thought chain content exists for a key\n */\n hasThinking(key: string): boolean {\n if (!this.enabled) return false;\n\n const entry = this.cache.get(key);\n if (!entry || !entry.thinkingText) return false;\n\n const age = Date.now() - entry.timestamp;\n return age <= this.memoryTtlMs;\n }\n\n /**\n * Gets current cache stats and memory footprint\n */\n getStats(): CacheStats {\n return {\n ...this.stats,\n memoryEntries: this.cache.size,\n dirty: this.dirty,\n diskEnabled: this.enabled,\n };\n }\n\n /**\n * Manually triggers immediate save to disk\n */\n async flush(): Promise {\n if (!this.enabled) return true;\n return this.saveToDisk();\n }\n\n /**\n * Graceful shutdown: stops all timers and flushes unsaved data to disk\n */\n shutdown(): void {\n if (this.writeTimer) {\n clearInterval(this.writeTimer);\n this.writeTimer = null;\n }\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n\n if (this.dirty && this.enabled) {\n this.saveToDisk();\n }\n }\n\n // ===========================================================================\n // Disk Operations\n // ===========================================================================\n\n /**\n * Loads signature cache from disk and validates TTL state\n */\n private loadFromDisk(): void {\n try {\n if (!existsSync(this.cacheFilePath)) {\n return;\n }\n\n const content = readFileSync(this.cacheFilePath, \"utf-8\");\n const data = JSON.parse(content) as CacheData;\n\n if (data.version !== \"1.0\") {\n // On version mismatch, silently ignore and start a fresh cache\n return;\n }\n\n const now = Date.now();\n for (const [key, entry] of Object.entries(data.entries)) {\n const age = now - entry.timestamp;\n if (age <= this.diskTtlMs) {\n this.cache.set(key, {\n value: entry.value,\n timestamp: entry.timestamp,\n thinkingText: entry.thinkingText,\n textPreview: entry.textPreview,\n toolIds: entry.toolIds,\n });\n }\n }\n } catch {\n // Fault tolerance: silently start fresh memory cache on disk load failure\n }\n }\n\n /**\n * Synchronously saves memory cache to disk (using atomic write: temp file then rename)\n * Merges with existing unexpired entries on disk during write\n */\n private saveToDisk(): boolean {\n try {\n const dir = dirname(this.cacheFilePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n ensureGitignoreSync(dir);\n\n const now = Date.now();\n\n // Step 1: Read existing disk entries\n let existingEntries: Record = {};\n if (existsSync(this.cacheFilePath)) {\n try {\n const content = readFileSync(this.cacheFilePath, \"utf-8\");\n const data = JSON.parse(content) as CacheData;\n existingEntries = data.entries || {};\n } catch {\n // Fault tolerance: overwrite if malformed or corrupted\n }\n }\n\n // Step 2: Filter expired disk entries\n const validDiskEntries: Record = {};\n for (const [key, entry] of Object.entries(existingEntries)) {\n const age = now - entry.timestamp;\n if (age <= this.diskTtlMs) {\n validDiskEntries[key] = entry;\n }\n }\n\n // Step 3: Overwrite or merge memory cache into valid disk entries (memory data is newest, highest priority)\n const mergedEntries: Record = { ...validDiskEntries };\n for (const [key, entry] of this.cache.entries()) {\n mergedEntries[key] = {\n value: entry.value,\n timestamp: entry.timestamp,\n thinkingText: entry.thinkingText,\n textPreview: entry.textPreview,\n toolIds: entry.toolIds,\n };\n }\n\n // Step 4: Build persistent structure\n const cacheData: CacheData = {\n version: \"1.0\",\n memory_ttl_seconds: this.memoryTtlMs / 1000,\n disk_ttl_seconds: this.diskTtlMs / 1000,\n entries: mergedEntries,\n statistics: {\n memory_hits: this.stats.memoryHits,\n disk_hits: this.stats.diskHits,\n misses: this.stats.misses,\n writes: this.stats.writes + 1,\n last_write: now,\n },\n };\n\n // Step 5: Atomic write (write to temp file, rename to finalize, preventing data corruption)\n const tmpPath = join(tmpdir(), `antigravity-cache-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);\n writeFileSync(tmpPath, JSON.stringify(cacheData, null, 2), \"utf-8\");\n\n try {\n renameSync(tmpPath, this.cacheFilePath);\n } catch {\n // Cross-volume rename on Windows might fail, fallback to copy + delete\n writeFileSync(this.cacheFilePath, readFileSync(tmpPath));\n try {\n unlinkSync(tmpPath);\n } catch {\n // Ignore temp file deletion failure\n }\n }\n\n this.stats.writes++;\n this.dirty = false;\n return true;\n } catch {\n // Disk writing is non-core, fail silently\n return false;\n }\n }\n\n // ===========================================================================\n // Background Tasks\n // ===========================================================================\n\n /**\n * Starts timers for auto-saving and auto-cleaning expired memory entries\n */\n private startBackgroundTasks(): void {\n // Periodic disk write (if modified)\n this.writeTimer = setInterval(() => {\n if (this.dirty) {\n this.saveToDisk();\n }\n }, this.writeIntervalMs);\n\n // Perform memory garbage collection every 30 mins to free unused space\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired();\n }, 30 * 60 * 1000);\n }\n\n /**\n * Removes memory cache entries exceeding their TTL\n */\n private cleanupExpired(): void {\n const now = Date.now();\n for (const [key, entry] of this.cache.entries()) {\n const age = now - entry.timestamp;\n if (age > this.memoryTtlMs) {\n this.cache.delete(key);\n }\n }\n }\n}\n\n// =============================================================================\n// Factory function\n// =============================================================================\n\n/**\n * Instantiates signature cache object based on config. Returns null if disabled.\n */\nexport function createSignatureCache(config: SignatureCacheConfig | undefined): SignatureCache | null {\n if (!config || !config.enabled) {\n return null;\n }\n return new SignatureCache(config);\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { analyzeConversationState, type ConversationState } from \"./thinking\";\n\nexport type TurnState = Pick;\n\ninterface TurnStateRecord {\n state: TurnState;\n updatedAt: number;\n}\n\ninterface TurnStateData {\n version: \"1.0\";\n entries: Record;\n updatedAt: number;\n}\n\nconst WRITE_THROTTLE_MS = 5000;\n\nfunction getConfigDir(): string {\n const platform = process.platform;\n if (platform === \"win32\") {\n return join(process.env.APPDATA || join(homedir(), \"AppData\", \"Roaming\"), \"opencode\");\n }\n const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), \".config\");\n return join(xdgConfig, \"opencode\");\n}\n\nfunction getTurnStateFilePath(): string {\n return join(getConfigDir(), \"antigravity-turn-states.json\");\n}\n\nfunction loadTurnStatesFromDisk(): Map {\n const result = new Map();\n try {\n const filePath = getTurnStateFilePath();\n if (!existsSync(filePath)) {\n return result;\n }\n\n const content = readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(content) as TurnStateData;\n if (data.version !== \"1.0\") {\n return result;\n }\n\n const now = Date.now();\n const maxAge = 24 * 60 * 60 * 1000;\n for (const [key, record] of Object.entries(data.entries)) {\n if (record.state && typeof record.state === \"object\" && now - record.updatedAt < maxAge) {\n result.set(key, record);\n }\n }\n } catch {\n }\n return result;\n}\n\nfunction saveTurnStatesToDisk(entries: Map): boolean {\n try {\n const filePath = getTurnStateFilePath();\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const now = Date.now();\n const maxAge = 24 * 60 * 60 * 1000;\n const serializable: Record = {};\n for (const [key, record] of entries.entries()) {\n if (now - record.updatedAt < maxAge) {\n serializable[key] = record;\n }\n }\n\n const data: TurnStateData = {\n version: \"1.0\",\n entries: serializable,\n updatedAt: now,\n };\n\n const tmpPath = join(tmpdir(), `antigravity-turn-states-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);\n writeFileSync(tmpPath, JSON.stringify(data), \"utf-8\");\n\n try {\n renameSync(tmpPath, filePath);\n } catch {\n writeFileSync(filePath, readFileSync(tmpPath));\n try {\n unlinkSync(tmpPath);\n } catch {\n }\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport class TurnStateTracker {\n private entries = new Map();\n private dirty = false;\n private lastWriteTime = 0;\n private writeTimer: ReturnType | null = null;\n private readonly diskEnabled: boolean;\n\n constructor(diskEnabled = true) {\n this.diskEnabled = diskEnabled;\n if (diskEnabled) {\n this.entries = loadTurnStatesFromDisk();\n }\n }\n\n getState(sessionId: string): TurnState | undefined {\n const record = this.entries.get(sessionId);\n if (!record) return undefined;\n return record.state;\n }\n\n needsThinkingRecovery(sessionId: string): boolean {\n const state = this.entries.get(sessionId);\n if (!state) return false;\n return state.state.inToolLoop && !state.state.turnHasThinking;\n }\n\n updateAfterResponse(sessionId: string, newState: TurnState): void {\n this.entries.set(sessionId, { state: newState, updatedAt: Date.now() });\n this.dirty = true;\n this.scheduleThrottledWrite();\n }\n\n recoverFromContents(sessionId: string, contents: any[]): TurnState {\n const fullState = analyzeConversationState(contents);\n const turnState: TurnState = {\n inToolLoop: fullState.inToolLoop,\n turnHasThinking: fullState.turnHasThinking,\n lastModelHasThinking: fullState.lastModelHasThinking,\n lastModelHasToolCalls: fullState.lastModelHasToolCalls,\n };\n this.entries.set(sessionId, { state: turnState, updatedAt: Date.now() });\n this.dirty = true;\n this.scheduleThrottledWrite();\n return turnState;\n }\n\n clear(sessionId: string): void {\n this.entries.delete(sessionId);\n this.dirty = true;\n this.scheduleThrottledWrite();\n }\n\n shutdown(): void {\n this.clearWriteTimer();\n if (this.dirty && this.diskEnabled) {\n saveTurnStatesToDisk(this.entries);\n this.dirty = false;\n }\n }\n\n private scheduleThrottledWrite(): void {\n if (!this.diskEnabled) return;\n if (this.writeTimer) {\n return;\n }\n\n const elapsed = Date.now() - this.lastWriteTime;\n const remaining = Math.max(0, WRITE_THROTTLE_MS - elapsed);\n\n this.writeTimer = setTimeout(() => {\n this.writeTimer = null;\n this.lastWriteTime = Date.now();\n if (this.dirty) {\n this.dirty = false;\n saveTurnStatesToDisk(this.entries);\n }\n }, remaining);\n\n if (this.writeTimer && typeof this.writeTimer === \"object\" && \"unref\" in this.writeTimer) {\n this.writeTimer.unref();\n }\n }\n\n private clearWriteTimer(): void {\n if (this.writeTimer) {\n clearTimeout(this.writeTimer);\n this.writeTimer = null;\n }\n }\n}\n\nlet trackerInstance: TurnStateTracker | null = null;\n\nexport function initTurnStateTracker(): TurnStateTracker {\n if (!trackerInstance) {\n try {\n trackerInstance = new TurnStateTracker(true);\n if (typeof process !== \"undefined\") {\n process.on(\"exit\", () => {\n trackerInstance?.shutdown();\n });\n }\n } catch {\n trackerInstance = new TurnStateTracker(false);\n }\n }\n return trackerInstance;\n}\n\nexport function getTurnStateTracker(): TurnStateTracker | null {\n return trackerInstance;\n}\n\nexport function shutdownTurnStateTracker(): void {\n if (trackerInstance) {\n trackerInstance.shutdown();\n trackerInstance = null;\n }\n}\n","import { createHash } from \"node:crypto\";\n\n/**\n * NOTE: Special Design - Streaming deduplication and signature state self-healing\n * Agy/Gemini official API has the following non-standard behaviors and strict constraints that must be specially handled here:\n * 1. [Streaming Deduplication]: In the data packets returned by the official API during streaming, thought chain (Thinking) content is output cumulatively.\n * We must perform hash comparison and truncation on each returned delta to prevent the IDE from receiving duplicate text.\n * 2. [Signature State Self-healing]: In multi-turn dialogues, if the thoughtSignature is lost due to tool calls or client state breakage,\n * the official API throws a signature mismatch error. An auto-detection mechanism (e.g., needsThinkingRecovery) is designed here to, upon signature breakage,\n * automatically backfill/align fallback thought chain fragments and signatures in context messages, allowing the dialogue chain to self-heal and continue.\n */\n\n// ============================================================================\n// Types & Interfaces\n// ============================================================================\n\n/**\n * Cached signed thought chain data structure\n */\nexport interface SignedThinking {\n /** Full thought chain text content */\n text: string;\n /** Corresponding server signature */\n signature: string;\n}\n\n/**\n * External contract interface for signature storage manager\n */\nexport interface SignatureStore {\n get(sessionKey: string): SignedThinking | undefined;\n set(sessionKey: string, value: SignedThinking): void;\n has(sessionKey: string): boolean;\n delete(sessionKey: string): void;\n}\n\n/**\n * Custom callback functions for the streaming phase\n */\nexport interface StreamingCallbacks {\n onCacheSignature?: (sessionKey: string, text: string, signature: string) => void;\n onInjectDebug?: (response: unknown, debugText: string) => unknown;\n transformThinkingParts?: (parts: unknown) => unknown;\n onTurnStateUpdate?: (sessionKey: string, state: { turnHasThinking: boolean; lastModelHasToolCalls: boolean }) => void;\n}\n\n/**\n * Configuration parameters for the streaming phase\n */\nexport interface StreamingOptions {\n /** Unique identifier for the signature session, used for cross-turn signature recovery */\n signatureSessionKey?: string;\n /** Debugging text to inject into the stream (optional) */\n debugText?: string;\n /** Whether to cache the latest generated signature in this stream */\n cacheSignatures?: boolean;\n /** Set of already rendered thought chain hashes, used to avoid duplicate output in tool call loops */\n displayedThinkingHashes?: Set;\n}\n\n/**\n * Text buffer for caching thought chains of a specific index or type (handles streaming chunk cumulative output)\n */\nexport interface ThoughtBuffer {\n get(index: number): string | undefined;\n set(index: number, text: string): void;\n clear(): void;\n}\n\n/**\n * Agent and tool interaction state record during multi-turn dialogue runtime\n */\nexport interface ConversationState {\n /** Whether inside an incomplete tool call loop (i.e., last turn ended with functionResponse, continuing this turn) */\n inToolLoop: boolean;\n /** Array index of the first model reply message in the current dialogue turn */\n turnStartIdx: number;\n /** Whether the start of the current dialogue turn contains a thought chain (thought) */\n turnHasThinking: boolean;\n /** Array index of the last model reply message */\n lastModelIdx: number;\n /** Whether the last model message contains a thought chain */\n lastModelHasThinking: boolean;\n /** Whether the last model message contains a tool call (tool_use) */\n lastModelHasToolCalls: boolean;\n}\n\n// ============================================================================\n// Stores & Buffers Factories\n// ============================================================================\n\n/**\n * Creates a memory Map-based signature storage manager\n */\nexport function createSignatureStore(): SignatureStore {\n const store = new Map();\n\n return {\n get: (key: string) => store.get(key),\n set: (key: string, value: SignedThinking) => {\n store.set(key, value);\n },\n has: (key: string) => store.has(key),\n delete: (key: string) => {\n store.delete(key);\n },\n };\n}\n\n/**\n * Creates a thought chain text accumulation buffer for temporarily storing streaming chunks\n */\nexport function createThoughtBuffer(): ThoughtBuffer {\n const buffer = new Map();\n\n return {\n get: (index: number) => buffer.get(index),\n set: (index: number, text: string) => {\n buffer.set(index, text);\n },\n clear: () => buffer.clear(),\n };\n}\n\n/**\n * Default global memory signature storage\n */\nexport const defaultSignatureStore = createSignatureStore();\n\n// ============================================================================\n// Hashing Helper\n// ============================================================================\n\nconst THINKING_HASH_HEX_LEN = 16;\n\n/**\n * Generates a SHA-256 hash of the thought chain content, truncated to 16 hex characters.\n * Consistent with the signature cache hash width in cache.ts.\n */\nfunction hashString(str: string): string {\n return createHash(\"sha256\").update(str, \"utf8\").digest(\"hex\").slice(0, THINKING_HASH_HEX_LEN);\n}\n\n// ============================================================================\n// Thinking State Detection & Historical Self-healing Helpers\n// ============================================================================\n\n/**\n * Checks if a message part belongs to the thought chain type\n * Compatible with Gemini (thought field) and Claude (type: \"thinking\", etc.)\n */\nfunction isThinkingPart(part: any): boolean {\n if (!part || typeof part !== \"object\") return false;\n return (\n part.thought === true ||\n part.type === \"thinking\" ||\n part.type === \"redacted_thinking\"\n );\n}\n\n/**\n * Checks if a message part is a tool execution result response\n */\nfunction isFunctionResponsePart(part: any): boolean {\n return part && typeof part === \"object\" && \"functionResponse\" in part;\n}\n\n/**\n * Checks if a message part is a model-initiated tool call request\n */\nfunction isFunctionCallPart(part: any): boolean {\n return part && typeof part === \"object\" && \"functionCall\" in part;\n}\n\n/**\n * Checks if a message is exclusively a tool execution return body (role = user but entirely functionResponse)\n */\nfunction isToolResultMessage(msg: any): boolean {\n if (!msg || msg.role !== \"user\") return false;\n const parts = msg.parts || [];\n return parts.some(isFunctionResponsePart);\n}\n\n/**\n * Checks if a message contains any thought chain segment (including Gemini array format or Claude content format)\n */\nfunction messageHasThinking(msg: any): boolean {\n if (!msg || typeof msg !== \"object\") return false;\n\n if (Array.isArray(msg.parts)) {\n return msg.parts.some(isThinkingPart);\n }\n\n if (Array.isArray(msg.content)) {\n return msg.content.some(\n (block: any) =>\n block?.type === \"thinking\" || block?.type === \"redacted_thinking\",\n );\n }\n\n return false;\n}\n\n/**\n * Checks if a message contains a tool call (supports Gemini and Claude tool_use)\n */\nfunction messageHasToolCalls(msg: any): boolean {\n if (!msg || typeof msg !== \"object\") return false;\n\n if (Array.isArray(msg.parts)) {\n return msg.parts.some(isFunctionCallPart);\n }\n\n if (Array.isArray(msg.content)) {\n return msg.content.some((block: any) => block?.type === \"tool_use\");\n }\n\n return false;\n}\n\n/**\n * Analyzes multi-turn historical dialogue arrays to extract context metrics like agent interaction loop state, model message positions, whether the turn has thoughts, etc.\n */\nexport function analyzeConversationState(contents: any[]): ConversationState {\n const state: ConversationState = {\n inToolLoop: false,\n turnStartIdx: -1,\n turnHasThinking: false,\n lastModelIdx: -1,\n lastModelHasThinking: false,\n lastModelHasToolCalls: false,\n };\n\n if (!Array.isArray(contents) || contents.length === 0) {\n return state;\n }\n\n // Find the position of the last real human user question\n let lastRealUserIdx = -1;\n for (let i = 0; i < contents.length; i++) {\n const msg = contents[i];\n if (msg?.role === \"user\" && !isToolResultMessage(msg)) {\n lastRealUserIdx = i;\n }\n }\n\n // Scan all model messages to extract features of the most recent reply round\n for (let i = 0; i < contents.length; i++) {\n const msg = contents[i];\n const role = msg?.role;\n\n if (role === \"model\" || role === \"assistant\") {\n const hasThinking = messageHasThinking(msg);\n const hasToolCalls = messageHasToolCalls(msg);\n\n // If this is the first model message after the most recent user question, treat as the start of the current turn\n if (i > lastRealUserIdx && state.turnStartIdx === -1) {\n state.turnStartIdx = i;\n state.turnHasThinking = hasThinking;\n }\n\n state.lastModelIdx = i;\n state.lastModelHasToolCalls = hasToolCalls;\n state.lastModelHasThinking = hasThinking;\n }\n }\n\n // Determine if in the tail of an incomplete tool loop (i.e., user just returned tool call results, waiting for model's next step)\n if (contents.length > 0) {\n const lastMsg = contents[contents.length - 1];\n if (lastMsg?.role === \"user\" && isToolResultMessage(lastMsg)) {\n state.inToolLoop = true;\n }\n }\n\n return state;\n}\n\n/**\n * Calculate the number of uncompleted tool responses at the tail\n */\nfunction countTrailingToolResults(contents: any[]): number {\n let count = 0;\n\n for (let i = contents.length - 1; i >= 0; i--) {\n const msg = contents[i];\n\n if (msg?.role === \"user\") {\n const parts = msg.parts || [];\n const functionResponses = parts.filter(isFunctionResponsePart);\n\n if (functionResponses.length > 0) {\n count += functionResponses.length;\n } else {\n break;\n }\n } else if (msg?.role === \"model\" || msg?.role === \"assistant\") {\n break;\n }\n }\n\n return count;\n}\n\n/**\n * Closes the tool execution loop and injects transition content to smoothly recover the dialogue without providing the old thought chain\n */\nexport function closeToolLoopForThinking(contents: any[]): any[] {\n const strippedContents = contents;\n const toolResultCount = countTrailingToolResults(strippedContents);\n let syntheticModelContent: string;\n\n if (toolResultCount === 0) {\n syntheticModelContent = \"[Processing prev ctx.]\";\n } else if (toolResultCount === 1) {\n syntheticModelContent = \"[Tool exec completed.]\";\n } else {\n syntheticModelContent = `[${toolResultCount} tool executions completed.]`;\n }\n\n const syntheticModel = {\n role: \"model\",\n parts: [{ text: syntheticModelContent }],\n };\n\n const syntheticUser = {\n role: \"user\",\n parts: [{ text: \"[Continue]\" }],\n };\n\n return [...strippedContents, syntheticModel, syntheticUser];\n}\n\n/**\n * Checks if the current state meets conditions to trigger historical self-healing\n */\nexport function needsThinkingRecovery(state: ConversationState): boolean {\n return state.inToolLoop && !state.turnHasThinking;\n}\n\n/**\n * Determines if the current model reply message had its thought chain pruned (has only tool calls but lost its preceding thought chain description)\n */\nexport function looksLikeCompactedThinkingTurn(msg: any): boolean {\n if (!msg || typeof msg !== \"object\") return false;\n\n const parts = msg.parts || [];\n if (parts.length === 0) return false;\n\n const hasFunctionCall = parts.some(\n (p: any) => p && typeof p === \"object\" && p.functionCall,\n );\n\n if (!hasFunctionCall) return false;\n\n const hasThinking = parts.some(\n (p: any) =>\n p &&\n typeof p === \"object\" &&\n (p.thought === true || p.type === \"thinking\" || p.type === \"redacted_thinking\"),\n );\n\n if (hasThinking) return false;\n\n const hasTextBeforeFunctionCall = parts.some((p: any, idx: number) => {\n if (!p || typeof p !== \"object\") return false;\n const firstFuncIdx = parts.findIndex(\n (fp: any) => fp && typeof fp === \"object\" && fp.functionCall,\n );\n if (idx >= firstFuncIdx) return false;\n return (\n \"text\" in p &&\n typeof p.text === \"string\" &&\n p.text.trim().length > 0 &&\n !p.thought\n );\n });\n\n return !hasTextBeforeFunctionCall;\n}\n\n/**\n * Deeply determines if the start of this Turn contains historical rounds whose thought chains might have been pruned/compressed by the system\n */\nexport function hasPossibleCompactedThinking(\n contents: any[],\n turnStartIdx: number,\n): boolean {\n if (!Array.isArray(contents) || turnStartIdx < 0) return false;\n\n for (let i = turnStartIdx; i < contents.length; i++) {\n const msg = contents[i];\n if (msg?.role === \"model\" && looksLikeCompactedThinkingTurn(msg)) {\n return true;\n }\n }\n\n return false;\n}\n\n// ============================================================================\n// SSE Deduplication Filter Processors (Streaming Transformers & Deduplicators)\n// ============================================================================\n\n/**\n * For streaming SSE data packets, calculates and locally strips duplicated thought chain text\n * Simultaneously supports Gemini's exclusive candidates.content structure and Claude's exclusive content[type=thinking] structure\n */\nexport function deduplicateThinkingText(\n response: unknown,\n sentBuffer: ThoughtBuffer,\n displayedThinkingHashes?: Set,\n): unknown {\n if (!response || typeof response !== \"object\") return response;\n\n const resp = response as Record;\n\n // Branch 1: Handle Gemini structure type (candidates)\n if (Array.isArray(resp.candidates)) {\n const newCandidates = resp.candidates.map((candidate: unknown, index: number) => {\n const cand = candidate as Record | null;\n if (!cand?.content) return candidate;\n\n const content = cand.content as Record;\n if (!Array.isArray(content.parts)) return candidate;\n\n const newParts = content.parts.map((part: unknown) => {\n const p = part as Record;\n\n if (p.thought === true || p.type === \"thinking\") {\n const fullText = (p.text || p.thinking || \"\") as string;\n\n // If this thought chain was already rendered, block it directly from the stream to prevent long overlapping rendering in tool call branches\n if (displayedThinkingHashes) {\n const hash = hashString(fullText);\n if (displayedThinkingHashes.has(hash)) {\n sentBuffer.set(index, fullText);\n return null;\n }\n displayedThinkingHashes.add(hash);\n }\n\n // Calculate incremental output (Delta)\n const sentText = sentBuffer.get(index) ?? \"\";\n\n if (fullText.startsWith(sentText)) {\n const delta = fullText.slice(sentText.length);\n sentBuffer.set(index, fullText);\n\n if (delta) {\n return { ...p, text: delta, thinking: delta };\n }\n return null;\n }\n\n sentBuffer.set(index, fullText);\n return part;\n }\n return part;\n });\n\n const filteredParts = newParts.filter((p) => p !== null);\n\n return {\n ...cand,\n content: { ...content, parts: filteredParts },\n };\n });\n\n return { ...resp, candidates: newCandidates };\n }\n\n // Branch 2: Handle Claude structure type (content blocks) because Agy backend forwarding includes Claude\n if (Array.isArray(resp.content)) {\n let thinkingIndex = 0;\n const newContent = resp.content.map((block: unknown) => {\n const b = block as Record | null;\n if (b?.type === \"thinking\") {\n const fullText = (b.thinking || b.text || \"\") as string;\n\n if (displayedThinkingHashes) {\n const hash = hashString(fullText);\n if (displayedThinkingHashes.has(hash)) {\n sentBuffer.set(thinkingIndex, fullText);\n thinkingIndex++;\n return null;\n }\n displayedThinkingHashes.add(hash);\n }\n\n const sentText = sentBuffer.get(thinkingIndex) ?? \"\";\n\n if (fullText.startsWith(sentText)) {\n const delta = fullText.slice(sentText.length);\n sentBuffer.set(thinkingIndex, fullText);\n thinkingIndex++;\n\n if (delta) {\n return { ...b, thinking: delta, text: delta };\n }\n return null;\n }\n\n sentBuffer.set(thinkingIndex, fullText);\n thinkingIndex++;\n return block;\n }\n return block;\n });\n\n const filteredContent = newContent.filter((b) => b !== null);\n if (filteredContent.length === 0) {\n return { ...resp, content: [] };\n }\n return { ...resp, content: filteredContent };\n }\n\n return response;\n}\n\n/**\n * Caches thought chain content and its validation signature from the returned message body for signature alignment in the next interaction round\n * Also supports Gemini signature mechanism (candidates[].thoughtSignature) and Claude signature mechanism (content[].signature)\n */\nexport function cacheThinkingSignaturesFromResponse(\n response: unknown,\n signatureSessionKey: string,\n signatureStore: SignatureStore,\n thoughtBuffer: ThoughtBuffer,\n onCacheSignature?: (sessionKey: string, text: string, signature: string) => void,\n): void {\n if (!response || typeof response !== \"object\") return;\n\n const resp = response as Record;\n\n // Branch 1: Collect and cache Gemini type signatures\n if (Array.isArray(resp.candidates)) {\n resp.candidates.forEach((candidate: unknown, index: number) => {\n const cand = candidate as Record | null;\n if (!cand?.content) return;\n const content = cand.content as Record;\n if (!Array.isArray(content.parts)) return;\n\n content.parts.forEach((part: unknown) => {\n const p = part as Record;\n if (p.thought === true || p.type === \"thinking\") {\n const text = (p.text || p.thinking || \"\") as string;\n if (text) {\n const current = thoughtBuffer.get(index) ?? \"\";\n thoughtBuffer.set(index, current + text);\n }\n }\n\n if (p.thoughtSignature) {\n const fullText = thoughtBuffer.get(index) ?? \"\";\n if (fullText) {\n const signature = p.thoughtSignature as string;\n onCacheSignature?.(signatureSessionKey, fullText, signature);\n signatureStore.set(signatureSessionKey, { text: fullText, signature });\n }\n }\n });\n });\n }\n\n // Branch 2: Collect and cache Claude type signatures\n if (Array.isArray(resp.content)) {\n const CLAUDE_BUFFER_KEY = 0;\n resp.content.forEach((block: unknown) => {\n const b = block as Record | null;\n if (b?.type === \"thinking\") {\n const text = (b.thinking || b.text || \"\") as string;\n if (text) {\n const current = thoughtBuffer.get(CLAUDE_BUFFER_KEY) ?? \"\";\n thoughtBuffer.set(CLAUDE_BUFFER_KEY, current + text);\n }\n }\n if (b?.signature) {\n const fullText = thoughtBuffer.get(CLAUDE_BUFFER_KEY) ?? \"\";\n if (fullText) {\n const signature = b.signature as string;\n onCacheSignature?.(signatureSessionKey, fullText, signature);\n signatureStore.set(signatureSessionKey, { text: fullText, signature });\n }\n }\n });\n }\n}\n\n/**\n * Transforms a single complete SSE event, triggering thought chain caching and incremental deduplication here\n */\nexport function transformSseEvent(\n eventText: string,\n signatureStore: SignatureStore,\n thoughtBuffer: ThoughtBuffer,\n sentThinkingBuffer: ThoughtBuffer,\n callbacks: StreamingCallbacks,\n options: StreamingOptions,\n debugState: { injected: boolean },\n): string {\n // Extract all data from data: lines\n const dataLines: string[] = [];\n const lines = eventText.split(/\\r?\\n/);\n let isDataEvent = false;\n\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n isDataEvent = true;\n dataLines.push(line.slice(5).trim());\n }\n }\n\n if (!isDataEvent) {\n return eventText;\n }\n\n const jsonString = dataLines.join(\"\\n\").trim();\n if (!jsonString) {\n return eventText;\n }\n\n try {\n const parsed = JSON.parse(jsonString) as Record | null;\n if (parsed && typeof parsed === \"object\" && parsed.response !== undefined) {\n // Extract and write to cache\n if (options.cacheSignatures && options.signatureSessionKey) {\n cacheThinkingSignaturesFromResponse(\n parsed.response,\n options.signatureSessionKey,\n signatureStore,\n thoughtBuffer,\n callbacks.onCacheSignature,\n );\n }\n\n // Calculate deduplication\n let response: unknown = deduplicateThinkingText(\n parsed.response,\n sentThinkingBuffer,\n options.displayedThinkingHashes\n );\n\n // Debug text injection\n if (options.debugText && callbacks.onInjectDebug && !debugState.injected) {\n response = callbacks.onInjectDebug(response, options.debugText);\n debugState.injected = true;\n }\n\n const transformed = callbacks.transformThinkingParts\n ? callbacks.transformThinkingParts(response)\n : response;\n return `data: ${JSON.stringify(transformed)}`;\n }\n } catch (_) {}\n return eventText;\n}\n\n/**\n * Creates a TransformStream processor to split, deduplicate, and recombine the output stream\n */\nexport function createStreamingTransformer(\n signatureStore: SignatureStore,\n callbacks: StreamingCallbacks,\n options: StreamingOptions = {},\n): TransformStream {\n const decoder = new TextDecoder();\n const encoder = new TextEncoder();\n let buffer = \"\";\n const thoughtBuffer = createThoughtBuffer();\n const sentThinkingBuffer = createThoughtBuffer();\n const debugState = { injected: false };\n let hasSeenUsageMetadata = false;\n let streamHasThinking = false;\n let streamHasToolCalls = false;\n\n const displayedThinkingHashes = options.displayedThinkingHashes ?? new Set();\n const mergedOptions = { ...options, displayedThinkingHashes };\n\n return new TransformStream({\n transform(chunk, controller) {\n buffer += decoder.decode(chunk, { stream: true });\n\n const events = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = events.pop() || \"\";\n\n for (const event of events) {\n if (!event.trim()) continue; // Skip empty events if any\n if (event.includes(\"usageMetadata\")) {\n hasSeenUsageMetadata = true;\n }\n if (!streamHasThinking) {\n streamHasThinking = event.includes('\"thought\":true') || event.includes('\"type\":\"thinking\"');\n }\n if (!streamHasToolCalls) {\n streamHasToolCalls = event.includes('\"functionCall\"');\n }\n\n const transformedEvent = transformSseEvent(\n event,\n signatureStore,\n thoughtBuffer,\n sentThinkingBuffer,\n callbacks,\n mergedOptions,\n debugState,\n );\n controller.enqueue(encoder.encode(transformedEvent + \"\\n\\n\"));\n }\n },\n flush(controller) {\n buffer += decoder.decode();\n\n if (buffer.trim()) {\n if (buffer.includes(\"usageMetadata\")) {\n hasSeenUsageMetadata = true;\n }\n if (!streamHasThinking) {\n streamHasThinking = buffer.includes('\"thought\":true') || buffer.includes('\"type\":\"thinking\"');\n }\n if (!streamHasToolCalls) {\n streamHasToolCalls = buffer.includes('\"functionCall\"');\n }\n const transformedEvent = transformSseEvent(\n buffer,\n signatureStore,\n thoughtBuffer,\n sentThinkingBuffer,\n callbacks,\n mergedOptions,\n debugState,\n );\n controller.enqueue(encoder.encode(transformedEvent + \"\\n\\n\"));\n }\n\n // Fallback strategy: If no token usage metadata was generated at the end, forcibly inject a 0-count fallback to ensure VS Code statistics compatibility\n if (!hasSeenUsageMetadata) {\n const syntheticUsage = {\n candidates: [\n {\n finishReason: \"STOP\",\n },\n ],\n usageMetadata: {\n promptTokenCount: 0,\n candidatesTokenCount: 0,\n totalTokenCount: 0,\n },\n };\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(syntheticUsage)}\\n\\n`));\n }\n\n if (callbacks.onTurnStateUpdate && options.signatureSessionKey) {\n callbacks.onTurnStateUpdate(options.signatureSessionKey, {\n turnHasThinking: streamHasThinking,\n lastModelHasToolCalls: streamHasToolCalls,\n });\n }\n },\n });\n}\n","export * as core from \"../core/index.js\";\nexport * from \"./schemas.js\";\nexport * from \"./checks.js\";\nexport * from \"./errors.js\";\nexport * from \"./parse.js\";\nexport * from \"./compat.js\";\n// zod-specified\nimport { config } from \"../core/index.js\";\nimport en from \"../locales/en.js\";\nconfig(en());\nexport { globalRegistry, registry, config, $output, $input, $brand, clone, regexes, treeifyError, prettifyError, formatError, flattenError, toJSONSchema, TimePrecision, util, NEVER, } from \"../core/index.js\";\nexport * as locales from \"../locales/index.js\";\n// iso\n// must be exported from top-level\n// https://github.com/colinhacks/zod/issues/4491\nexport { ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration } from \"./iso.js\";\nexport * as iso from \"./iso.js\";\nexport * as coerce from \"./coerce.js\";\n","export * from \"./core.js\";\nexport * from \"./parse.js\";\nexport * from \"./errors.js\";\nexport * from \"./schemas.js\";\nexport * from \"./checks.js\";\nexport * from \"./versions.js\";\nexport * as util from \"./util.js\";\nexport * as regexes from \"./regexes.js\";\nexport * as locales from \"../locales/index.js\";\nexport * from \"./registries.js\";\nexport * from \"./doc.js\";\nexport * from \"./api.js\";\nexport * from \"./to-json-schema.js\";\nexport * as JSONSchema from \"./json-schema.js\";\n","/** A special constant with type `never` */\nexport const NEVER = Object.freeze({\n status: \"aborted\",\n});\nexport /*@__NO_SIDE_EFFECTS__*/ function $constructor(name, initializer, params) {\n function init(inst, def) {\n var _a;\n Object.defineProperty(inst, \"_zod\", {\n value: inst._zod ?? {},\n enumerable: false,\n });\n (_a = inst._zod).traits ?? (_a.traits = new Set());\n inst._zod.traits.add(name);\n initializer(inst, def);\n // support prototype modifications\n for (const k in _.prototype) {\n if (!(k in inst))\n Object.defineProperty(inst, k, { value: _.prototype[k].bind(inst) });\n }\n inst._zod.constr = _;\n inst._zod.def = def;\n }\n // doesn't work if Parent has a constructor with arguments\n const Parent = params?.Parent ?? Object;\n class Definition extends Parent {\n }\n Object.defineProperty(Definition, \"name\", { value: name });\n function _(def) {\n var _a;\n const inst = params?.Parent ? new Definition() : this;\n init(inst, def);\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n for (const fn of inst._zod.deferred) {\n fn();\n }\n return inst;\n }\n Object.defineProperty(_, \"init\", { value: init });\n Object.defineProperty(_, Symbol.hasInstance, {\n value: (inst) => {\n if (params?.Parent && inst instanceof params.Parent)\n return true;\n return inst?._zod?.traits?.has(name);\n },\n });\n Object.defineProperty(_, \"name\", { value: name });\n return _;\n}\n////////////////////////////// UTILITIES ///////////////////////////////////////\nexport const $brand = Symbol(\"zod_brand\");\nexport class $ZodAsyncError extends Error {\n constructor() {\n super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);\n }\n}\nexport class $ZodEncodeError extends Error {\n constructor(name) {\n super(`Encountered unidirectional transform during encode: ${name}`);\n this.name = \"ZodEncodeError\";\n }\n}\nexport const globalConfig = {};\nexport function config(newConfig) {\n if (newConfig)\n Object.assign(globalConfig, newConfig);\n return globalConfig;\n}\n","// functions\nexport function assertEqual(val) {\n return val;\n}\nexport function assertNotEqual(val) {\n return val;\n}\nexport function assertIs(_arg) { }\nexport function assertNever(_x) {\n throw new Error();\n}\nexport function assert(_) { }\nexport function getEnumValues(entries) {\n const numericValues = Object.values(entries).filter((v) => typeof v === \"number\");\n const values = Object.entries(entries)\n .filter(([k, _]) => numericValues.indexOf(+k) === -1)\n .map(([_, v]) => v);\n return values;\n}\nexport function joinValues(array, separator = \"|\") {\n return array.map((val) => stringifyPrimitive(val)).join(separator);\n}\nexport function jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\")\n return value.toString();\n return value;\n}\nexport function cached(getter) {\n const set = false;\n return {\n get value() {\n if (!set) {\n const value = getter();\n Object.defineProperty(this, \"value\", { value });\n return value;\n }\n throw new Error(\"cached value already set\");\n },\n };\n}\nexport function nullish(input) {\n return input === null || input === undefined;\n}\nexport function cleanRegex(source) {\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n return source.slice(start, end);\n}\nexport function floatSafeRemainder(val, step) {\n const valDecCount = (val.toString().split(\".\")[1] || \"\").length;\n const stepString = step.toString();\n let stepDecCount = (stepString.split(\".\")[1] || \"\").length;\n if (stepDecCount === 0 && /\\d?e-\\d?/.test(stepString)) {\n const match = stepString.match(/\\d?e-(\\d?)/);\n if (match?.[1]) {\n stepDecCount = Number.parseInt(match[1]);\n }\n }\n const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount;\n const valInt = Number.parseInt(val.toFixed(decCount).replace(\".\", \"\"));\n const stepInt = Number.parseInt(step.toFixed(decCount).replace(\".\", \"\"));\n return (valInt % stepInt) / 10 ** decCount;\n}\nconst EVALUATING = Symbol(\"evaluating\");\nexport function defineLazy(object, key, getter) {\n let value = undefined;\n Object.defineProperty(object, key, {\n get() {\n if (value === EVALUATING) {\n // Circular reference detected, return undefined to break the cycle\n return undefined;\n }\n if (value === undefined) {\n value = EVALUATING;\n value = getter();\n }\n return value;\n },\n set(v) {\n Object.defineProperty(object, key, {\n value: v,\n // configurable: true,\n });\n // object[key] = v;\n },\n configurable: true,\n });\n}\nexport function objectClone(obj) {\n return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));\n}\nexport function assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true,\n });\n}\nexport function mergeDefs(...defs) {\n const mergedDescriptors = {};\n for (const def of defs) {\n const descriptors = Object.getOwnPropertyDescriptors(def);\n Object.assign(mergedDescriptors, descriptors);\n }\n return Object.defineProperties({}, mergedDescriptors);\n}\nexport function cloneDef(schema) {\n return mergeDefs(schema._zod.def);\n}\nexport function getElementAtPath(obj, path) {\n if (!path)\n return obj;\n return path.reduce((acc, key) => acc?.[key], obj);\n}\nexport function promiseAllObject(promisesObj) {\n const keys = Object.keys(promisesObj);\n const promises = keys.map((key) => promisesObj[key]);\n return Promise.all(promises).then((results) => {\n const resolvedObj = {};\n for (let i = 0; i < keys.length; i++) {\n resolvedObj[keys[i]] = results[i];\n }\n return resolvedObj;\n });\n}\nexport function randomString(length = 10) {\n const chars = \"abcdefghijklmnopqrstuvwxyz\";\n let str = \"\";\n for (let i = 0; i < length; i++) {\n str += chars[Math.floor(Math.random() * chars.length)];\n }\n return str;\n}\nexport function esc(str) {\n return JSON.stringify(str);\n}\nexport const captureStackTrace = (\"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => { });\nexport function isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nexport const allowsEval = cached(() => {\n // @ts-ignore\n if (typeof navigator !== \"undefined\" && navigator?.userAgent?.includes(\"Cloudflare\")) {\n return false;\n }\n try {\n const F = Function;\n new F(\"\");\n return true;\n }\n catch (_) {\n return false;\n }\n});\nexport function isPlainObject(o) {\n if (isObject(o) === false)\n return false;\n // modified constructor\n const ctor = o.constructor;\n if (ctor === undefined)\n return true;\n // modified prototype\n const prot = ctor.prototype;\n if (isObject(prot) === false)\n return false;\n // ctor doesn't have static `isPrototypeOf`\n if (Object.prototype.hasOwnProperty.call(prot, \"isPrototypeOf\") === false) {\n return false;\n }\n return true;\n}\nexport function shallowClone(o) {\n if (isPlainObject(o))\n return { ...o };\n if (Array.isArray(o))\n return [...o];\n return o;\n}\nexport function numKeys(data) {\n let keyCount = 0;\n for (const key in data) {\n if (Object.prototype.hasOwnProperty.call(data, key)) {\n keyCount++;\n }\n }\n return keyCount;\n}\nexport const getParsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"undefined\":\n return \"undefined\";\n case \"string\":\n return \"string\";\n case \"number\":\n return Number.isNaN(data) ? \"nan\" : \"number\";\n case \"boolean\":\n return \"boolean\";\n case \"function\":\n return \"function\";\n case \"bigint\":\n return \"bigint\";\n case \"symbol\":\n return \"symbol\";\n case \"object\":\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (data.then && typeof data.then === \"function\" && data.catch && typeof data.catch === \"function\") {\n return \"promise\";\n }\n if (typeof Map !== \"undefined\" && data instanceof Map) {\n return \"map\";\n }\n if (typeof Set !== \"undefined\" && data instanceof Set) {\n return \"set\";\n }\n if (typeof Date !== \"undefined\" && data instanceof Date) {\n return \"date\";\n }\n // @ts-ignore\n if (typeof File !== \"undefined\" && data instanceof File) {\n return \"file\";\n }\n return \"object\";\n default:\n throw new Error(`Unknown data type: ${t}`);\n }\n};\nexport const propertyKeyTypes = new Set([\"string\", \"number\", \"symbol\"]);\nexport const primitiveTypes = new Set([\"string\", \"number\", \"bigint\", \"boolean\", \"symbol\", \"undefined\"]);\nexport function escapeRegex(str) {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n// zod-specific utils\nexport function clone(inst, def, params) {\n const cl = new inst._zod.constr(def ?? inst._zod.def);\n if (!def || params?.parent)\n cl._zod.parent = inst;\n return cl;\n}\nexport function normalizeParams(_params) {\n const params = _params;\n if (!params)\n return {};\n if (typeof params === \"string\")\n return { error: () => params };\n if (params?.message !== undefined) {\n if (params?.error !== undefined)\n throw new Error(\"Cannot specify both `message` and `error` params\");\n params.error = params.message;\n }\n delete params.message;\n if (typeof params.error === \"string\")\n return { ...params, error: () => params.error };\n return params;\n}\nexport function createTransparentProxy(getter) {\n let target;\n return new Proxy({}, {\n get(_, prop, receiver) {\n target ?? (target = getter());\n return Reflect.get(target, prop, receiver);\n },\n set(_, prop, value, receiver) {\n target ?? (target = getter());\n return Reflect.set(target, prop, value, receiver);\n },\n has(_, prop) {\n target ?? (target = getter());\n return Reflect.has(target, prop);\n },\n deleteProperty(_, prop) {\n target ?? (target = getter());\n return Reflect.deleteProperty(target, prop);\n },\n ownKeys(_) {\n target ?? (target = getter());\n return Reflect.ownKeys(target);\n },\n getOwnPropertyDescriptor(_, prop) {\n target ?? (target = getter());\n return Reflect.getOwnPropertyDescriptor(target, prop);\n },\n defineProperty(_, prop, descriptor) {\n target ?? (target = getter());\n return Reflect.defineProperty(target, prop, descriptor);\n },\n });\n}\nexport function stringifyPrimitive(value) {\n if (typeof value === \"bigint\")\n return value.toString() + \"n\";\n if (typeof value === \"string\")\n return `\"${value}\"`;\n return `${value}`;\n}\nexport function optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin === \"optional\" && shape[k]._zod.optout === \"optional\";\n });\n}\nexport const NUMBER_FORMAT_RANGES = {\n safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],\n int32: [-2147483648, 2147483647],\n uint32: [0, 4294967295],\n float32: [-3.4028234663852886e38, 3.4028234663852886e38],\n float64: [-Number.MAX_VALUE, Number.MAX_VALUE],\n};\nexport const BIGINT_FORMAT_RANGES = {\n int64: [/* @__PURE__*/ BigInt(\"-9223372036854775808\"), /* @__PURE__*/ BigInt(\"9223372036854775807\")],\n uint64: [/* @__PURE__*/ BigInt(0), /* @__PURE__*/ BigInt(\"18446744073709551615\")],\n};\nexport function pick(schema, mask) {\n const currDef = schema._zod.def;\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const newShape = {};\n for (const key in mask) {\n if (!(key in currDef.shape)) {\n throw new Error(`Unrecognized key: \"${key}\"`);\n }\n if (!mask[key])\n continue;\n newShape[key] = currDef.shape[key];\n }\n assignProp(this, \"shape\", newShape); // self-caching\n return newShape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\nexport function omit(schema, mask) {\n const currDef = schema._zod.def;\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const newShape = { ...schema._zod.def.shape };\n for (const key in mask) {\n if (!(key in currDef.shape)) {\n throw new Error(`Unrecognized key: \"${key}\"`);\n }\n if (!mask[key])\n continue;\n delete newShape[key];\n }\n assignProp(this, \"shape\", newShape); // self-caching\n return newShape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\nexport function extend(schema, shape) {\n if (!isPlainObject(shape)) {\n throw new Error(\"Invalid input to extend: expected a plain object\");\n }\n const checks = schema._zod.def.checks;\n const hasChecks = checks && checks.length > 0;\n if (hasChecks) {\n throw new Error(\"Object schemas containing refinements cannot be extended. Use `.safeExtend()` instead.\");\n }\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const _shape = { ...schema._zod.def.shape, ...shape };\n assignProp(this, \"shape\", _shape); // self-caching\n return _shape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\nexport function safeExtend(schema, shape) {\n if (!isPlainObject(shape)) {\n throw new Error(\"Invalid input to safeExtend: expected a plain object\");\n }\n const def = {\n ...schema._zod.def,\n get shape() {\n const _shape = { ...schema._zod.def.shape, ...shape };\n assignProp(this, \"shape\", _shape); // self-caching\n return _shape;\n },\n checks: schema._zod.def.checks,\n };\n return clone(schema, def);\n}\nexport function merge(a, b) {\n const def = mergeDefs(a._zod.def, {\n get shape() {\n const _shape = { ...a._zod.def.shape, ...b._zod.def.shape };\n assignProp(this, \"shape\", _shape); // self-caching\n return _shape;\n },\n get catchall() {\n return b._zod.def.catchall;\n },\n checks: [], // delete existing checks\n });\n return clone(a, def);\n}\nexport function partial(Class, schema, mask) {\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const oldShape = schema._zod.def.shape;\n const shape = { ...oldShape };\n if (mask) {\n for (const key in mask) {\n if (!(key in oldShape)) {\n throw new Error(`Unrecognized key: \"${key}\"`);\n }\n if (!mask[key])\n continue;\n // if (oldShape[key]!._zod.optin === \"optional\") continue;\n shape[key] = Class\n ? new Class({\n type: \"optional\",\n innerType: oldShape[key],\n })\n : oldShape[key];\n }\n }\n else {\n for (const key in oldShape) {\n // if (oldShape[key]!._zod.optin === \"optional\") continue;\n shape[key] = Class\n ? new Class({\n type: \"optional\",\n innerType: oldShape[key],\n })\n : oldShape[key];\n }\n }\n assignProp(this, \"shape\", shape); // self-caching\n return shape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\nexport function required(Class, schema, mask) {\n const def = mergeDefs(schema._zod.def, {\n get shape() {\n const oldShape = schema._zod.def.shape;\n const shape = { ...oldShape };\n if (mask) {\n for (const key in mask) {\n if (!(key in shape)) {\n throw new Error(`Unrecognized key: \"${key}\"`);\n }\n if (!mask[key])\n continue;\n // overwrite with non-optional\n shape[key] = new Class({\n type: \"nonoptional\",\n innerType: oldShape[key],\n });\n }\n }\n else {\n for (const key in oldShape) {\n // overwrite with non-optional\n shape[key] = new Class({\n type: \"nonoptional\",\n innerType: oldShape[key],\n });\n }\n }\n assignProp(this, \"shape\", shape); // self-caching\n return shape;\n },\n checks: [],\n });\n return clone(schema, def);\n}\n// invalid_type | too_big | too_small | invalid_format | not_multiple_of | unrecognized_keys | invalid_union | invalid_key | invalid_element | invalid_value | custom\nexport function aborted(x, startIndex = 0) {\n if (x.aborted === true)\n return true;\n for (let i = startIndex; i < x.issues.length; i++) {\n if (x.issues[i]?.continue !== true) {\n return true;\n }\n }\n return false;\n}\nexport function prefixIssues(path, issues) {\n return issues.map((iss) => {\n var _a;\n (_a = iss).path ?? (_a.path = []);\n iss.path.unshift(path);\n return iss;\n });\n}\nexport function unwrapMessage(message) {\n return typeof message === \"string\" ? message : message?.message;\n}\nexport function finalizeIssue(iss, ctx, config) {\n const full = { ...iss, path: iss.path ?? [] };\n // for backwards compatibility\n if (!iss.message) {\n const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ??\n unwrapMessage(ctx?.error?.(iss)) ??\n unwrapMessage(config.customError?.(iss)) ??\n unwrapMessage(config.localeError?.(iss)) ??\n \"Invalid input\";\n full.message = message;\n }\n // delete (full as any).def;\n delete full.inst;\n delete full.continue;\n if (!ctx?.reportInput) {\n delete full.input;\n }\n return full;\n}\nexport function getSizableOrigin(input) {\n if (input instanceof Set)\n return \"set\";\n if (input instanceof Map)\n return \"map\";\n // @ts-ignore\n if (input instanceof File)\n return \"file\";\n return \"unknown\";\n}\nexport function getLengthableOrigin(input) {\n if (Array.isArray(input))\n return \"array\";\n if (typeof input === \"string\")\n return \"string\";\n return \"unknown\";\n}\nexport function issue(...args) {\n const [iss, input, inst] = args;\n if (typeof iss === \"string\") {\n return {\n message: iss,\n code: \"custom\",\n input,\n inst,\n };\n }\n return { ...iss };\n}\nexport function cleanEnum(obj) {\n return Object.entries(obj)\n .filter(([k, _]) => {\n // return true if NaN, meaning it's not a number, thus a string key\n return Number.isNaN(Number.parseInt(k, 10));\n })\n .map((el) => el[1]);\n}\n// Codec utility functions\nexport function base64ToUint8Array(base64) {\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes;\n}\nexport function uint8ArrayToBase64(bytes) {\n let binaryString = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binaryString += String.fromCharCode(bytes[i]);\n }\n return btoa(binaryString);\n}\nexport function base64urlToUint8Array(base64url) {\n const base64 = base64url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padding = \"=\".repeat((4 - (base64.length % 4)) % 4);\n return base64ToUint8Array(base64 + padding);\n}\nexport function uint8ArrayToBase64url(bytes) {\n return uint8ArrayToBase64(bytes).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n}\nexport function hexToUint8Array(hex) {\n const cleanHex = hex.replace(/^0x/, \"\");\n if (cleanHex.length % 2 !== 0) {\n throw new Error(\"Invalid hex string length\");\n }\n const bytes = new Uint8Array(cleanHex.length / 2);\n for (let i = 0; i < cleanHex.length; i += 2) {\n bytes[i / 2] = Number.parseInt(cleanHex.slice(i, i + 2), 16);\n }\n return bytes;\n}\nexport function uint8ArrayToHex(bytes) {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n// instanceof\nexport class Class {\n constructor(..._args) { }\n}\n","import { $constructor } from \"./core.js\";\nimport * as util from \"./util.js\";\nconst initializer = (inst, def) => {\n inst.name = \"$ZodError\";\n Object.defineProperty(inst, \"_zod\", {\n value: inst._zod,\n enumerable: false,\n });\n Object.defineProperty(inst, \"issues\", {\n value: def,\n enumerable: false,\n });\n inst.message = JSON.stringify(def, util.jsonStringifyReplacer, 2);\n Object.defineProperty(inst, \"toString\", {\n value: () => inst.message,\n enumerable: false,\n });\n};\nexport const $ZodError = $constructor(\"$ZodError\", initializer);\nexport const $ZodRealError = $constructor(\"$ZodError\", initializer, { Parent: Error });\nexport function flattenError(error, mapper = (issue) => issue.message) {\n const fieldErrors = {};\n const formErrors = [];\n for (const sub of error.issues) {\n if (sub.path.length > 0) {\n fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];\n fieldErrors[sub.path[0]].push(mapper(sub));\n }\n else {\n formErrors.push(mapper(sub));\n }\n }\n return { formErrors, fieldErrors };\n}\nexport function formatError(error, _mapper) {\n const mapper = _mapper ||\n function (issue) {\n return issue.message;\n };\n const fieldErrors = { _errors: [] };\n const processError = (error) => {\n for (const issue of error.issues) {\n if (issue.code === \"invalid_union\" && issue.errors.length) {\n issue.errors.map((issues) => processError({ issues }));\n }\n else if (issue.code === \"invalid_key\") {\n processError({ issues: issue.issues });\n }\n else if (issue.code === \"invalid_element\") {\n processError({ issues: issue.issues });\n }\n else if (issue.path.length === 0) {\n fieldErrors._errors.push(mapper(issue));\n }\n else {\n let curr = fieldErrors;\n let i = 0;\n while (i < issue.path.length) {\n const el = issue.path[i];\n const terminal = i === issue.path.length - 1;\n if (!terminal) {\n curr[el] = curr[el] || { _errors: [] };\n }\n else {\n curr[el] = curr[el] || { _errors: [] };\n curr[el]._errors.push(mapper(issue));\n }\n curr = curr[el];\n i++;\n }\n }\n }\n };\n processError(error);\n return fieldErrors;\n}\nexport function treeifyError(error, _mapper) {\n const mapper = _mapper ||\n function (issue) {\n return issue.message;\n };\n const result = { errors: [] };\n const processError = (error, path = []) => {\n var _a, _b;\n for (const issue of error.issues) {\n if (issue.code === \"invalid_union\" && issue.errors.length) {\n // regular union error\n issue.errors.map((issues) => processError({ issues }, issue.path));\n }\n else if (issue.code === \"invalid_key\") {\n processError({ issues: issue.issues }, issue.path);\n }\n else if (issue.code === \"invalid_element\") {\n processError({ issues: issue.issues }, issue.path);\n }\n else {\n const fullpath = [...path, ...issue.path];\n if (fullpath.length === 0) {\n result.errors.push(mapper(issue));\n continue;\n }\n let curr = result;\n let i = 0;\n while (i < fullpath.length) {\n const el = fullpath[i];\n const terminal = i === fullpath.length - 1;\n if (typeof el === \"string\") {\n curr.properties ?? (curr.properties = {});\n (_a = curr.properties)[el] ?? (_a[el] = { errors: [] });\n curr = curr.properties[el];\n }\n else {\n curr.items ?? (curr.items = []);\n (_b = curr.items)[el] ?? (_b[el] = { errors: [] });\n curr = curr.items[el];\n }\n if (terminal) {\n curr.errors.push(mapper(issue));\n }\n i++;\n }\n }\n }\n };\n processError(error);\n return result;\n}\n/** Format a ZodError as a human-readable string in the following form.\n *\n * From\n *\n * ```ts\n * ZodError {\n * issues: [\n * {\n * expected: 'string',\n * code: 'invalid_type',\n * path: [ 'username' ],\n * message: 'Invalid input: expected string'\n * },\n * {\n * expected: 'number',\n * code: 'invalid_type',\n * path: [ 'favoriteNumbers', 1 ],\n * message: 'Invalid input: expected number'\n * }\n * ];\n * }\n * ```\n *\n * to\n *\n * ```\n * username\n * ✖ Expected number, received string at \"username\n * favoriteNumbers[0]\n * ✖ Invalid input: expected number\n * ```\n */\nexport function toDotPath(_path) {\n const segs = [];\n const path = _path.map((seg) => (typeof seg === \"object\" ? seg.key : seg));\n for (const seg of path) {\n if (typeof seg === \"number\")\n segs.push(`[${seg}]`);\n else if (typeof seg === \"symbol\")\n segs.push(`[${JSON.stringify(String(seg))}]`);\n else if (/[^\\w$]/.test(seg))\n segs.push(`[${JSON.stringify(seg)}]`);\n else {\n if (segs.length)\n segs.push(\".\");\n segs.push(seg);\n }\n }\n return segs.join(\"\");\n}\nexport function prettifyError(error) {\n const lines = [];\n // sort by path length\n const issues = [...error.issues].sort((a, b) => (a.path ?? []).length - (b.path ?? []).length);\n // Process each issue\n for (const issue of issues) {\n lines.push(`✖ ${issue.message}`);\n if (issue.path?.length)\n lines.push(` → at ${toDotPath(issue.path)}`);\n }\n // Convert Map to formatted string\n return lines.join(\"\\n\");\n}\n","import * as core from \"./core.js\";\nimport * as errors from \"./errors.js\";\nimport * as util from \"./util.js\";\nexport const _parse = (_Err) => (schema, value, _ctx, _params) => {\n const ctx = _ctx ? Object.assign(_ctx, { async: false }) : { async: false };\n const result = schema._zod.run({ value, issues: [] }, ctx);\n if (result instanceof Promise) {\n throw new core.$ZodAsyncError();\n }\n if (result.issues.length) {\n const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));\n util.captureStackTrace(e, _params?.callee);\n throw e;\n }\n return result.value;\n};\nexport const parse = /* @__PURE__*/ _parse(errors.$ZodRealError);\nexport const _parseAsync = (_Err) => async (schema, value, _ctx, params) => {\n const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };\n let result = schema._zod.run({ value, issues: [] }, ctx);\n if (result instanceof Promise)\n result = await result;\n if (result.issues.length) {\n const e = new (params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));\n util.captureStackTrace(e, params?.callee);\n throw e;\n }\n return result.value;\n};\nexport const parseAsync = /* @__PURE__*/ _parseAsync(errors.$ZodRealError);\nexport const _safeParse = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? { ..._ctx, async: false } : { async: false };\n const result = schema._zod.run({ value, issues: [] }, ctx);\n if (result instanceof Promise) {\n throw new core.$ZodAsyncError();\n }\n return result.issues.length\n ? {\n success: false,\n error: new (_Err ?? errors.$ZodError)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))),\n }\n : { success: true, data: result.value };\n};\nexport const safeParse = /* @__PURE__*/ _safeParse(errors.$ZodRealError);\nexport const _safeParseAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };\n let result = schema._zod.run({ value, issues: [] }, ctx);\n if (result instanceof Promise)\n result = await result;\n return result.issues.length\n ? {\n success: false,\n error: new _Err(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))),\n }\n : { success: true, data: result.value };\n};\nexport const safeParseAsync = /* @__PURE__*/ _safeParseAsync(errors.$ZodRealError);\nexport const _encode = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { direction: \"backward\" }) : { direction: \"backward\" };\n return _parse(_Err)(schema, value, ctx);\n};\nexport const encode = /* @__PURE__*/ _encode(errors.$ZodRealError);\nexport const _decode = (_Err) => (schema, value, _ctx) => {\n return _parse(_Err)(schema, value, _ctx);\n};\nexport const decode = /* @__PURE__*/ _decode(errors.$ZodRealError);\nexport const _encodeAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { direction: \"backward\" }) : { direction: \"backward\" };\n return _parseAsync(_Err)(schema, value, ctx);\n};\nexport const encodeAsync = /* @__PURE__*/ _encodeAsync(errors.$ZodRealError);\nexport const _decodeAsync = (_Err) => async (schema, value, _ctx) => {\n return _parseAsync(_Err)(schema, value, _ctx);\n};\nexport const decodeAsync = /* @__PURE__*/ _decodeAsync(errors.$ZodRealError);\nexport const _safeEncode = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { direction: \"backward\" }) : { direction: \"backward\" };\n return _safeParse(_Err)(schema, value, ctx);\n};\nexport const safeEncode = /* @__PURE__*/ _safeEncode(errors.$ZodRealError);\nexport const _safeDecode = (_Err) => (schema, value, _ctx) => {\n return _safeParse(_Err)(schema, value, _ctx);\n};\nexport const safeDecode = /* @__PURE__*/ _safeDecode(errors.$ZodRealError);\nexport const _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? Object.assign(_ctx, { direction: \"backward\" }) : { direction: \"backward\" };\n return _safeParseAsync(_Err)(schema, value, ctx);\n};\nexport const safeEncodeAsync = /* @__PURE__*/ _safeEncodeAsync(errors.$ZodRealError);\nexport const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {\n return _safeParseAsync(_Err)(schema, value, _ctx);\n};\nexport const safeDecodeAsync = /* @__PURE__*/ _safeDecodeAsync(errors.$ZodRealError);\n","export const cuid = /^[cC][^\\s-]{8,}$/;\nexport const cuid2 = /^[0-9a-z]+$/;\nexport const ulid = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;\nexport const xid = /^[0-9a-vA-V]{20}$/;\nexport const ksuid = /^[A-Za-z0-9]{27}$/;\nexport const nanoid = /^[a-zA-Z0-9_-]{21}$/;\n/** ISO 8601-1 duration regex. Does not support the 8601-2 extensions like negative durations or fractional/negative components. */\nexport const duration = /^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$/;\n/** Implements ISO 8601-2 extensions like explicit +- prefixes, mixing weeks with other units, and fractional/negative components. */\nexport const extendedDuration = /^[-+]?P(?!$)(?:(?:[-+]?\\d+Y)|(?:[-+]?\\d+[.,]\\d+Y$))?(?:(?:[-+]?\\d+M)|(?:[-+]?\\d+[.,]\\d+M$))?(?:(?:[-+]?\\d+W)|(?:[-+]?\\d+[.,]\\d+W$))?(?:(?:[-+]?\\d+D)|(?:[-+]?\\d+[.,]\\d+D$))?(?:T(?=[\\d+-])(?:(?:[-+]?\\d+H)|(?:[-+]?\\d+[.,]\\d+H$))?(?:(?:[-+]?\\d+M)|(?:[-+]?\\d+[.,]\\d+M$))?(?:[-+]?\\d+(?:[.,]\\d+)?S)?)??$/;\n/** A regex for any UUID-like identifier: 8-4-4-4-12 hex pattern */\nexport const guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;\n/** Returns a regex for validating an RFC 9562/4122 UUID.\n *\n * @param version Optionally specify a version 1-8. If no version is specified, all versions are supported. */\nexport const uuid = (version) => {\n if (!version)\n return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;\n return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);\n};\nexport const uuid4 = /*@__PURE__*/ uuid(4);\nexport const uuid6 = /*@__PURE__*/ uuid(6);\nexport const uuid7 = /*@__PURE__*/ uuid(7);\n/** Practical email validation */\nexport const email = /^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$/;\n/** Equivalent to the HTML5 input[type=email] validation implemented by browsers. Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email */\nexport const html5Email = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;\n/** The classic emailregex.com regex for RFC 5322-compliant emails */\nexport const rfc5322Email = /^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;\n/** A loose regex that allows Unicode characters, enforces length limits, and that's about it. */\nexport const unicodeEmail = /^[^\\s@\"]{1,64}@[^\\s@]{1,255}$/u;\nexport const idnEmail = unicodeEmail;\nexport const browserEmail = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;\n// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression\nconst _emoji = `^(\\\\p{Extended_Pictographic}|\\\\p{Emoji_Component})+$`;\nexport function emoji() {\n return new RegExp(_emoji, \"u\");\n}\nexport const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;\nexport const ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;\nexport const cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\/([0-9]|[1-2][0-9]|3[0-2])$/;\nexport const cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;\n// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript\nexport const base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;\nexport const base64url = /^[A-Za-z0-9_-]*$/;\n// based on https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address\n// export const hostname: RegExp = /^([a-zA-Z0-9-]+\\.)*[a-zA-Z0-9-]+$/;\nexport const hostname = /^(?=.{1,253}\\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\\.?$/;\nexport const domain = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$/;\n// https://blog.stevenlevithan.com/archives/validate-phone-number#r4-3 (regex sans spaces)\nexport const e164 = /^\\+(?:[0-9]){6,14}[0-9]$/;\n// const dateSource = `((\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\\\d|30)|(02)-(0[1-9]|1\\\\d|2[0-8])))`;\nconst dateSource = `(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))`;\nexport const date = /*@__PURE__*/ new RegExp(`^${dateSource}$`);\nfunction timeSource(args) {\n const hhmm = `(?:[01]\\\\d|2[0-3]):[0-5]\\\\d`;\n const regex = typeof args.precision === \"number\"\n ? args.precision === -1\n ? `${hhmm}`\n : args.precision === 0\n ? `${hhmm}:[0-5]\\\\d`\n : `${hhmm}:[0-5]\\\\d\\\\.\\\\d{${args.precision}}`\n : `${hhmm}(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?`;\n return regex;\n}\nexport function time(args) {\n return new RegExp(`^${timeSource(args)}$`);\n}\n// Adapted from https://stackoverflow.com/a/3143231\nexport function datetime(args) {\n const time = timeSource({ precision: args.precision });\n const opts = [\"Z\"];\n if (args.local)\n opts.push(\"\");\n // if (args.offset) opts.push(`([+-]\\\\d{2}:\\\\d{2})`);\n if (args.offset)\n opts.push(`([+-](?:[01]\\\\d|2[0-3]):[0-5]\\\\d)`);\n const timeRegex = `${time}(?:${opts.join(\"|\")})`;\n return new RegExp(`^${dateSource}T(?:${timeRegex})$`);\n}\nexport const string = (params) => {\n const regex = params ? `[\\\\s\\\\S]{${params?.minimum ?? 0},${params?.maximum ?? \"\"}}` : `[\\\\s\\\\S]*`;\n return new RegExp(`^${regex}$`);\n};\nexport const bigint = /^-?\\d+n?$/;\nexport const integer = /^-?\\d+$/;\nexport const number = /^-?\\d+(?:\\.\\d+)?/;\nexport const boolean = /^(?:true|false)$/i;\nconst _null = /^null$/i;\nexport { _null as null };\nconst _undefined = /^undefined$/i;\nexport { _undefined as undefined };\n// regex for string with no uppercase letters\nexport const lowercase = /^[^A-Z]*$/;\n// regex for string with no lowercase letters\nexport const uppercase = /^[^a-z]*$/;\n// regex for hexadecimal strings (any length)\nexport const hex = /^[0-9a-fA-F]*$/;\n// Hash regexes for different algorithms and encodings\n// Helper function to create base64 regex with exact length and padding\nfunction fixedBase64(bodyLength, padding) {\n return new RegExp(`^[A-Za-z0-9+/]{${bodyLength}}${padding}$`);\n}\n// Helper function to create base64url regex with exact length (no padding)\nfunction fixedBase64url(length) {\n return new RegExp(`^[A-Za-z0-9_-]{${length}}$`);\n}\n// MD5 (16 bytes): base64 = 24 chars total (22 + \"==\")\nexport const md5_hex = /^[0-9a-fA-F]{32}$/;\nexport const md5_base64 = /*@__PURE__*/ fixedBase64(22, \"==\");\nexport const md5_base64url = /*@__PURE__*/ fixedBase64url(22);\n// SHA1 (20 bytes): base64 = 28 chars total (27 + \"=\")\nexport const sha1_hex = /^[0-9a-fA-F]{40}$/;\nexport const sha1_base64 = /*@__PURE__*/ fixedBase64(27, \"=\");\nexport const sha1_base64url = /*@__PURE__*/ fixedBase64url(27);\n// SHA256 (32 bytes): base64 = 44 chars total (43 + \"=\")\nexport const sha256_hex = /^[0-9a-fA-F]{64}$/;\nexport const sha256_base64 = /*@__PURE__*/ fixedBase64(43, \"=\");\nexport const sha256_base64url = /*@__PURE__*/ fixedBase64url(43);\n// SHA384 (48 bytes): base64 = 64 chars total (no padding)\nexport const sha384_hex = /^[0-9a-fA-F]{96}$/;\nexport const sha384_base64 = /*@__PURE__*/ fixedBase64(64, \"\");\nexport const sha384_base64url = /*@__PURE__*/ fixedBase64url(64);\n// SHA512 (64 bytes): base64 = 88 chars total (86 + \"==\")\nexport const sha512_hex = /^[0-9a-fA-F]{128}$/;\nexport const sha512_base64 = /*@__PURE__*/ fixedBase64(86, \"==\");\nexport const sha512_base64url = /*@__PURE__*/ fixedBase64url(86);\n","// import { $ZodType } from \"./schemas.js\";\nimport * as core from \"./core.js\";\nimport * as regexes from \"./regexes.js\";\nimport * as util from \"./util.js\";\nexport const $ZodCheck = /*@__PURE__*/ core.$constructor(\"$ZodCheck\", (inst, def) => {\n var _a;\n inst._zod ?? (inst._zod = {});\n inst._zod.def = def;\n (_a = inst._zod).onattach ?? (_a.onattach = []);\n});\nconst numericOriginMap = {\n number: \"number\",\n bigint: \"bigint\",\n object: \"date\",\n};\nexport const $ZodCheckLessThan = /*@__PURE__*/ core.$constructor(\"$ZodCheckLessThan\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const origin = numericOriginMap[typeof def.value];\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n const curr = (def.inclusive ? bag.maximum : bag.exclusiveMaximum) ?? Number.POSITIVE_INFINITY;\n if (def.value < curr) {\n if (def.inclusive)\n bag.maximum = def.value;\n else\n bag.exclusiveMaximum = def.value;\n }\n });\n inst._zod.check = (payload) => {\n if (def.inclusive ? payload.value <= def.value : payload.value < def.value) {\n return;\n }\n payload.issues.push({\n origin,\n code: \"too_big\",\n maximum: def.value,\n input: payload.value,\n inclusive: def.inclusive,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckGreaterThan = /*@__PURE__*/ core.$constructor(\"$ZodCheckGreaterThan\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const origin = numericOriginMap[typeof def.value];\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n const curr = (def.inclusive ? bag.minimum : bag.exclusiveMinimum) ?? Number.NEGATIVE_INFINITY;\n if (def.value > curr) {\n if (def.inclusive)\n bag.minimum = def.value;\n else\n bag.exclusiveMinimum = def.value;\n }\n });\n inst._zod.check = (payload) => {\n if (def.inclusive ? payload.value >= def.value : payload.value > def.value) {\n return;\n }\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.value,\n input: payload.value,\n inclusive: def.inclusive,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckMultipleOf = \n/*@__PURE__*/ core.$constructor(\"$ZodCheckMultipleOf\", (inst, def) => {\n $ZodCheck.init(inst, def);\n inst._zod.onattach.push((inst) => {\n var _a;\n (_a = inst._zod.bag).multipleOf ?? (_a.multipleOf = def.value);\n });\n inst._zod.check = (payload) => {\n if (typeof payload.value !== typeof def.value)\n throw new Error(\"Cannot mix number and bigint in multiple_of check.\");\n const isMultiple = typeof payload.value === \"bigint\"\n ? payload.value % def.value === BigInt(0)\n : util.floatSafeRemainder(payload.value, def.value) === 0;\n if (isMultiple)\n return;\n payload.issues.push({\n origin: typeof payload.value,\n code: \"not_multiple_of\",\n divisor: def.value,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckNumberFormat = /*@__PURE__*/ core.$constructor(\"$ZodCheckNumberFormat\", (inst, def) => {\n $ZodCheck.init(inst, def); // no format checks\n def.format = def.format || \"float64\";\n const isInt = def.format?.includes(\"int\");\n const origin = isInt ? \"int\" : \"number\";\n const [minimum, maximum] = util.NUMBER_FORMAT_RANGES[def.format];\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = def.format;\n bag.minimum = minimum;\n bag.maximum = maximum;\n if (isInt)\n bag.pattern = regexes.integer;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n if (isInt) {\n if (!Number.isInteger(input)) {\n // invalid_format issue\n // payload.issues.push({\n // expected: def.format,\n // format: def.format,\n // code: \"invalid_format\",\n // input,\n // inst,\n // });\n // invalid_type issue\n payload.issues.push({\n expected: origin,\n format: def.format,\n code: \"invalid_type\",\n continue: false,\n input,\n inst,\n });\n return;\n // not_multiple_of issue\n // payload.issues.push({\n // code: \"not_multiple_of\",\n // origin: \"number\",\n // input,\n // inst,\n // divisor: 1,\n // });\n }\n if (!Number.isSafeInteger(input)) {\n if (input > 0) {\n // too_big\n payload.issues.push({\n input,\n code: \"too_big\",\n maximum: Number.MAX_SAFE_INTEGER,\n note: \"Integers must be within the safe integer range.\",\n inst,\n origin,\n continue: !def.abort,\n });\n }\n else {\n // too_small\n payload.issues.push({\n input,\n code: \"too_small\",\n minimum: Number.MIN_SAFE_INTEGER,\n note: \"Integers must be within the safe integer range.\",\n inst,\n origin,\n continue: !def.abort,\n });\n }\n return;\n }\n }\n if (input < minimum) {\n payload.issues.push({\n origin: \"number\",\n input,\n code: \"too_small\",\n minimum,\n inclusive: true,\n inst,\n continue: !def.abort,\n });\n }\n if (input > maximum) {\n payload.issues.push({\n origin: \"number\",\n input,\n code: \"too_big\",\n maximum,\n inst,\n });\n }\n };\n});\nexport const $ZodCheckBigIntFormat = /*@__PURE__*/ core.$constructor(\"$ZodCheckBigIntFormat\", (inst, def) => {\n $ZodCheck.init(inst, def); // no format checks\n const [minimum, maximum] = util.BIGINT_FORMAT_RANGES[def.format];\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = def.format;\n bag.minimum = minimum;\n bag.maximum = maximum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n if (input < minimum) {\n payload.issues.push({\n origin: \"bigint\",\n input,\n code: \"too_small\",\n minimum: minimum,\n inclusive: true,\n inst,\n continue: !def.abort,\n });\n }\n if (input > maximum) {\n payload.issues.push({\n origin: \"bigint\",\n input,\n code: \"too_big\",\n maximum,\n inst,\n });\n }\n };\n});\nexport const $ZodCheckMaxSize = /*@__PURE__*/ core.$constructor(\"$ZodCheckMaxSize\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.size !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY);\n if (def.maximum < curr)\n inst._zod.bag.maximum = def.maximum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const size = input.size;\n if (size <= def.maximum)\n return;\n payload.issues.push({\n origin: util.getSizableOrigin(input),\n code: \"too_big\",\n maximum: def.maximum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckMinSize = /*@__PURE__*/ core.$constructor(\"$ZodCheckMinSize\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.size !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY);\n if (def.minimum > curr)\n inst._zod.bag.minimum = def.minimum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const size = input.size;\n if (size >= def.minimum)\n return;\n payload.issues.push({\n origin: util.getSizableOrigin(input),\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckSizeEquals = /*@__PURE__*/ core.$constructor(\"$ZodCheckSizeEquals\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.size !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.minimum = def.size;\n bag.maximum = def.size;\n bag.size = def.size;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const size = input.size;\n if (size === def.size)\n return;\n const tooBig = size > def.size;\n payload.issues.push({\n origin: util.getSizableOrigin(input),\n ...(tooBig ? { code: \"too_big\", maximum: def.size } : { code: \"too_small\", minimum: def.size }),\n inclusive: true,\n exact: true,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckMaxLength = /*@__PURE__*/ core.$constructor(\"$ZodCheckMaxLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.length !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY);\n if (def.maximum < curr)\n inst._zod.bag.maximum = def.maximum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const length = input.length;\n if (length <= def.maximum)\n return;\n const origin = util.getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_big\",\n maximum: def.maximum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckMinLength = /*@__PURE__*/ core.$constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.length !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY);\n if (def.minimum > curr)\n inst._zod.bag.minimum = def.minimum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const length = input.length;\n if (length >= def.minimum)\n return;\n const origin = util.getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckLengthEquals = /*@__PURE__*/ core.$constructor(\"$ZodCheckLengthEquals\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !util.nullish(val) && val.length !== undefined;\n });\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.minimum = def.length;\n bag.maximum = def.length;\n bag.length = def.length;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const length = input.length;\n if (length === def.length)\n return;\n const origin = util.getLengthableOrigin(input);\n const tooBig = length > def.length;\n payload.issues.push({\n origin,\n ...(tooBig ? { code: \"too_big\", maximum: def.length } : { code: \"too_small\", minimum: def.length }),\n inclusive: true,\n exact: true,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckStringFormat = /*@__PURE__*/ core.$constructor(\"$ZodCheckStringFormat\", (inst, def) => {\n var _a, _b;\n $ZodCheck.init(inst, def);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = def.format;\n if (def.pattern) {\n bag.patterns ?? (bag.patterns = new Set());\n bag.patterns.add(def.pattern);\n }\n });\n if (def.pattern)\n (_a = inst._zod).check ?? (_a.check = (payload) => {\n def.pattern.lastIndex = 0;\n if (def.pattern.test(payload.value))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: def.format,\n input: payload.value,\n ...(def.pattern ? { pattern: def.pattern.toString() } : {}),\n inst,\n continue: !def.abort,\n });\n });\n else\n (_b = inst._zod).check ?? (_b.check = () => { });\n});\nexport const $ZodCheckRegex = /*@__PURE__*/ core.$constructor(\"$ZodCheckRegex\", (inst, def) => {\n $ZodCheckStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n def.pattern.lastIndex = 0;\n if (def.pattern.test(payload.value))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"regex\",\n input: payload.value,\n pattern: def.pattern.toString(),\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckLowerCase = /*@__PURE__*/ core.$constructor(\"$ZodCheckLowerCase\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.lowercase);\n $ZodCheckStringFormat.init(inst, def);\n});\nexport const $ZodCheckUpperCase = /*@__PURE__*/ core.$constructor(\"$ZodCheckUpperCase\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.uppercase);\n $ZodCheckStringFormat.init(inst, def);\n});\nexport const $ZodCheckIncludes = /*@__PURE__*/ core.$constructor(\"$ZodCheckIncludes\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const escapedRegex = util.escapeRegex(def.includes);\n const pattern = new RegExp(typeof def.position === \"number\" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);\n def.pattern = pattern;\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.includes(def.includes, def.position))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"includes\",\n includes: def.includes,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckStartsWith = /*@__PURE__*/ core.$constructor(\"$ZodCheckStartsWith\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const pattern = new RegExp(`^${util.escapeRegex(def.prefix)}.*`);\n def.pattern ?? (def.pattern = pattern);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.startsWith(def.prefix))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"starts_with\",\n prefix: def.prefix,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckEndsWith = /*@__PURE__*/ core.$constructor(\"$ZodCheckEndsWith\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const pattern = new RegExp(`.*${util.escapeRegex(def.suffix)}$`);\n def.pattern ?? (def.pattern = pattern);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.endsWith(def.suffix))\n return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"ends_with\",\n suffix: def.suffix,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\n///////////////////////////////////\n///// $ZodCheckProperty /////\n///////////////////////////////////\nfunction handleCheckPropertyResult(result, payload, property) {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(property, result.issues));\n }\n}\nexport const $ZodCheckProperty = /*@__PURE__*/ core.$constructor(\"$ZodCheckProperty\", (inst, def) => {\n $ZodCheck.init(inst, def);\n inst._zod.check = (payload) => {\n const result = def.schema._zod.run({\n value: payload.value[def.property],\n issues: [],\n }, {});\n if (result instanceof Promise) {\n return result.then((result) => handleCheckPropertyResult(result, payload, def.property));\n }\n handleCheckPropertyResult(result, payload, def.property);\n return;\n };\n});\nexport const $ZodCheckMimeType = /*@__PURE__*/ core.$constructor(\"$ZodCheckMimeType\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const mimeSet = new Set(def.mime);\n inst._zod.onattach.push((inst) => {\n inst._zod.bag.mime = def.mime;\n });\n inst._zod.check = (payload) => {\n if (mimeSet.has(payload.value.type))\n return;\n payload.issues.push({\n code: \"invalid_value\",\n values: def.mime,\n input: payload.value.type,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCheckOverwrite = /*@__PURE__*/ core.$constructor(\"$ZodCheckOverwrite\", (inst, def) => {\n $ZodCheck.init(inst, def);\n inst._zod.check = (payload) => {\n payload.value = def.tx(payload.value);\n };\n});\n","export class Doc {\n constructor(args = []) {\n this.content = [];\n this.indent = 0;\n if (this)\n this.args = args;\n }\n indented(fn) {\n this.indent += 1;\n fn(this);\n this.indent -= 1;\n }\n write(arg) {\n if (typeof arg === \"function\") {\n arg(this, { execution: \"sync\" });\n arg(this, { execution: \"async\" });\n return;\n }\n const content = arg;\n const lines = content.split(\"\\n\").filter((x) => x);\n const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length));\n const dedented = lines.map((x) => x.slice(minIndent)).map((x) => \" \".repeat(this.indent * 2) + x);\n for (const line of dedented) {\n this.content.push(line);\n }\n }\n compile() {\n const F = Function;\n const args = this?.args;\n const content = this?.content ?? [``];\n const lines = [...content.map((x) => ` ${x}`)];\n // console.log(lines.join(\"\\n\"));\n return new F(...args, lines.join(\"\\n\"));\n }\n}\n","export const version = {\n major: 4,\n minor: 1,\n patch: 8,\n};\n","import * as checks from \"./checks.js\";\nimport * as core from \"./core.js\";\nimport { Doc } from \"./doc.js\";\nimport { parse, parseAsync, safeParse, safeParseAsync } from \"./parse.js\";\nimport * as regexes from \"./regexes.js\";\nimport * as util from \"./util.js\";\nimport { version } from \"./versions.js\";\nexport const $ZodType = /*@__PURE__*/ core.$constructor(\"$ZodType\", (inst, def) => {\n var _a;\n inst ?? (inst = {});\n inst._zod.def = def; // set _def property\n inst._zod.bag = inst._zod.bag || {}; // initialize _bag object\n inst._zod.version = version;\n const checks = [...(inst._zod.def.checks ?? [])];\n // if inst is itself a checks.$ZodCheck, run it as a check\n if (inst._zod.traits.has(\"$ZodCheck\")) {\n checks.unshift(inst);\n }\n for (const ch of checks) {\n for (const fn of ch._zod.onattach) {\n fn(inst);\n }\n }\n if (checks.length === 0) {\n // deferred initializer\n // inst._zod.parse is not yet defined\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n inst._zod.deferred?.push(() => {\n inst._zod.run = inst._zod.parse;\n });\n }\n else {\n const runChecks = (payload, checks, ctx) => {\n let isAborted = util.aborted(payload);\n let asyncResult;\n for (const ch of checks) {\n if (ch._zod.def.when) {\n const shouldRun = ch._zod.def.when(payload);\n if (!shouldRun)\n continue;\n }\n else if (isAborted) {\n continue;\n }\n const currLen = payload.issues.length;\n const _ = ch._zod.check(payload);\n if (_ instanceof Promise && ctx?.async === false) {\n throw new core.$ZodAsyncError();\n }\n if (asyncResult || _ instanceof Promise) {\n asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {\n await _;\n const nextLen = payload.issues.length;\n if (nextLen === currLen)\n return;\n if (!isAborted)\n isAborted = util.aborted(payload, currLen);\n });\n }\n else {\n const nextLen = payload.issues.length;\n if (nextLen === currLen)\n continue;\n if (!isAborted)\n isAborted = util.aborted(payload, currLen);\n }\n }\n if (asyncResult) {\n return asyncResult.then(() => {\n return payload;\n });\n }\n return payload;\n };\n // const handleChecksResult = (\n // checkResult: ParsePayload,\n // originalResult: ParsePayload,\n // ctx: ParseContextInternal\n // ): util.MaybeAsync => {\n // // if the checks mutated the value && there are no issues, re-parse the result\n // if (checkResult.value !== originalResult.value && !checkResult.issues.length)\n // return inst._zod.parse(checkResult, ctx);\n // return originalResult;\n // };\n const handleCanaryResult = (canary, payload, ctx) => {\n // abort if the canary is aborted\n if (util.aborted(canary)) {\n canary.aborted = true;\n return canary;\n }\n // run checks first, then\n const checkResult = runChecks(payload, checks, ctx);\n if (checkResult instanceof Promise) {\n if (ctx.async === false)\n throw new core.$ZodAsyncError();\n return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));\n }\n return inst._zod.parse(checkResult, ctx);\n };\n inst._zod.run = (payload, ctx) => {\n if (ctx.skipChecks) {\n return inst._zod.parse(payload, ctx);\n }\n if (ctx.direction === \"backward\") {\n // run canary\n // initial pass (no checks)\n const canary = inst._zod.parse({ value: payload.value, issues: [] }, { ...ctx, skipChecks: true });\n if (canary instanceof Promise) {\n return canary.then((canary) => {\n return handleCanaryResult(canary, payload, ctx);\n });\n }\n return handleCanaryResult(canary, payload, ctx);\n }\n // forward\n const result = inst._zod.parse(payload, ctx);\n if (result instanceof Promise) {\n if (ctx.async === false)\n throw new core.$ZodAsyncError();\n return result.then((result) => runChecks(result, checks, ctx));\n }\n return runChecks(result, checks, ctx);\n };\n }\n inst[\"~standard\"] = {\n validate: (value) => {\n try {\n const r = safeParse(inst, value);\n return r.success ? { value: r.data } : { issues: r.error?.issues };\n }\n catch (_) {\n return safeParseAsync(inst, value).then((r) => (r.success ? { value: r.data } : { issues: r.error?.issues }));\n }\n },\n vendor: \"zod\",\n version: 1,\n };\n});\nexport { clone } from \"./util.js\";\nexport const $ZodString = /*@__PURE__*/ core.$constructor(\"$ZodString\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = [...(inst?._zod.bag?.patterns ?? [])].pop() ?? regexes.string(inst._zod.bag);\n inst._zod.parse = (payload, _) => {\n if (def.coerce)\n try {\n payload.value = String(payload.value);\n }\n catch (_) { }\n if (typeof payload.value === \"string\")\n return payload;\n payload.issues.push({\n expected: \"string\",\n code: \"invalid_type\",\n input: payload.value,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodStringFormat = /*@__PURE__*/ core.$constructor(\"$ZodStringFormat\", (inst, def) => {\n // check initialization must come first\n checks.$ZodCheckStringFormat.init(inst, def);\n $ZodString.init(inst, def);\n});\nexport const $ZodGUID = /*@__PURE__*/ core.$constructor(\"$ZodGUID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.guid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodUUID = /*@__PURE__*/ core.$constructor(\"$ZodUUID\", (inst, def) => {\n if (def.version) {\n const versionMap = {\n v1: 1,\n v2: 2,\n v3: 3,\n v4: 4,\n v5: 5,\n v6: 6,\n v7: 7,\n v8: 8,\n };\n const v = versionMap[def.version];\n if (v === undefined)\n throw new Error(`Invalid UUID version: \"${def.version}\"`);\n def.pattern ?? (def.pattern = regexes.uuid(v));\n }\n else\n def.pattern ?? (def.pattern = regexes.uuid());\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodEmail = /*@__PURE__*/ core.$constructor(\"$ZodEmail\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.email);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodURL = /*@__PURE__*/ core.$constructor(\"$ZodURL\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n try {\n // Trim whitespace from input\n const trimmed = payload.value.trim();\n // @ts-ignore\n const url = new URL(trimmed);\n if (def.hostname) {\n def.hostname.lastIndex = 0;\n if (!def.hostname.test(url.hostname)) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid hostname\",\n pattern: regexes.hostname.source,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n }\n if (def.protocol) {\n def.protocol.lastIndex = 0;\n if (!def.protocol.test(url.protocol.endsWith(\":\") ? url.protocol.slice(0, -1) : url.protocol)) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid protocol\",\n pattern: def.protocol.source,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n }\n // Set the output value based on normalize flag\n if (def.normalize) {\n // Use normalized URL\n payload.value = url.href;\n }\n else {\n // Preserve the original input (trimmed)\n payload.value = trimmed;\n }\n return;\n }\n catch (_) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n };\n});\nexport const $ZodEmoji = /*@__PURE__*/ core.$constructor(\"$ZodEmoji\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.emoji());\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodNanoID = /*@__PURE__*/ core.$constructor(\"$ZodNanoID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.nanoid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodCUID = /*@__PURE__*/ core.$constructor(\"$ZodCUID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.cuid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodCUID2 = /*@__PURE__*/ core.$constructor(\"$ZodCUID2\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.cuid2);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodULID = /*@__PURE__*/ core.$constructor(\"$ZodULID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.ulid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodXID = /*@__PURE__*/ core.$constructor(\"$ZodXID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.xid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodKSUID = /*@__PURE__*/ core.$constructor(\"$ZodKSUID\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.ksuid);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodISODateTime = /*@__PURE__*/ core.$constructor(\"$ZodISODateTime\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.datetime(def));\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodISODate = /*@__PURE__*/ core.$constructor(\"$ZodISODate\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.date);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodISOTime = /*@__PURE__*/ core.$constructor(\"$ZodISOTime\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.time(def));\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodISODuration = /*@__PURE__*/ core.$constructor(\"$ZodISODuration\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.duration);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodIPv4 = /*@__PURE__*/ core.$constructor(\"$ZodIPv4\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.ipv4);\n $ZodStringFormat.init(inst, def);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = `ipv4`;\n });\n});\nexport const $ZodIPv6 = /*@__PURE__*/ core.$constructor(\"$ZodIPv6\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.ipv6);\n $ZodStringFormat.init(inst, def);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = `ipv6`;\n });\n inst._zod.check = (payload) => {\n try {\n // @ts-ignore\n new URL(`http://[${payload.value}]`);\n // return;\n }\n catch {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"ipv6\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n };\n});\nexport const $ZodCIDRv4 = /*@__PURE__*/ core.$constructor(\"$ZodCIDRv4\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.cidrv4);\n $ZodStringFormat.init(inst, def);\n});\nexport const $ZodCIDRv6 = /*@__PURE__*/ core.$constructor(\"$ZodCIDRv6\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.cidrv6); // not used for validation\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n const parts = payload.value.split(\"/\");\n try {\n if (parts.length !== 2)\n throw new Error();\n const [address, prefix] = parts;\n if (!prefix)\n throw new Error();\n const prefixNum = Number(prefix);\n if (`${prefixNum}` !== prefix)\n throw new Error();\n if (prefixNum < 0 || prefixNum > 128)\n throw new Error();\n // @ts-ignore\n new URL(`http://[${address}]`);\n }\n catch {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"cidrv6\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n }\n };\n});\n////////////////////////////// ZodBase64 //////////////////////////////\nexport function isValidBase64(data) {\n if (data === \"\")\n return true;\n if (data.length % 4 !== 0)\n return false;\n try {\n // @ts-ignore\n atob(data);\n return true;\n }\n catch {\n return false;\n }\n}\nexport const $ZodBase64 = /*@__PURE__*/ core.$constructor(\"$ZodBase64\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.base64);\n $ZodStringFormat.init(inst, def);\n inst._zod.onattach.push((inst) => {\n inst._zod.bag.contentEncoding = \"base64\";\n });\n inst._zod.check = (payload) => {\n if (isValidBase64(payload.value))\n return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"base64\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\n////////////////////////////// ZodBase64 //////////////////////////////\nexport function isValidBase64URL(data) {\n if (!regexes.base64url.test(data))\n return false;\n const base64 = data.replace(/[-_]/g, (c) => (c === \"-\" ? \"+\" : \"/\"));\n const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, \"=\");\n return isValidBase64(padded);\n}\nexport const $ZodBase64URL = /*@__PURE__*/ core.$constructor(\"$ZodBase64URL\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.base64url);\n $ZodStringFormat.init(inst, def);\n inst._zod.onattach.push((inst) => {\n inst._zod.bag.contentEncoding = \"base64url\";\n });\n inst._zod.check = (payload) => {\n if (isValidBase64URL(payload.value))\n return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"base64url\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodE164 = /*@__PURE__*/ core.$constructor(\"$ZodE164\", (inst, def) => {\n def.pattern ?? (def.pattern = regexes.e164);\n $ZodStringFormat.init(inst, def);\n});\n////////////////////////////// ZodJWT //////////////////////////////\nexport function isValidJWT(token, algorithm = null) {\n try {\n const tokensParts = token.split(\".\");\n if (tokensParts.length !== 3)\n return false;\n const [header] = tokensParts;\n if (!header)\n return false;\n // @ts-ignore\n const parsedHeader = JSON.parse(atob(header));\n if (\"typ\" in parsedHeader && parsedHeader?.typ !== \"JWT\")\n return false;\n if (!parsedHeader.alg)\n return false;\n if (algorithm && (!(\"alg\" in parsedHeader) || parsedHeader.alg !== algorithm))\n return false;\n return true;\n }\n catch {\n return false;\n }\n}\nexport const $ZodJWT = /*@__PURE__*/ core.$constructor(\"$ZodJWT\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n if (isValidJWT(payload.value, def.alg))\n return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"jwt\",\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodCustomStringFormat = /*@__PURE__*/ core.$constructor(\"$ZodCustomStringFormat\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n if (def.fn(payload.value))\n return;\n payload.issues.push({\n code: \"invalid_format\",\n format: def.format,\n input: payload.value,\n inst,\n continue: !def.abort,\n });\n };\n});\nexport const $ZodNumber = /*@__PURE__*/ core.$constructor(\"$ZodNumber\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = inst._zod.bag.pattern ?? regexes.number;\n inst._zod.parse = (payload, _ctx) => {\n if (def.coerce)\n try {\n payload.value = Number(payload.value);\n }\n catch (_) { }\n const input = payload.value;\n if (typeof input === \"number\" && !Number.isNaN(input) && Number.isFinite(input)) {\n return payload;\n }\n const received = typeof input === \"number\"\n ? Number.isNaN(input)\n ? \"NaN\"\n : !Number.isFinite(input)\n ? \"Infinity\"\n : undefined\n : undefined;\n payload.issues.push({\n expected: \"number\",\n code: \"invalid_type\",\n input,\n inst,\n ...(received ? { received } : {}),\n });\n return payload;\n };\n});\nexport const $ZodNumberFormat = /*@__PURE__*/ core.$constructor(\"$ZodNumber\", (inst, def) => {\n checks.$ZodCheckNumberFormat.init(inst, def);\n $ZodNumber.init(inst, def); // no format checksp\n});\nexport const $ZodBoolean = /*@__PURE__*/ core.$constructor(\"$ZodBoolean\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = regexes.boolean;\n inst._zod.parse = (payload, _ctx) => {\n if (def.coerce)\n try {\n payload.value = Boolean(payload.value);\n }\n catch (_) { }\n const input = payload.value;\n if (typeof input === \"boolean\")\n return payload;\n payload.issues.push({\n expected: \"boolean\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodBigInt = /*@__PURE__*/ core.$constructor(\"$ZodBigInt\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = regexes.bigint;\n inst._zod.parse = (payload, _ctx) => {\n if (def.coerce)\n try {\n payload.value = BigInt(payload.value);\n }\n catch (_) { }\n if (typeof payload.value === \"bigint\")\n return payload;\n payload.issues.push({\n expected: \"bigint\",\n code: \"invalid_type\",\n input: payload.value,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodBigIntFormat = /*@__PURE__*/ core.$constructor(\"$ZodBigInt\", (inst, def) => {\n checks.$ZodCheckBigIntFormat.init(inst, def);\n $ZodBigInt.init(inst, def); // no format checks\n});\nexport const $ZodSymbol = /*@__PURE__*/ core.$constructor(\"$ZodSymbol\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (typeof input === \"symbol\")\n return payload;\n payload.issues.push({\n expected: \"symbol\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodUndefined = /*@__PURE__*/ core.$constructor(\"$ZodUndefined\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = regexes.undefined;\n inst._zod.values = new Set([undefined]);\n inst._zod.optin = \"optional\";\n inst._zod.optout = \"optional\";\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (typeof input === \"undefined\")\n return payload;\n payload.issues.push({\n expected: \"undefined\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodNull = /*@__PURE__*/ core.$constructor(\"$ZodNull\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = regexes.null;\n inst._zod.values = new Set([null]);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (input === null)\n return payload;\n payload.issues.push({\n expected: \"null\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodAny = /*@__PURE__*/ core.$constructor(\"$ZodAny\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nexport const $ZodUnknown = /*@__PURE__*/ core.$constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nexport const $ZodNever = /*@__PURE__*/ core.$constructor(\"$ZodNever\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n payload.issues.push({\n expected: \"never\",\n code: \"invalid_type\",\n input: payload.value,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodVoid = /*@__PURE__*/ core.$constructor(\"$ZodVoid\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (typeof input === \"undefined\")\n return payload;\n payload.issues.push({\n expected: \"void\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodDate = /*@__PURE__*/ core.$constructor(\"$ZodDate\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n if (def.coerce) {\n try {\n payload.value = new Date(payload.value);\n }\n catch (_err) { }\n }\n const input = payload.value;\n const isDate = input instanceof Date;\n const isValidDate = isDate && !Number.isNaN(input.getTime());\n if (isValidDate)\n return payload;\n payload.issues.push({\n expected: \"date\",\n code: \"invalid_type\",\n input,\n ...(isDate ? { received: \"Invalid Date\" } : {}),\n inst,\n });\n return payload;\n };\n});\nfunction handleArrayResult(result, final, index) {\n if (result.issues.length) {\n final.issues.push(...util.prefixIssues(index, result.issues));\n }\n final.value[index] = result.value;\n}\nexport const $ZodArray = /*@__PURE__*/ core.$constructor(\"$ZodArray\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n expected: \"array\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n payload.value = Array(input.length);\n const proms = [];\n for (let i = 0; i < input.length; i++) {\n const item = input[i];\n const result = def.element._zod.run({\n value: item,\n issues: [],\n }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => handleArrayResult(result, payload, i)));\n }\n else {\n handleArrayResult(result, payload, i);\n }\n }\n if (proms.length) {\n return Promise.all(proms).then(() => payload);\n }\n return payload; //handleArrayResultsAsync(parseResults, final);\n };\n});\nfunction handlePropertyResult(result, final, key, input) {\n if (result.issues.length) {\n final.issues.push(...util.prefixIssues(key, result.issues));\n }\n if (result.value === undefined) {\n if (key in input) {\n final.value[key] = undefined;\n }\n }\n else {\n final.value[key] = result.value;\n }\n}\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n for (const k of keys) {\n if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) {\n throw new Error(`Invalid element at key \"${k}\": expected a Zod schema`);\n }\n }\n const okeys = util.optionalKeys(def.shape);\n return {\n ...def,\n keys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys),\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst) {\n const unrecognized = [];\n // iterate over input keys\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n for (const key of Object.keys(input)) {\n if (keySet.has(key))\n continue;\n if (t === \"never\") {\n unrecognized.push(key);\n continue;\n }\n const r = _catchall.run({ value: input[key], issues: [] }, ctx);\n if (r instanceof Promise) {\n proms.push(r.then((r) => handlePropertyResult(r, payload, key, input)));\n }\n else {\n handlePropertyResult(r, payload, key, input);\n }\n }\n if (unrecognized.length) {\n payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst,\n });\n }\n if (!proms.length)\n return payload;\n return Promise.all(proms).then(() => {\n return payload;\n });\n}\nexport const $ZodObject = /*@__PURE__*/ core.$constructor(\"$ZodObject\", (inst, def) => {\n // requires cast because technically $ZodObject doesn't extend\n $ZodType.init(inst, def);\n const _normalized = util.cached(() => normalizeDef(def));\n util.defineLazy(inst._zod, \"propValues\", () => {\n const shape = def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n propValues[key] ?? (propValues[key] = new Set());\n for (const v of field.values)\n propValues[key].add(v);\n }\n }\n return propValues;\n });\n const isObject = util.isObject;\n const catchall = def.catchall;\n let value;\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n payload.value = {};\n const proms = [];\n const shape = value.shape;\n for (const key of value.keys) {\n const el = shape[key];\n const r = el._zod.run({ value: input[key], issues: [] }, ctx);\n if (r instanceof Promise) {\n proms.push(r.then((r) => handlePropertyResult(r, payload, key, input)));\n }\n else {\n handlePropertyResult(r, payload, key, input);\n }\n }\n if (!catchall) {\n return proms.length ? Promise.all(proms).then(() => payload) : payload;\n }\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst);\n };\n});\nexport const $ZodObjectJIT = /*@__PURE__*/ core.$constructor(\"$ZodObjectJIT\", (inst, def) => {\n // requires cast because technically $ZodObject doesn't extend\n $ZodObject.init(inst, def);\n const superParse = inst._zod.parse;\n const _normalized = util.cached(() => normalizeDef(def));\n const generateFastpass = (shape) => {\n const doc = new Doc([\"shape\", \"payload\", \"ctx\"]);\n const normalized = _normalized.value;\n const parseStr = (key) => {\n const k = util.esc(key);\n return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;\n };\n doc.write(`const input = payload.value;`);\n const ids = Object.create(null);\n let counter = 0;\n for (const key of normalized.keys) {\n ids[key] = `key_${counter++}`;\n }\n // A: preserve key order {\n doc.write(`const newResult = {};`);\n for (const key of normalized.keys) {\n const id = ids[key];\n const k = util.esc(key);\n doc.write(`const ${id} = ${parseStr(key)};`);\n doc.write(`\n if (${id}.issues.length) {\n payload.issues = payload.issues.concat(${id}.issues.map(iss => ({\n ...iss,\n path: iss.path ? [${k}, ...iss.path] : [${k}]\n })));\n }\n \n \n if (${id}.value === undefined) {\n if (${k} in input) {\n newResult[${k}] = undefined;\n }\n } else {\n newResult[${k}] = ${id}.value;\n }\n \n `);\n }\n doc.write(`payload.value = newResult;`);\n doc.write(`return payload;`);\n const fn = doc.compile();\n return (payload, ctx) => fn(shape, payload, ctx);\n };\n let fastpass;\n const isObject = util.isObject;\n const jit = !core.globalConfig.jitless;\n const allowsEval = util.allowsEval;\n const fastEnabled = jit && allowsEval.value; // && !def.catchall;\n const catchall = def.catchall;\n let value;\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n if (jit && fastEnabled && ctx?.async === false && ctx.jitless !== true) {\n // always synchronous\n if (!fastpass)\n fastpass = generateFastpass(def.shape);\n payload = fastpass(payload, ctx);\n if (!catchall)\n return payload;\n return handleCatchall([], input, payload, ctx, value, inst);\n }\n return superParse(payload, ctx);\n };\n});\nfunction handleUnionResults(results, final, inst, ctx) {\n for (const result of results) {\n if (result.issues.length === 0) {\n final.value = result.value;\n return final;\n }\n }\n const nonaborted = results.filter((r) => !util.aborted(r));\n if (nonaborted.length === 1) {\n final.value = nonaborted[0].value;\n return nonaborted[0];\n }\n final.issues.push({\n code: \"invalid_union\",\n input: final.value,\n inst,\n errors: results.map((result) => result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))),\n });\n return final;\n}\nexport const $ZodUnion = /*@__PURE__*/ core.$constructor(\"$ZodUnion\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"optin\", () => def.options.some((o) => o._zod.optin === \"optional\") ? \"optional\" : undefined);\n util.defineLazy(inst._zod, \"optout\", () => def.options.some((o) => o._zod.optout === \"optional\") ? \"optional\" : undefined);\n util.defineLazy(inst._zod, \"values\", () => {\n if (def.options.every((o) => o._zod.values)) {\n return new Set(def.options.flatMap((option) => Array.from(option._zod.values)));\n }\n return undefined;\n });\n util.defineLazy(inst._zod, \"pattern\", () => {\n if (def.options.every((o) => o._zod.pattern)) {\n const patterns = def.options.map((o) => o._zod.pattern);\n return new RegExp(`^(${patterns.map((p) => util.cleanRegex(p.source)).join(\"|\")})$`);\n }\n return undefined;\n });\n const single = def.options.length === 1;\n const first = def.options[0]._zod.run;\n inst._zod.parse = (payload, ctx) => {\n if (single) {\n return first(payload, ctx);\n }\n let async = false;\n const results = [];\n for (const option of def.options) {\n const result = option._zod.run({\n value: payload.value,\n issues: [],\n }, ctx);\n if (result instanceof Promise) {\n results.push(result);\n async = true;\n }\n else {\n if (result.issues.length === 0)\n return result;\n results.push(result);\n }\n }\n if (!async)\n return handleUnionResults(results, payload, inst, ctx);\n return Promise.all(results).then((results) => {\n return handleUnionResults(results, payload, inst, ctx);\n });\n };\n});\nexport const $ZodDiscriminatedUnion = \n/*@__PURE__*/\ncore.$constructor(\"$ZodDiscriminatedUnion\", (inst, def) => {\n $ZodUnion.init(inst, def);\n const _super = inst._zod.parse;\n util.defineLazy(inst._zod, \"propValues\", () => {\n const propValues = {};\n for (const option of def.options) {\n const pv = option._zod.propValues;\n if (!pv || Object.keys(pv).length === 0)\n throw new Error(`Invalid discriminated union option at index \"${def.options.indexOf(option)}\"`);\n for (const [k, v] of Object.entries(pv)) {\n if (!propValues[k])\n propValues[k] = new Set();\n for (const val of v) {\n propValues[k].add(val);\n }\n }\n }\n return propValues;\n });\n const disc = util.cached(() => {\n const opts = def.options;\n const map = new Map();\n for (const o of opts) {\n const values = o._zod.propValues?.[def.discriminator];\n if (!values || values.size === 0)\n throw new Error(`Invalid discriminated union option at index \"${def.options.indexOf(o)}\"`);\n for (const v of values) {\n if (map.has(v)) {\n throw new Error(`Duplicate discriminator value \"${String(v)}\"`);\n }\n map.set(v, o);\n }\n }\n return map;\n });\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!util.isObject(input)) {\n payload.issues.push({\n code: \"invalid_type\",\n expected: \"object\",\n input,\n inst,\n });\n return payload;\n }\n const opt = disc.value.get(input?.[def.discriminator]);\n if (opt) {\n return opt._zod.run(payload, ctx);\n }\n if (def.unionFallback) {\n return _super(payload, ctx);\n }\n // no matching discriminator\n payload.issues.push({\n code: \"invalid_union\",\n errors: [],\n note: \"No matching discriminator\",\n discriminator: def.discriminator,\n input,\n path: [def.discriminator],\n inst,\n });\n return payload;\n };\n});\nexport const $ZodIntersection = /*@__PURE__*/ core.$constructor(\"$ZodIntersection\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n const left = def.left._zod.run({ value: input, issues: [] }, ctx);\n const right = def.right._zod.run({ value: input, issues: [] }, ctx);\n const async = left instanceof Promise || right instanceof Promise;\n if (async) {\n return Promise.all([left, right]).then(([left, right]) => {\n return handleIntersectionResults(payload, left, right);\n });\n }\n return handleIntersectionResults(payload, left, right);\n };\n});\nfunction mergeValues(a, b) {\n // const aType = parse.t(a);\n // const bType = parse.t(b);\n if (a === b) {\n return { valid: true, data: a };\n }\n if (a instanceof Date && b instanceof Date && +a === +b) {\n return { valid: true, data: a };\n }\n if (util.isPlainObject(a) && util.isPlainObject(b)) {\n const bKeys = Object.keys(b);\n const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);\n const newObj = { ...a, ...b };\n for (const key of sharedKeys) {\n const sharedValue = mergeValues(a[key], b[key]);\n if (!sharedValue.valid) {\n return {\n valid: false,\n mergeErrorPath: [key, ...sharedValue.mergeErrorPath],\n };\n }\n newObj[key] = sharedValue.data;\n }\n return { valid: true, data: newObj };\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) {\n return { valid: false, mergeErrorPath: [] };\n }\n const newArray = [];\n for (let index = 0; index < a.length; index++) {\n const itemA = a[index];\n const itemB = b[index];\n const sharedValue = mergeValues(itemA, itemB);\n if (!sharedValue.valid) {\n return {\n valid: false,\n mergeErrorPath: [index, ...sharedValue.mergeErrorPath],\n };\n }\n newArray.push(sharedValue.data);\n }\n return { valid: true, data: newArray };\n }\n return { valid: false, mergeErrorPath: [] };\n}\nfunction handleIntersectionResults(result, left, right) {\n if (left.issues.length) {\n result.issues.push(...left.issues);\n }\n if (right.issues.length) {\n result.issues.push(...right.issues);\n }\n if (util.aborted(result))\n return result;\n const merged = mergeValues(left.value, right.value);\n if (!merged.valid) {\n throw new Error(`Unmergable intersection. Error path: ` + `${JSON.stringify(merged.mergeErrorPath)}`);\n }\n result.value = merged.data;\n return result;\n}\nexport const $ZodTuple = /*@__PURE__*/ core.$constructor(\"$ZodTuple\", (inst, def) => {\n $ZodType.init(inst, def);\n const items = def.items;\n const optStart = items.length - [...items].reverse().findIndex((item) => item._zod.optin !== \"optional\");\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n input,\n inst,\n expected: \"tuple\",\n code: \"invalid_type\",\n });\n return payload;\n }\n payload.value = [];\n const proms = [];\n if (!def.rest) {\n const tooBig = input.length > items.length;\n const tooSmall = input.length < optStart - 1;\n if (tooBig || tooSmall) {\n payload.issues.push({\n ...(tooBig ? { code: \"too_big\", maximum: items.length } : { code: \"too_small\", minimum: items.length }),\n input,\n inst,\n origin: \"array\",\n });\n return payload;\n }\n }\n let i = -1;\n for (const item of items) {\n i++;\n if (i >= input.length)\n if (i >= optStart)\n continue;\n const result = item._zod.run({\n value: input[i],\n issues: [],\n }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => handleTupleResult(result, payload, i)));\n }\n else {\n handleTupleResult(result, payload, i);\n }\n }\n if (def.rest) {\n const rest = input.slice(items.length);\n for (const el of rest) {\n i++;\n const result = def.rest._zod.run({\n value: el,\n issues: [],\n }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => handleTupleResult(result, payload, i)));\n }\n else {\n handleTupleResult(result, payload, i);\n }\n }\n }\n if (proms.length)\n return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleTupleResult(result, final, index) {\n if (result.issues.length) {\n final.issues.push(...util.prefixIssues(index, result.issues));\n }\n final.value[index] = result.value;\n}\nexport const $ZodRecord = /*@__PURE__*/ core.$constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!util.isPlainObject(input)) {\n payload.issues.push({\n expected: \"record\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n const proms = [];\n if (def.keyType._zod.values) {\n const values = def.keyType._zod.values;\n payload.value = {};\n for (const key of values) {\n if (typeof key === \"string\" || typeof key === \"number\" || typeof key === \"symbol\") {\n const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(key, result.issues));\n }\n payload.value[key] = result.value;\n }));\n }\n else {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(key, result.issues));\n }\n payload.value[key] = result.value;\n }\n }\n }\n let unrecognized;\n for (const key in input) {\n if (!values.has(key)) {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n }\n if (unrecognized && unrecognized.length > 0) {\n payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n });\n }\n }\n else {\n payload.value = {};\n for (const key of Reflect.ownKeys(input)) {\n if (key === \"__proto__\")\n continue;\n const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);\n if (keyResult instanceof Promise) {\n throw new Error(\"Async schemas not supported in object keys currently\");\n }\n if (keyResult.issues.length) {\n payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n input: key,\n path: [key],\n inst,\n });\n payload.value[keyResult.value] = keyResult.value;\n continue;\n }\n const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(key, result.issues));\n }\n payload.value[keyResult.value] = result.value;\n }));\n }\n else {\n if (result.issues.length) {\n payload.issues.push(...util.prefixIssues(key, result.issues));\n }\n payload.value[keyResult.value] = result.value;\n }\n }\n }\n if (proms.length) {\n return Promise.all(proms).then(() => payload);\n }\n return payload;\n };\n});\nexport const $ZodMap = /*@__PURE__*/ core.$constructor(\"$ZodMap\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!(input instanceof Map)) {\n payload.issues.push({\n expected: \"map\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n }\n const proms = [];\n payload.value = new Map();\n for (const [key, value] of input) {\n const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);\n const valueResult = def.valueType._zod.run({ value: value, issues: [] }, ctx);\n if (keyResult instanceof Promise || valueResult instanceof Promise) {\n proms.push(Promise.all([keyResult, valueResult]).then(([keyResult, valueResult]) => {\n handleMapResult(keyResult, valueResult, payload, key, input, inst, ctx);\n }));\n }\n else {\n handleMapResult(keyResult, valueResult, payload, key, input, inst, ctx);\n }\n }\n if (proms.length)\n return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {\n if (keyResult.issues.length) {\n if (util.propertyKeyTypes.has(typeof key)) {\n final.issues.push(...util.prefixIssues(key, keyResult.issues));\n }\n else {\n final.issues.push({\n code: \"invalid_key\",\n origin: \"map\",\n input,\n inst,\n issues: keyResult.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n });\n }\n }\n if (valueResult.issues.length) {\n if (util.propertyKeyTypes.has(typeof key)) {\n final.issues.push(...util.prefixIssues(key, valueResult.issues));\n }\n else {\n final.issues.push({\n origin: \"map\",\n code: \"invalid_element\",\n input,\n inst,\n key: key,\n issues: valueResult.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n });\n }\n }\n final.value.set(keyResult.value, valueResult.value);\n}\nexport const $ZodSet = /*@__PURE__*/ core.$constructor(\"$ZodSet\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!(input instanceof Set)) {\n payload.issues.push({\n input,\n inst,\n expected: \"set\",\n code: \"invalid_type\",\n });\n return payload;\n }\n const proms = [];\n payload.value = new Set();\n for (const item of input) {\n const result = def.valueType._zod.run({ value: item, issues: [] }, ctx);\n if (result instanceof Promise) {\n proms.push(result.then((result) => handleSetResult(result, payload)));\n }\n else\n handleSetResult(result, payload);\n }\n if (proms.length)\n return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleSetResult(result, final) {\n if (result.issues.length) {\n final.issues.push(...result.issues);\n }\n final.value.add(result.value);\n}\nexport const $ZodEnum = /*@__PURE__*/ core.$constructor(\"$ZodEnum\", (inst, def) => {\n $ZodType.init(inst, def);\n const values = util.getEnumValues(def.entries);\n const valuesSet = new Set(values);\n inst._zod.values = valuesSet;\n inst._zod.pattern = new RegExp(`^(${values\n .filter((k) => util.propertyKeyTypes.has(typeof k))\n .map((o) => (typeof o === \"string\" ? util.escapeRegex(o) : o.toString()))\n .join(\"|\")})$`);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (valuesSet.has(input)) {\n return payload;\n }\n payload.issues.push({\n code: \"invalid_value\",\n values,\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodLiteral = /*@__PURE__*/ core.$constructor(\"$ZodLiteral\", (inst, def) => {\n $ZodType.init(inst, def);\n if (def.values.length === 0) {\n throw new Error(\"Cannot create literal schema with no valid values\");\n }\n inst._zod.values = new Set(def.values);\n inst._zod.pattern = new RegExp(`^(${def.values\n .map((o) => (typeof o === \"string\" ? util.escapeRegex(o) : o ? util.escapeRegex(o.toString()) : String(o)))\n .join(\"|\")})$`);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (inst._zod.values.has(input)) {\n return payload;\n }\n payload.issues.push({\n code: \"invalid_value\",\n values: def.values,\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodFile = /*@__PURE__*/ core.$constructor(\"$ZodFile\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n // @ts-ignore\n if (input instanceof File)\n return payload;\n payload.issues.push({\n expected: \"file\",\n code: \"invalid_type\",\n input,\n inst,\n });\n return payload;\n };\n});\nexport const $ZodTransform = /*@__PURE__*/ core.$constructor(\"$ZodTransform\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n throw new core.$ZodEncodeError(inst.constructor.name);\n }\n const _out = def.transform(payload.value, payload);\n if (ctx.async) {\n const output = _out instanceof Promise ? _out : Promise.resolve(_out);\n return output.then((output) => {\n payload.value = output;\n return payload;\n });\n }\n if (_out instanceof Promise) {\n throw new core.$ZodAsyncError();\n }\n payload.value = _out;\n return payload;\n };\n});\nfunction handleOptionalResult(result, input) {\n if (result.issues.length && input === undefined) {\n return { issues: [], value: undefined };\n }\n return result;\n}\nexport const $ZodOptional = /*@__PURE__*/ core.$constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n inst._zod.optout = \"optional\";\n util.defineLazy(inst._zod, \"values\", () => {\n return def.innerType._zod.values ? new Set([...def.innerType._zod.values, undefined]) : undefined;\n });\n util.defineLazy(inst._zod, \"pattern\", () => {\n const pattern = def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${util.cleanRegex(pattern.source)})?$`) : undefined;\n });\n inst._zod.parse = (payload, ctx) => {\n if (def.innerType._zod.optin === \"optional\") {\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise)\n return result.then((r) => handleOptionalResult(r, payload.value));\n return handleOptionalResult(result, payload.value);\n }\n if (payload.value === undefined) {\n return payload;\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nexport const $ZodNullable = /*@__PURE__*/ core.$constructor(\"$ZodNullable\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"optin\", () => def.innerType._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n util.defineLazy(inst._zod, \"pattern\", () => {\n const pattern = def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${util.cleanRegex(pattern.source)}|null)$`) : undefined;\n });\n util.defineLazy(inst._zod, \"values\", () => {\n return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : undefined;\n });\n inst._zod.parse = (payload, ctx) => {\n // Forward direction (decode): allow null to pass through\n if (payload.value === null)\n return payload;\n return def.innerType._zod.run(payload, ctx);\n };\n});\nexport const $ZodDefault = /*@__PURE__*/ core.$constructor(\"$ZodDefault\", (inst, def) => {\n $ZodType.init(inst, def);\n // inst._zod.qin = \"true\";\n inst._zod.optin = \"optional\";\n util.defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n return def.innerType._zod.run(payload, ctx);\n }\n // Forward direction (decode): apply defaults for undefined input\n if (payload.value === undefined) {\n payload.value = def.defaultValue;\n /**\n * $ZodDefault returns the default value immediately in forward direction.\n * It doesn't pass the default value into the validator (\"prefault\"). There's no reason to pass the default value through validation. The validity of the default is enforced by TypeScript statically. Otherwise, it's the responsibility of the user to ensure the default is valid. In the case of pipes with divergent in/out types, you can specify the default on the `in` schema of your ZodPipe to set a \"prefault\" for the pipe. */\n return payload;\n }\n // Forward direction: continue with default handling\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then((result) => handleDefaultResult(result, def));\n }\n return handleDefaultResult(result, def);\n };\n});\nfunction handleDefaultResult(payload, def) {\n if (payload.value === undefined) {\n payload.value = def.defaultValue;\n }\n return payload;\n}\nexport const $ZodPrefault = /*@__PURE__*/ core.$constructor(\"$ZodPrefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n util.defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n return def.innerType._zod.run(payload, ctx);\n }\n // Forward direction (decode): apply prefault for undefined input\n if (payload.value === undefined) {\n payload.value = def.defaultValue;\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nexport const $ZodNonOptional = /*@__PURE__*/ core.$constructor(\"$ZodNonOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"values\", () => {\n const v = def.innerType._zod.values;\n return v ? new Set([...v].filter((x) => x !== undefined)) : undefined;\n });\n inst._zod.parse = (payload, ctx) => {\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then((result) => handleNonOptionalResult(result, inst));\n }\n return handleNonOptionalResult(result, inst);\n };\n});\nfunction handleNonOptionalResult(payload, inst) {\n if (!payload.issues.length && payload.value === undefined) {\n payload.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: payload.value,\n inst,\n });\n }\n return payload;\n}\nexport const $ZodSuccess = /*@__PURE__*/ core.$constructor(\"$ZodSuccess\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n throw new core.$ZodEncodeError(\"ZodSuccess\");\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then((result) => {\n payload.value = result.issues.length === 0;\n return payload;\n });\n }\n payload.value = result.issues.length === 0;\n return payload;\n };\n});\nexport const $ZodCatch = /*@__PURE__*/ core.$constructor(\"$ZodCatch\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"optin\", () => def.innerType._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n util.defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n return def.innerType._zod.run(payload, ctx);\n }\n // Forward direction (decode): apply catch logic\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then((result) => {\n payload.value = result.value;\n if (result.issues.length) {\n payload.value = def.catchValue({\n ...payload,\n error: {\n issues: result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n },\n input: payload.value,\n });\n payload.issues = [];\n }\n return payload;\n });\n }\n payload.value = result.value;\n if (result.issues.length) {\n payload.value = def.catchValue({\n ...payload,\n error: {\n issues: result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())),\n },\n input: payload.value,\n });\n payload.issues = [];\n }\n return payload;\n };\n});\nexport const $ZodNaN = /*@__PURE__*/ core.$constructor(\"$ZodNaN\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n if (typeof payload.value !== \"number\" || !Number.isNaN(payload.value)) {\n payload.issues.push({\n input: payload.value,\n inst,\n expected: \"nan\",\n code: \"invalid_type\",\n });\n return payload;\n }\n return payload;\n };\n});\nexport const $ZodPipe = /*@__PURE__*/ core.$constructor(\"$ZodPipe\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"values\", () => def.in._zod.values);\n util.defineLazy(inst._zod, \"optin\", () => def.in._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.out._zod.optout);\n util.defineLazy(inst._zod, \"propValues\", () => def.in._zod.propValues);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n const right = def.out._zod.run(payload, ctx);\n if (right instanceof Promise) {\n return right.then((right) => handlePipeResult(right, def.in, ctx));\n }\n return handlePipeResult(right, def.in, ctx);\n }\n const left = def.in._zod.run(payload, ctx);\n if (left instanceof Promise) {\n return left.then((left) => handlePipeResult(left, def.out, ctx));\n }\n return handlePipeResult(left, def.out, ctx);\n };\n});\nfunction handlePipeResult(left, next, ctx) {\n if (left.issues.length) {\n // prevent further checks\n left.aborted = true;\n return left;\n }\n return next._zod.run({ value: left.value, issues: left.issues }, ctx);\n}\nexport const $ZodCodec = /*@__PURE__*/ core.$constructor(\"$ZodCodec\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"values\", () => def.in._zod.values);\n util.defineLazy(inst._zod, \"optin\", () => def.in._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.out._zod.optout);\n util.defineLazy(inst._zod, \"propValues\", () => def.in._zod.propValues);\n inst._zod.parse = (payload, ctx) => {\n const direction = ctx.direction || \"forward\";\n if (direction === \"forward\") {\n const left = def.in._zod.run(payload, ctx);\n if (left instanceof Promise) {\n return left.then((left) => handleCodecAResult(left, def, ctx));\n }\n return handleCodecAResult(left, def, ctx);\n }\n else {\n const right = def.out._zod.run(payload, ctx);\n if (right instanceof Promise) {\n return right.then((right) => handleCodecAResult(right, def, ctx));\n }\n return handleCodecAResult(right, def, ctx);\n }\n };\n});\nfunction handleCodecAResult(result, def, ctx) {\n if (result.issues.length) {\n // prevent further checks\n result.aborted = true;\n return result;\n }\n const direction = ctx.direction || \"forward\";\n if (direction === \"forward\") {\n const transformed = def.transform(result.value, result);\n if (transformed instanceof Promise) {\n return transformed.then((value) => handleCodecTxResult(result, value, def.out, ctx));\n }\n return handleCodecTxResult(result, transformed, def.out, ctx);\n }\n else {\n const transformed = def.reverseTransform(result.value, result);\n if (transformed instanceof Promise) {\n return transformed.then((value) => handleCodecTxResult(result, value, def.in, ctx));\n }\n return handleCodecTxResult(result, transformed, def.in, ctx);\n }\n}\nfunction handleCodecTxResult(left, value, nextSchema, ctx) {\n // Check if transform added any issues\n if (left.issues.length) {\n left.aborted = true;\n return left;\n }\n return nextSchema._zod.run({ value, issues: left.issues }, ctx);\n}\nexport const $ZodReadonly = /*@__PURE__*/ core.$constructor(\"$ZodReadonly\", (inst, def) => {\n $ZodType.init(inst, def);\n util.defineLazy(inst._zod, \"propValues\", () => def.innerType._zod.propValues);\n util.defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n util.defineLazy(inst._zod, \"optin\", () => def.innerType._zod.optin);\n util.defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n return def.innerType._zod.run(payload, ctx);\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) {\n return result.then(handleReadonlyResult);\n }\n return handleReadonlyResult(result);\n };\n});\nfunction handleReadonlyResult(payload) {\n payload.value = Object.freeze(payload.value);\n return payload;\n}\nexport const $ZodTemplateLiteral = /*@__PURE__*/ core.$constructor(\"$ZodTemplateLiteral\", (inst, def) => {\n $ZodType.init(inst, def);\n const regexParts = [];\n for (const part of def.parts) {\n if (typeof part === \"object\" && part !== null) {\n // is Zod schema\n if (!part._zod.pattern) {\n // if (!source)\n throw new Error(`Invalid template literal part, no pattern found: ${[...part._zod.traits].shift()}`);\n }\n const source = part._zod.pattern instanceof RegExp ? part._zod.pattern.source : part._zod.pattern;\n if (!source)\n throw new Error(`Invalid template literal part: ${part._zod.traits}`);\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n regexParts.push(source.slice(start, end));\n }\n else if (part === null || util.primitiveTypes.has(typeof part)) {\n regexParts.push(util.escapeRegex(`${part}`));\n }\n else {\n throw new Error(`Invalid template literal part: ${part}`);\n }\n }\n inst._zod.pattern = new RegExp(`^${regexParts.join(\"\")}$`);\n inst._zod.parse = (payload, _ctx) => {\n if (typeof payload.value !== \"string\") {\n payload.issues.push({\n input: payload.value,\n inst,\n expected: \"template_literal\",\n code: \"invalid_type\",\n });\n return payload;\n }\n inst._zod.pattern.lastIndex = 0;\n if (!inst._zod.pattern.test(payload.value)) {\n payload.issues.push({\n input: payload.value,\n inst,\n code: \"invalid_format\",\n format: def.format ?? \"template_literal\",\n pattern: inst._zod.pattern.source,\n });\n return payload;\n }\n return payload;\n };\n});\nexport const $ZodFunction = /*@__PURE__*/ core.$constructor(\"$ZodFunction\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._def = def;\n inst._zod.def = def;\n inst.implement = (func) => {\n if (typeof func !== \"function\") {\n throw new Error(\"implement() must be called with a function\");\n }\n return function (...args) {\n const parsedArgs = inst._def.input ? parse(inst._def.input, args) : args;\n const result = Reflect.apply(func, this, parsedArgs);\n if (inst._def.output) {\n return parse(inst._def.output, result);\n }\n return result;\n };\n };\n inst.implementAsync = (func) => {\n if (typeof func !== \"function\") {\n throw new Error(\"implementAsync() must be called with a function\");\n }\n return async function (...args) {\n const parsedArgs = inst._def.input ? await parseAsync(inst._def.input, args) : args;\n const result = await Reflect.apply(func, this, parsedArgs);\n if (inst._def.output) {\n return await parseAsync(inst._def.output, result);\n }\n return result;\n };\n };\n inst._zod.parse = (payload, _ctx) => {\n if (typeof payload.value !== \"function\") {\n payload.issues.push({\n code: \"invalid_type\",\n expected: \"function\",\n input: payload.value,\n inst,\n });\n return payload;\n }\n // Check if output is a promise type to determine if we should use async implementation\n const hasPromiseOutput = inst._def.output && inst._def.output._zod.def.type === \"promise\";\n if (hasPromiseOutput) {\n payload.value = inst.implementAsync(payload.value);\n }\n else {\n payload.value = inst.implement(payload.value);\n }\n return payload;\n };\n inst.input = (...args) => {\n const F = inst.constructor;\n if (Array.isArray(args[0])) {\n return new F({\n type: \"function\",\n input: new $ZodTuple({\n type: \"tuple\",\n items: args[0],\n rest: args[1],\n }),\n output: inst._def.output,\n });\n }\n return new F({\n type: \"function\",\n input: args[0],\n output: inst._def.output,\n });\n };\n inst.output = (output) => {\n const F = inst.constructor;\n return new F({\n type: \"function\",\n input: inst._def.input,\n output,\n });\n };\n return inst;\n});\nexport const $ZodPromise = /*@__PURE__*/ core.$constructor(\"$ZodPromise\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n return Promise.resolve(payload.value).then((inner) => def.innerType._zod.run({ value: inner, issues: [] }, ctx));\n };\n});\nexport const $ZodLazy = /*@__PURE__*/ core.$constructor(\"$ZodLazy\", (inst, def) => {\n $ZodType.init(inst, def);\n // let _innerType!: any;\n // util.defineLazy(def, \"getter\", () => {\n // if (!_innerType) {\n // _innerType = def.getter();\n // }\n // return () => _innerType;\n // });\n util.defineLazy(inst._zod, \"innerType\", () => def.getter());\n util.defineLazy(inst._zod, \"pattern\", () => inst._zod.innerType._zod.pattern);\n util.defineLazy(inst._zod, \"propValues\", () => inst._zod.innerType._zod.propValues);\n util.defineLazy(inst._zod, \"optin\", () => inst._zod.innerType._zod.optin ?? undefined);\n util.defineLazy(inst._zod, \"optout\", () => inst._zod.innerType._zod.optout ?? undefined);\n inst._zod.parse = (payload, ctx) => {\n const inner = inst._zod.innerType;\n return inner._zod.run(payload, ctx);\n };\n});\nexport const $ZodCustom = /*@__PURE__*/ core.$constructor(\"$ZodCustom\", (inst, def) => {\n checks.$ZodCheck.init(inst, def);\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _) => {\n return payload;\n };\n inst._zod.check = (payload) => {\n const input = payload.value;\n const r = def.fn(input);\n if (r instanceof Promise) {\n return r.then((r) => handleRefineResult(r, payload, input, inst));\n }\n handleRefineResult(r, payload, input, inst);\n return;\n };\n});\nfunction handleRefineResult(result, payload, input, inst) {\n if (!result) {\n const _iss = {\n code: \"custom\",\n input,\n inst, // incorporates params.error into issue reporting\n path: [...(inst._zod.def.path ?? [])], // incorporates params.error into issue reporting\n continue: !inst._zod.def.abort,\n // params: inst._zod.def.params,\n };\n if (inst._zod.def.params)\n _iss.params = inst._zod.def.params;\n payload.issues.push(util.issue(_iss));\n }\n}\n","export { default as ar } from \"./ar.js\";\nexport { default as az } from \"./az.js\";\nexport { default as be } from \"./be.js\";\nexport { default as ca } from \"./ca.js\";\nexport { default as cs } from \"./cs.js\";\nexport { default as da } from \"./da.js\";\nexport { default as de } from \"./de.js\";\nexport { default as en } from \"./en.js\";\nexport { default as eo } from \"./eo.js\";\nexport { default as es } from \"./es.js\";\nexport { default as fa } from \"./fa.js\";\nexport { default as fi } from \"./fi.js\";\nexport { default as fr } from \"./fr.js\";\nexport { default as frCA } from \"./fr-CA.js\";\nexport { default as he } from \"./he.js\";\nexport { default as hu } from \"./hu.js\";\nexport { default as id } from \"./id.js\";\nexport { default as is } from \"./is.js\";\nexport { default as it } from \"./it.js\";\nexport { default as ja } from \"./ja.js\";\nexport { default as ka } from \"./ka.js\";\nexport { default as kh } from \"./kh.js\";\nexport { default as km } from \"./km.js\";\nexport { default as ko } from \"./ko.js\";\nexport { default as lt } from \"./lt.js\";\nexport { default as mk } from \"./mk.js\";\nexport { default as ms } from \"./ms.js\";\nexport { default as nl } from \"./nl.js\";\nexport { default as no } from \"./no.js\";\nexport { default as ota } from \"./ota.js\";\nexport { default as ps } from \"./ps.js\";\nexport { default as pl } from \"./pl.js\";\nexport { default as pt } from \"./pt.js\";\nexport { default as ru } from \"./ru.js\";\nexport { default as sl } from \"./sl.js\";\nexport { default as sv } from \"./sv.js\";\nexport { default as ta } from \"./ta.js\";\nexport { default as th } from \"./th.js\";\nexport { default as tr } from \"./tr.js\";\nexport { default as ua } from \"./ua.js\";\nexport { default as uk } from \"./uk.js\";\nexport { default as ur } from \"./ur.js\";\nexport { default as vi } from \"./vi.js\";\nexport { default as zhCN } from \"./zh-CN.js\";\nexport { default as zhTW } from \"./zh-TW.js\";\nexport { default as yo } from \"./yo.js\";\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"حرف\", verb: \"أن يحوي\" },\n file: { unit: \"بايت\", verb: \"أن يحوي\" },\n array: { unit: \"عنصر\", verb: \"أن يحوي\" },\n set: { unit: \"عنصر\", verb: \"أن يحوي\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"مدخل\",\n email: \"بريد إلكتروني\",\n url: \"رابط\",\n emoji: \"إيموجي\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"تاريخ ووقت بمعيار ISO\",\n date: \"تاريخ بمعيار ISO\",\n time: \"وقت بمعيار ISO\",\n duration: \"مدة بمعيار ISO\",\n ipv4: \"عنوان IPv4\",\n ipv6: \"عنوان IPv6\",\n cidrv4: \"مدى عناوين بصيغة IPv4\",\n cidrv6: \"مدى عناوين بصيغة IPv6\",\n base64: \"نَص بترميز base64-encoded\",\n base64url: \"نَص بترميز base64url-encoded\",\n json_string: \"نَص على هيئة JSON\",\n e164: \"رقم هاتف بمعيار E.164\",\n jwt: \"JWT\",\n template_literal: \"مدخل\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `مدخلات غير مقبولة: يفترض إدخال ${issue.expected}، ولكن تم إدخال ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `مدخلات غير مقبولة: يفترض إدخال ${util.stringifyPrimitive(issue.values[0])}`;\n return `اختيار غير مقبول: يتوقع انتقاء أحد هذه الخيارات: ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return ` أكبر من اللازم: يفترض أن تكون ${issue.origin ?? \"القيمة\"} ${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"عنصر\"}`;\n return `أكبر من اللازم: يفترض أن تكون ${issue.origin ?? \"القيمة\"} ${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `أصغر من اللازم: يفترض لـ ${issue.origin} أن يكون ${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `أصغر من اللازم: يفترض لـ ${issue.origin} أن يكون ${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `نَص غير مقبول: يجب أن يبدأ بـ \"${issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `نَص غير مقبول: يجب أن ينتهي بـ \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `نَص غير مقبول: يجب أن يتضمَّن \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `نَص غير مقبول: يجب أن يطابق النمط ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} غير مقبول`;\n }\n case \"not_multiple_of\":\n return `رقم غير مقبول: يجب أن يكون من مضاعفات ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `معرف${issue.keys.length > 1 ? \"ات\" : \"\"} غريب${issue.keys.length > 1 ? \"ة\" : \"\"}: ${util.joinValues(issue.keys, \"، \")}`;\n case \"invalid_key\":\n return `معرف غير مقبول في ${issue.origin}`;\n case \"invalid_union\":\n return \"مدخل غير مقبول\";\n case \"invalid_element\":\n return `مدخل غير مقبول في ${issue.origin}`;\n default:\n return \"مدخل غير مقبول\";\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"simvol\", verb: \"olmalıdır\" },\n file: { unit: \"bayt\", verb: \"olmalıdır\" },\n array: { unit: \"element\", verb: \"olmalıdır\" },\n set: { unit: \"element\", verb: \"olmalıdır\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Yanlış dəyər: gözlənilən ${issue.expected}, daxil olan ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Yanlış dəyər: gözlənilən ${util.stringifyPrimitive(issue.values[0])}`;\n return `Yanlış seçim: aşağıdakılardan biri olmalıdır: ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Çox böyük: gözlənilən ${issue.origin ?? \"dəyər\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"element\"}`;\n return `Çox böyük: gözlənilən ${issue.origin ?? \"dəyər\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Çox kiçik: gözlənilən ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Çox kiçik: gözlənilən ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Yanlış mətn: \"${_issue.prefix}\" ilə başlamalıdır`;\n if (_issue.format === \"ends_with\")\n return `Yanlış mətn: \"${_issue.suffix}\" ilə bitməlidir`;\n if (_issue.format === \"includes\")\n return `Yanlış mətn: \"${_issue.includes}\" daxil olmalıdır`;\n if (_issue.format === \"regex\")\n return `Yanlış mətn: ${_issue.pattern} şablonuna uyğun olmalıdır`;\n return `Yanlış ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Yanlış ədəd: ${issue.divisor} ilə bölünə bilən olmalıdır`;\n case \"unrecognized_keys\":\n return `Tanınmayan açar${issue.keys.length > 1 ? \"lar\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} daxilində yanlış açar`;\n case \"invalid_union\":\n return \"Yanlış dəyər\";\n case \"invalid_element\":\n return `${issue.origin} daxilində yanlış dəyər`;\n default:\n return `Yanlış dəyər`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nfunction getBelarusianPlural(count, one, few, many) {\n const absCount = Math.abs(count);\n const lastDigit = absCount % 10;\n const lastTwoDigits = absCount % 100;\n if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {\n return many;\n }\n if (lastDigit === 1) {\n return one;\n }\n if (lastDigit >= 2 && lastDigit <= 4) {\n return few;\n }\n return many;\n}\nconst error = () => {\n const Sizable = {\n string: {\n unit: {\n one: \"сімвал\",\n few: \"сімвалы\",\n many: \"сімвалаў\",\n },\n verb: \"мець\",\n },\n array: {\n unit: {\n one: \"элемент\",\n few: \"элементы\",\n many: \"элементаў\",\n },\n verb: \"мець\",\n },\n set: {\n unit: {\n one: \"элемент\",\n few: \"элементы\",\n many: \"элементаў\",\n },\n verb: \"мець\",\n },\n file: {\n unit: {\n one: \"байт\",\n few: \"байты\",\n many: \"байтаў\",\n },\n verb: \"мець\",\n },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"лік\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"масіў\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"увод\",\n email: \"email адрас\",\n url: \"URL\",\n emoji: \"эмодзі\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO дата і час\",\n date: \"ISO дата\",\n time: \"ISO час\",\n duration: \"ISO працягласць\",\n ipv4: \"IPv4 адрас\",\n ipv6: \"IPv6 адрас\",\n cidrv4: \"IPv4 дыяпазон\",\n cidrv6: \"IPv6 дыяпазон\",\n base64: \"радок у фармаце base64\",\n base64url: \"радок у фармаце base64url\",\n json_string: \"JSON радок\",\n e164: \"нумар E.164\",\n jwt: \"JWT\",\n template_literal: \"увод\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Няправільны ўвод: чакаўся ${issue.expected}, атрымана ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Няправільны ўвод: чакалася ${util.stringifyPrimitive(issue.values[0])}`;\n return `Няправільны варыянт: чакаўся адзін з ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n const maxValue = Number(issue.maximum);\n const unit = getBelarusianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);\n return `Занадта вялікі: чакалася, што ${issue.origin ?? \"значэнне\"} павінна ${sizing.verb} ${adj}${issue.maximum.toString()} ${unit}`;\n }\n return `Занадта вялікі: чакалася, што ${issue.origin ?? \"значэнне\"} павінна быць ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n const minValue = Number(issue.minimum);\n const unit = getBelarusianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);\n return `Занадта малы: чакалася, што ${issue.origin} павінна ${sizing.verb} ${adj}${issue.minimum.toString()} ${unit}`;\n }\n return `Занадта малы: чакалася, што ${issue.origin} павінна быць ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Няправільны радок: павінен пачынацца з \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Няправільны радок: павінен заканчвацца на \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Няправільны радок: павінен змяшчаць \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Няправільны радок: павінен адпавядаць шаблону ${_issue.pattern}`;\n return `Няправільны ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Няправільны лік: павінен быць кратным ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Нераспазнаны ${issue.keys.length > 1 ? \"ключы\" : \"ключ\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Няправільны ключ у ${issue.origin}`;\n case \"invalid_union\":\n return \"Няправільны ўвод\";\n case \"invalid_element\":\n return `Няправільнае значэнне ў ${issue.origin}`;\n default:\n return `Няправільны ўвод`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caràcters\", verb: \"contenir\" },\n file: { unit: \"bytes\", verb: \"contenir\" },\n array: { unit: \"elements\", verb: \"contenir\" },\n set: { unit: \"elements\", verb: \"contenir\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"entrada\",\n email: \"adreça electrònica\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"data i hora ISO\",\n date: \"data ISO\",\n time: \"hora ISO\",\n duration: \"durada ISO\",\n ipv4: \"adreça IPv4\",\n ipv6: \"adreça IPv6\",\n cidrv4: \"rang IPv4\",\n cidrv6: \"rang IPv6\",\n base64: \"cadena codificada en base64\",\n base64url: \"cadena codificada en base64url\",\n json_string: \"cadena JSON\",\n e164: \"número E.164\",\n jwt: \"JWT\",\n template_literal: \"entrada\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Tipus invàlid: s'esperava ${issue.expected}, s'ha rebut ${parsedType(issue.input)}`;\n // return `Tipus invàlid: s'esperava ${issue.expected}, s'ha rebut ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Valor invàlid: s'esperava ${util.stringifyPrimitive(issue.values[0])}`;\n return `Opció invàlida: s'esperava una de ${util.joinValues(issue.values, \" o \")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"com a màxim\" : \"menys de\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Massa gran: s'esperava que ${issue.origin ?? \"el valor\"} contingués ${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Massa gran: s'esperava que ${issue.origin ?? \"el valor\"} fos ${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"com a mínim\" : \"més de\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Massa petit: s'esperava que ${issue.origin} contingués ${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Massa petit: s'esperava que ${issue.origin} fos ${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Format invàlid: ha de començar amb \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Format invàlid: ha d'acabar amb \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Format invàlid: ha d'incloure \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Format invàlid: ha de coincidir amb el patró ${_issue.pattern}`;\n return `Format invàlid per a ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Número invàlid: ha de ser múltiple de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Clau${issue.keys.length > 1 ? \"s\" : \"\"} no reconeguda${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Clau invàlida a ${issue.origin}`;\n case \"invalid_union\":\n return \"Entrada invàlida\"; // Could also be \"Tipus d'unió invàlid\" but \"Entrada invàlida\" is more general\n case \"invalid_element\":\n return `Element invàlid a ${issue.origin}`;\n default:\n return `Entrada invàlida`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"znaků\", verb: \"mít\" },\n file: { unit: \"bajtů\", verb: \"mít\" },\n array: { unit: \"prvků\", verb: \"mít\" },\n set: { unit: \"prvků\", verb: \"mít\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"číslo\";\n }\n case \"string\": {\n return \"řetězec\";\n }\n case \"boolean\": {\n return \"boolean\";\n }\n case \"bigint\": {\n return \"bigint\";\n }\n case \"function\": {\n return \"funkce\";\n }\n case \"symbol\": {\n return \"symbol\";\n }\n case \"undefined\": {\n return \"undefined\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"pole\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"regulární výraz\",\n email: \"e-mailová adresa\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"datum a čas ve formátu ISO\",\n date: \"datum ve formátu ISO\",\n time: \"čas ve formátu ISO\",\n duration: \"doba trvání ISO\",\n ipv4: \"IPv4 adresa\",\n ipv6: \"IPv6 adresa\",\n cidrv4: \"rozsah IPv4\",\n cidrv6: \"rozsah IPv6\",\n base64: \"řetězec zakódovaný ve formátu base64\",\n base64url: \"řetězec zakódovaný ve formátu base64url\",\n json_string: \"řetězec ve formátu JSON\",\n e164: \"číslo E.164\",\n jwt: \"JWT\",\n template_literal: \"vstup\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Neplatný vstup: očekáváno ${issue.expected}, obdrženo ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Neplatný vstup: očekáváno ${util.stringifyPrimitive(issue.values[0])}`;\n return `Neplatná možnost: očekávána jedna z hodnot ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Hodnota je příliš velká: ${issue.origin ?? \"hodnota\"} musí mít ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"prvků\"}`;\n }\n return `Hodnota je příliš velká: ${issue.origin ?? \"hodnota\"} musí být ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Hodnota je příliš malá: ${issue.origin ?? \"hodnota\"} musí mít ${adj}${issue.minimum.toString()} ${sizing.unit ?? \"prvků\"}`;\n }\n return `Hodnota je příliš malá: ${issue.origin ?? \"hodnota\"} musí být ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Neplatný řetězec: musí začínat na \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Neplatný řetězec: musí končit na \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Neplatný řetězec: musí obsahovat \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Neplatný řetězec: musí odpovídat vzoru ${_issue.pattern}`;\n return `Neplatný formát ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Neplatné číslo: musí být násobkem ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Neznámé klíče: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Neplatný klíč v ${issue.origin}`;\n case \"invalid_union\":\n return \"Neplatný vstup\";\n case \"invalid_element\":\n return `Neplatná hodnota v ${issue.origin}`;\n default:\n return `Neplatný vstup`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"tegn\", verb: \"havde\" },\n file: { unit: \"bytes\", verb: \"havde\" },\n array: { unit: \"elementer\", verb: \"indeholdt\" },\n set: { unit: \"elementer\", verb: \"indeholdt\" },\n };\n const TypeNames = {\n string: \"streng\",\n number: \"tal\",\n boolean: \"boolean\",\n array: \"liste\",\n object: \"objekt\",\n set: \"sæt\",\n file: \"fil\",\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n function getTypeName(type) {\n return TypeNames[type] ?? type;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"tal\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"liste\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n return \"objekt\";\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"e-mailadresse\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO dato- og klokkeslæt\",\n date: \"ISO-dato\",\n time: \"ISO-klokkeslæt\",\n duration: \"ISO-varighed\",\n ipv4: \"IPv4-område\",\n ipv6: \"IPv6-område\",\n cidrv4: \"IPv4-spektrum\",\n cidrv6: \"IPv6-spektrum\",\n base64: \"base64-kodet streng\",\n base64url: \"base64url-kodet streng\",\n json_string: \"JSON-streng\",\n e164: \"E.164-nummer\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ugyldigt input: forventede ${getTypeName(issue.expected)}, fik ${getTypeName(parsedType(issue.input))}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ugyldig værdi: forventede ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ugyldigt valg: forventede en af følgende ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n const origin = getTypeName(issue.origin);\n if (sizing)\n return `For stor: forventede ${origin ?? \"value\"} ${sizing.verb} ${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"elementer\"}`;\n return `For stor: forventede ${origin ?? \"value\"} havde ${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n const origin = getTypeName(issue.origin);\n if (sizing) {\n return `For lille: forventede ${origin} ${sizing.verb} ${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `For lille: forventede ${origin} havde ${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Ugyldig streng: skal starte med \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Ugyldig streng: skal ende med \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ugyldig streng: skal indeholde \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ugyldig streng: skal matche mønsteret ${_issue.pattern}`;\n return `Ugyldig ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ugyldigt tal: skal være deleligt med ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Ukendte nøgler\" : \"Ukendt nøgle\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ugyldig nøgle i ${issue.origin}`;\n case \"invalid_union\":\n return \"Ugyldigt input: matcher ingen af de tilladte typer\";\n case \"invalid_element\":\n return `Ugyldig værdi i ${issue.origin}`;\n default:\n return `Ugyldigt input`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"Zeichen\", verb: \"zu haben\" },\n file: { unit: \"Bytes\", verb: \"zu haben\" },\n array: { unit: \"Elemente\", verb: \"zu haben\" },\n set: { unit: \"Elemente\", verb: \"zu haben\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"Zahl\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"Array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"Eingabe\",\n email: \"E-Mail-Adresse\",\n url: \"URL\",\n emoji: \"Emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO-Datum und -Uhrzeit\",\n date: \"ISO-Datum\",\n time: \"ISO-Uhrzeit\",\n duration: \"ISO-Dauer\",\n ipv4: \"IPv4-Adresse\",\n ipv6: \"IPv6-Adresse\",\n cidrv4: \"IPv4-Bereich\",\n cidrv6: \"IPv6-Bereich\",\n base64: \"Base64-codierter String\",\n base64url: \"Base64-URL-codierter String\",\n json_string: \"JSON-String\",\n e164: \"E.164-Nummer\",\n jwt: \"JWT\",\n template_literal: \"Eingabe\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ungültige Eingabe: erwartet ${issue.expected}, erhalten ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ungültige Eingabe: erwartet ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ungültige Option: erwartet eine von ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Zu groß: erwartet, dass ${issue.origin ?? \"Wert\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"Elemente\"} hat`;\n return `Zu groß: erwartet, dass ${issue.origin ?? \"Wert\"} ${adj}${issue.maximum.toString()} ist`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Zu klein: erwartet, dass ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit} hat`;\n }\n return `Zu klein: erwartet, dass ${issue.origin} ${adj}${issue.minimum.toString()} ist`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Ungültiger String: muss mit \"${_issue.prefix}\" beginnen`;\n if (_issue.format === \"ends_with\")\n return `Ungültiger String: muss mit \"${_issue.suffix}\" enden`;\n if (_issue.format === \"includes\")\n return `Ungültiger String: muss \"${_issue.includes}\" enthalten`;\n if (_issue.format === \"regex\")\n return `Ungültiger String: muss dem Muster ${_issue.pattern} entsprechen`;\n return `Ungültig: ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ungültige Zahl: muss ein Vielfaches von ${issue.divisor} sein`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Unbekannte Schlüssel\" : \"Unbekannter Schlüssel\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ungültiger Schlüssel in ${issue.origin}`;\n case \"invalid_union\":\n return \"Ungültige Eingabe\";\n case \"invalid_element\":\n return `Ungültiger Wert in ${issue.origin}`;\n default:\n return `Ungültige Eingabe`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"characters\", verb: \"to have\" },\n file: { unit: \"bytes\", verb: \"to have\" },\n array: { unit: \"items\", verb: \"to have\" },\n set: { unit: \"items\", verb: \"to have\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Invalid input: expected ${issue.expected}, received ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Invalid input: expected ${util.stringifyPrimitive(issue.values[0])}`;\n return `Invalid option: expected one of ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Too big: expected ${issue.origin ?? \"value\"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Too big: expected ${issue.origin ?? \"value\"} to be ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Invalid string: must start with \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Invalid string: must end with \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Invalid string: must include \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Invalid string: must match pattern ${_issue.pattern}`;\n return `Invalid ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Invalid number: must be a multiple of ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Unrecognized key${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Invalid key in ${issue.origin}`;\n case \"invalid_union\":\n return \"Invalid input\";\n case \"invalid_element\":\n return `Invalid value in ${issue.origin}`;\n default:\n return `Invalid input`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"nombro\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tabelo\";\n }\n if (data === null) {\n return \"senvalora\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"karaktrojn\", verb: \"havi\" },\n file: { unit: \"bajtojn\", verb: \"havi\" },\n array: { unit: \"elementojn\", verb: \"havi\" },\n set: { unit: \"elementojn\", verb: \"havi\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"enigo\",\n email: \"retadreso\",\n url: \"URL\",\n emoji: \"emoĝio\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO-datotempo\",\n date: \"ISO-dato\",\n time: \"ISO-tempo\",\n duration: \"ISO-daŭro\",\n ipv4: \"IPv4-adreso\",\n ipv6: \"IPv6-adreso\",\n cidrv4: \"IPv4-rango\",\n cidrv6: \"IPv6-rango\",\n base64: \"64-ume kodita karaktraro\",\n base64url: \"URL-64-ume kodita karaktraro\",\n json_string: \"JSON-karaktraro\",\n e164: \"E.164-nombro\",\n jwt: \"JWT\",\n template_literal: \"enigo\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Nevalida enigo: atendiĝis ${issue.expected}, riceviĝis ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Nevalida enigo: atendiĝis ${util.stringifyPrimitive(issue.values[0])}`;\n return `Nevalida opcio: atendiĝis unu el ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Tro granda: atendiĝis ke ${issue.origin ?? \"valoro\"} havu ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementojn\"}`;\n return `Tro granda: atendiĝis ke ${issue.origin ?? \"valoro\"} havu ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Tro malgranda: atendiĝis ke ${issue.origin} havu ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Tro malgranda: atendiĝis ke ${issue.origin} estu ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Nevalida karaktraro: devas komenciĝi per \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Nevalida karaktraro: devas finiĝi per \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Nevalida karaktraro: devas inkluzivi \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Nevalida karaktraro: devas kongrui kun la modelo ${_issue.pattern}`;\n return `Nevalida ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Nevalida nombro: devas esti oblo de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Nekonata${issue.keys.length > 1 ? \"j\" : \"\"} ŝlosilo${issue.keys.length > 1 ? \"j\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Nevalida ŝlosilo en ${issue.origin}`;\n case \"invalid_union\":\n return \"Nevalida enigo\";\n case \"invalid_element\":\n return `Nevalida valoro en ${issue.origin}`;\n default:\n return `Nevalida enigo`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caracteres\", verb: \"tener\" },\n file: { unit: \"bytes\", verb: \"tener\" },\n array: { unit: \"elementos\", verb: \"tener\" },\n set: { unit: \"elementos\", verb: \"tener\" },\n };\n const TypeNames = {\n string: \"texto\",\n number: \"número\",\n boolean: \"booleano\",\n array: \"arreglo\",\n object: \"objeto\",\n set: \"conjunto\",\n file: \"archivo\",\n date: \"fecha\",\n bigint: \"número grande\",\n symbol: \"símbolo\",\n undefined: \"indefinido\",\n null: \"nulo\",\n function: \"función\",\n map: \"mapa\",\n record: \"registro\",\n tuple: \"tupla\",\n enum: \"enumeración\",\n union: \"unión\",\n literal: \"literal\",\n promise: \"promesa\",\n void: \"vacío\",\n never: \"nunca\",\n unknown: \"desconocido\",\n any: \"cualquiera\",\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n function getTypeName(type) {\n return TypeNames[type] ?? type;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype) {\n return data.constructor.name;\n }\n return \"object\";\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"entrada\",\n email: \"dirección de correo electrónico\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"fecha y hora ISO\",\n date: \"fecha ISO\",\n time: \"hora ISO\",\n duration: \"duración ISO\",\n ipv4: \"dirección IPv4\",\n ipv6: \"dirección IPv6\",\n cidrv4: \"rango IPv4\",\n cidrv6: \"rango IPv6\",\n base64: \"cadena codificada en base64\",\n base64url: \"URL codificada en base64\",\n json_string: \"cadena JSON\",\n e164: \"número E.164\",\n jwt: \"JWT\",\n template_literal: \"entrada\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Entrada inválida: se esperaba ${getTypeName(issue.expected)}, recibido ${getTypeName(parsedType(issue.input))}`;\n // return `Entrada inválida: se esperaba ${issue.expected}, recibido ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Entrada inválida: se esperaba ${util.stringifyPrimitive(issue.values[0])}`;\n return `Opción inválida: se esperaba una de ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n const origin = getTypeName(issue.origin);\n if (sizing)\n return `Demasiado grande: se esperaba que ${origin ?? \"valor\"} tuviera ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementos\"}`;\n return `Demasiado grande: se esperaba que ${origin ?? \"valor\"} fuera ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n const origin = getTypeName(issue.origin);\n if (sizing) {\n return `Demasiado pequeño: se esperaba que ${origin} tuviera ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Demasiado pequeño: se esperaba que ${origin} fuera ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Cadena inválida: debe comenzar con \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Cadena inválida: debe terminar en \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Cadena inválida: debe incluir \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Cadena inválida: debe coincidir con el patrón ${_issue.pattern}`;\n return `Inválido ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Número inválido: debe ser múltiplo de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Llave${issue.keys.length > 1 ? \"s\" : \"\"} desconocida${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Llave inválida en ${getTypeName(issue.origin)}`;\n case \"invalid_union\":\n return \"Entrada inválida\";\n case \"invalid_element\":\n return `Valor inválido en ${getTypeName(issue.origin)}`;\n default:\n return `Entrada inválida`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"کاراکتر\", verb: \"داشته باشد\" },\n file: { unit: \"بایت\", verb: \"داشته باشد\" },\n array: { unit: \"آیتم\", verb: \"داشته باشد\" },\n set: { unit: \"آیتم\", verb: \"داشته باشد\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"عدد\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"آرایه\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ورودی\",\n email: \"آدرس ایمیل\",\n url: \"URL\",\n emoji: \"ایموجی\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"تاریخ و زمان ایزو\",\n date: \"تاریخ ایزو\",\n time: \"زمان ایزو\",\n duration: \"مدت زمان ایزو\",\n ipv4: \"IPv4 آدرس\",\n ipv6: \"IPv6 آدرس\",\n cidrv4: \"IPv4 دامنه\",\n cidrv6: \"IPv6 دامنه\",\n base64: \"base64-encoded رشته\",\n base64url: \"base64url-encoded رشته\",\n json_string: \"JSON رشته\",\n e164: \"E.164 عدد\",\n jwt: \"JWT\",\n template_literal: \"ورودی\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `ورودی نامعتبر: می‌بایست ${issue.expected} می‌بود، ${parsedType(issue.input)} دریافت شد`;\n case \"invalid_value\":\n if (issue.values.length === 1) {\n return `ورودی نامعتبر: می‌بایست ${util.stringifyPrimitive(issue.values[0])} می‌بود`;\n }\n return `گزینه نامعتبر: می‌بایست یکی از ${util.joinValues(issue.values, \"|\")} می‌بود`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `خیلی بزرگ: ${issue.origin ?? \"مقدار\"} باید ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"عنصر\"} باشد`;\n }\n return `خیلی بزرگ: ${issue.origin ?? \"مقدار\"} باید ${adj}${issue.maximum.toString()} باشد`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `خیلی کوچک: ${issue.origin} باید ${adj}${issue.minimum.toString()} ${sizing.unit} باشد`;\n }\n return `خیلی کوچک: ${issue.origin} باید ${adj}${issue.minimum.toString()} باشد`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `رشته نامعتبر: باید با \"${_issue.prefix}\" شروع شود`;\n }\n if (_issue.format === \"ends_with\") {\n return `رشته نامعتبر: باید با \"${_issue.suffix}\" تمام شود`;\n }\n if (_issue.format === \"includes\") {\n return `رشته نامعتبر: باید شامل \"${_issue.includes}\" باشد`;\n }\n if (_issue.format === \"regex\") {\n return `رشته نامعتبر: باید با الگوی ${_issue.pattern} مطابقت داشته باشد`;\n }\n return `${Nouns[_issue.format] ?? issue.format} نامعتبر`;\n }\n case \"not_multiple_of\":\n return `عدد نامعتبر: باید مضرب ${issue.divisor} باشد`;\n case \"unrecognized_keys\":\n return `کلید${issue.keys.length > 1 ? \"های\" : \"\"} ناشناس: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `کلید ناشناس در ${issue.origin}`;\n case \"invalid_union\":\n return `ورودی نامعتبر`;\n case \"invalid_element\":\n return `مقدار نامعتبر در ${issue.origin}`;\n default:\n return `ورودی نامعتبر`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"merkkiä\", subject: \"merkkijonon\" },\n file: { unit: \"tavua\", subject: \"tiedoston\" },\n array: { unit: \"alkiota\", subject: \"listan\" },\n set: { unit: \"alkiota\", subject: \"joukon\" },\n number: { unit: \"\", subject: \"luvun\" },\n bigint: { unit: \"\", subject: \"suuren kokonaisluvun\" },\n int: { unit: \"\", subject: \"kokonaisluvun\" },\n date: { unit: \"\", subject: \"päivämäärän\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"säännöllinen lauseke\",\n email: \"sähköpostiosoite\",\n url: \"URL-osoite\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO-aikaleima\",\n date: \"ISO-päivämäärä\",\n time: \"ISO-aika\",\n duration: \"ISO-kesto\",\n ipv4: \"IPv4-osoite\",\n ipv6: \"IPv6-osoite\",\n cidrv4: \"IPv4-alue\",\n cidrv6: \"IPv6-alue\",\n base64: \"base64-koodattu merkkijono\",\n base64url: \"base64url-koodattu merkkijono\",\n json_string: \"JSON-merkkijono\",\n e164: \"E.164-luku\",\n jwt: \"JWT\",\n template_literal: \"templaattimerkkijono\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Virheellinen tyyppi: odotettiin ${issue.expected}, oli ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Virheellinen syöte: täytyy olla ${util.stringifyPrimitive(issue.values[0])}`;\n return `Virheellinen valinta: täytyy olla yksi seuraavista: ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Liian suuri: ${sizing.subject} täytyy olla ${adj}${issue.maximum.toString()} ${sizing.unit}`.trim();\n }\n return `Liian suuri: arvon täytyy olla ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Liian pieni: ${sizing.subject} täytyy olla ${adj}${issue.minimum.toString()} ${sizing.unit}`.trim();\n }\n return `Liian pieni: arvon täytyy olla ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Virheellinen syöte: täytyy alkaa \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Virheellinen syöte: täytyy loppua \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Virheellinen syöte: täytyy sisältää \"${_issue.includes}\"`;\n if (_issue.format === \"regex\") {\n return `Virheellinen syöte: täytyy vastata säännöllistä lauseketta ${_issue.pattern}`;\n }\n return `Virheellinen ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Virheellinen luku: täytyy olla luvun ${issue.divisor} monikerta`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Tuntemattomat avaimet\" : \"Tuntematon avain\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return \"Virheellinen avain tietueessa\";\n case \"invalid_union\":\n return \"Virheellinen unioni\";\n case \"invalid_element\":\n return \"Virheellinen arvo joukossa\";\n default:\n return `Virheellinen syöte`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caractères\", verb: \"avoir\" },\n file: { unit: \"octets\", verb: \"avoir\" },\n array: { unit: \"éléments\", verb: \"avoir\" },\n set: { unit: \"éléments\", verb: \"avoir\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"nombre\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tableau\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"entrée\",\n email: \"adresse e-mail\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"date et heure ISO\",\n date: \"date ISO\",\n time: \"heure ISO\",\n duration: \"durée ISO\",\n ipv4: \"adresse IPv4\",\n ipv6: \"adresse IPv6\",\n cidrv4: \"plage IPv4\",\n cidrv6: \"plage IPv6\",\n base64: \"chaîne encodée en base64\",\n base64url: \"chaîne encodée en base64url\",\n json_string: \"chaîne JSON\",\n e164: \"numéro E.164\",\n jwt: \"JWT\",\n template_literal: \"entrée\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Entrée invalide : ${issue.expected} attendu, ${parsedType(issue.input)} reçu`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Entrée invalide : ${util.stringifyPrimitive(issue.values[0])} attendu`;\n return `Option invalide : une valeur parmi ${util.joinValues(issue.values, \"|\")} attendue`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Trop grand : ${issue.origin ?? \"valeur\"} doit ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"élément(s)\"}`;\n return `Trop grand : ${issue.origin ?? \"valeur\"} doit être ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Trop petit : ${issue.origin} doit ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Trop petit : ${issue.origin} doit être ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Chaîne invalide : doit commencer par \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Chaîne invalide : doit se terminer par \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Chaîne invalide : doit inclure \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Chaîne invalide : doit correspondre au modèle ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} invalide`;\n }\n case \"not_multiple_of\":\n return `Nombre invalide : doit être un multiple de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Clé${issue.keys.length > 1 ? \"s\" : \"\"} non reconnue${issue.keys.length > 1 ? \"s\" : \"\"} : ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Clé invalide dans ${issue.origin}`;\n case \"invalid_union\":\n return \"Entrée invalide\";\n case \"invalid_element\":\n return `Valeur invalide dans ${issue.origin}`;\n default:\n return `Entrée invalide`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caractères\", verb: \"avoir\" },\n file: { unit: \"octets\", verb: \"avoir\" },\n array: { unit: \"éléments\", verb: \"avoir\" },\n set: { unit: \"éléments\", verb: \"avoir\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"entrée\",\n email: \"adresse courriel\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"date-heure ISO\",\n date: \"date ISO\",\n time: \"heure ISO\",\n duration: \"durée ISO\",\n ipv4: \"adresse IPv4\",\n ipv6: \"adresse IPv6\",\n cidrv4: \"plage IPv4\",\n cidrv6: \"plage IPv6\",\n base64: \"chaîne encodée en base64\",\n base64url: \"chaîne encodée en base64url\",\n json_string: \"chaîne JSON\",\n e164: \"numéro E.164\",\n jwt: \"JWT\",\n template_literal: \"entrée\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Entrée invalide : attendu ${issue.expected}, reçu ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Entrée invalide : attendu ${util.stringifyPrimitive(issue.values[0])}`;\n return `Option invalide : attendu l'une des valeurs suivantes ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"≤\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Trop grand : attendu que ${issue.origin ?? \"la valeur\"} ait ${adj}${issue.maximum.toString()} ${sizing.unit}`;\n return `Trop grand : attendu que ${issue.origin ?? \"la valeur\"} soit ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"≥\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Trop petit : attendu que ${issue.origin} ait ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Trop petit : attendu que ${issue.origin} soit ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Chaîne invalide : doit commencer par \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Chaîne invalide : doit se terminer par \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Chaîne invalide : doit inclure \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Chaîne invalide : doit correspondre au motif ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} invalide`;\n }\n case \"not_multiple_of\":\n return `Nombre invalide : doit être un multiple de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Clé${issue.keys.length > 1 ? \"s\" : \"\"} non reconnue${issue.keys.length > 1 ? \"s\" : \"\"} : ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Clé invalide dans ${issue.origin}`;\n case \"invalid_union\":\n return \"Entrée invalide\";\n case \"invalid_element\":\n return `Valeur invalide dans ${issue.origin}`;\n default:\n return `Entrée invalide`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"אותיות\", verb: \"לכלול\" },\n file: { unit: \"בייטים\", verb: \"לכלול\" },\n array: { unit: \"פריטים\", verb: \"לכלול\" },\n set: { unit: \"פריטים\", verb: \"לכלול\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"קלט\",\n email: \"כתובת אימייל\",\n url: \"כתובת רשת\",\n emoji: \"אימוג'י\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"תאריך וזמן ISO\",\n date: \"תאריך ISO\",\n time: \"זמן ISO\",\n duration: \"משך זמן ISO\",\n ipv4: \"כתובת IPv4\",\n ipv6: \"כתובת IPv6\",\n cidrv4: \"טווח IPv4\",\n cidrv6: \"טווח IPv6\",\n base64: \"מחרוזת בבסיס 64\",\n base64url: \"מחרוזת בבסיס 64 לכתובות רשת\",\n json_string: \"מחרוזת JSON\",\n e164: \"מספר E.164\",\n jwt: \"JWT\",\n template_literal: \"קלט\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `קלט לא תקין: צריך ${issue.expected}, התקבל ${parsedType(issue.input)}`;\n // return `Invalid input: expected ${issue.expected}, received ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `קלט לא תקין: צריך ${util.stringifyPrimitive(issue.values[0])}`;\n return `קלט לא תקין: צריך אחת מהאפשרויות ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `גדול מדי: ${issue.origin ?? \"value\"} צריך להיות ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `גדול מדי: ${issue.origin ?? \"value\"} צריך להיות ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `קטן מדי: ${issue.origin} צריך להיות ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `קטן מדי: ${issue.origin} צריך להיות ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `מחרוזת לא תקינה: חייבת להתחיל ב\"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `מחרוזת לא תקינה: חייבת להסתיים ב \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `מחרוזת לא תקינה: חייבת לכלול \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `מחרוזת לא תקינה: חייבת להתאים לתבנית ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} לא תקין`;\n }\n case \"not_multiple_of\":\n return `מספר לא תקין: חייב להיות מכפלה של ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `מפתח${issue.keys.length > 1 ? \"ות\" : \"\"} לא מזוה${issue.keys.length > 1 ? \"ים\" : \"ה\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `מפתח לא תקין ב${issue.origin}`;\n case \"invalid_union\":\n return \"קלט לא תקין\";\n case \"invalid_element\":\n return `ערך לא תקין ב${issue.origin}`;\n default:\n return `קלט לא תקין`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"karakter\", verb: \"legyen\" },\n file: { unit: \"byte\", verb: \"legyen\" },\n array: { unit: \"elem\", verb: \"legyen\" },\n set: { unit: \"elem\", verb: \"legyen\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"szám\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tömb\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"bemenet\",\n email: \"email cím\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO időbélyeg\",\n date: \"ISO dátum\",\n time: \"ISO idő\",\n duration: \"ISO időintervallum\",\n ipv4: \"IPv4 cím\",\n ipv6: \"IPv6 cím\",\n cidrv4: \"IPv4 tartomány\",\n cidrv6: \"IPv6 tartomány\",\n base64: \"base64-kódolt string\",\n base64url: \"base64url-kódolt string\",\n json_string: \"JSON string\",\n e164: \"E.164 szám\",\n jwt: \"JWT\",\n template_literal: \"bemenet\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Érvénytelen bemenet: a várt érték ${issue.expected}, a kapott érték ${parsedType(issue.input)}`;\n // return `Invalid input: expected ${issue.expected}, received ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Érvénytelen bemenet: a várt érték ${util.stringifyPrimitive(issue.values[0])}`;\n return `Érvénytelen opció: valamelyik érték várt ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Túl nagy: ${issue.origin ?? \"érték\"} mérete túl nagy ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elem\"}`;\n return `Túl nagy: a bemeneti érték ${issue.origin ?? \"érték\"} túl nagy: ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Túl kicsi: a bemeneti érték ${issue.origin} mérete túl kicsi ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Túl kicsi: a bemeneti érték ${issue.origin} túl kicsi ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Érvénytelen string: \"${_issue.prefix}\" értékkel kell kezdődnie`;\n if (_issue.format === \"ends_with\")\n return `Érvénytelen string: \"${_issue.suffix}\" értékkel kell végződnie`;\n if (_issue.format === \"includes\")\n return `Érvénytelen string: \"${_issue.includes}\" értéket kell tartalmaznia`;\n if (_issue.format === \"regex\")\n return `Érvénytelen string: ${_issue.pattern} mintának kell megfelelnie`;\n return `Érvénytelen ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Érvénytelen szám: ${issue.divisor} többszörösének kell lennie`;\n case \"unrecognized_keys\":\n return `Ismeretlen kulcs${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Érvénytelen kulcs ${issue.origin}`;\n case \"invalid_union\":\n return \"Érvénytelen bemenet\";\n case \"invalid_element\":\n return `Érvénytelen érték: ${issue.origin}`;\n default:\n return `Érvénytelen bemenet`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"karakter\", verb: \"memiliki\" },\n file: { unit: \"byte\", verb: \"memiliki\" },\n array: { unit: \"item\", verb: \"memiliki\" },\n set: { unit: \"item\", verb: \"memiliki\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"alamat email\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"tanggal dan waktu format ISO\",\n date: \"tanggal format ISO\",\n time: \"jam format ISO\",\n duration: \"durasi format ISO\",\n ipv4: \"alamat IPv4\",\n ipv6: \"alamat IPv6\",\n cidrv4: \"rentang alamat IPv4\",\n cidrv6: \"rentang alamat IPv6\",\n base64: \"string dengan enkode base64\",\n base64url: \"string dengan enkode base64url\",\n json_string: \"string JSON\",\n e164: \"angka E.164\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Input tidak valid: diharapkan ${issue.expected}, diterima ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Input tidak valid: diharapkan ${util.stringifyPrimitive(issue.values[0])}`;\n return `Pilihan tidak valid: diharapkan salah satu dari ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Terlalu besar: diharapkan ${issue.origin ?? \"value\"} memiliki ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elemen\"}`;\n return `Terlalu besar: diharapkan ${issue.origin ?? \"value\"} menjadi ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Terlalu kecil: diharapkan ${issue.origin} memiliki ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Terlalu kecil: diharapkan ${issue.origin} menjadi ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `String tidak valid: harus dimulai dengan \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `String tidak valid: harus berakhir dengan \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `String tidak valid: harus menyertakan \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `String tidak valid: harus sesuai pola ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} tidak valid`;\n }\n case \"not_multiple_of\":\n return `Angka tidak valid: harus kelipatan dari ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Kunci tidak dikenali ${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Kunci tidak valid di ${issue.origin}`;\n case \"invalid_union\":\n return \"Input tidak valid\";\n case \"invalid_element\":\n return `Nilai tidak valid di ${issue.origin}`;\n default:\n return `Input tidak valid`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"númer\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"fylki\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"stafi\", verb: \"að hafa\" },\n file: { unit: \"bæti\", verb: \"að hafa\" },\n array: { unit: \"hluti\", verb: \"að hafa\" },\n set: { unit: \"hluti\", verb: \"að hafa\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"gildi\",\n email: \"netfang\",\n url: \"vefslóð\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO dagsetning og tími\",\n date: \"ISO dagsetning\",\n time: \"ISO tími\",\n duration: \"ISO tímalengd\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded strengur\",\n base64url: \"base64url-encoded strengur\",\n json_string: \"JSON strengur\",\n e164: \"E.164 tölugildi\",\n jwt: \"JWT\",\n template_literal: \"gildi\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Rangt gildi: Þú slóst inn ${parsedType(issue.input)} þar sem á að vera ${issue.expected}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Rangt gildi: gert ráð fyrir ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ógilt val: má vera eitt af eftirfarandi ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Of stórt: gert er ráð fyrir að ${issue.origin ?? \"gildi\"} hafi ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"hluti\"}`;\n return `Of stórt: gert er ráð fyrir að ${issue.origin ?? \"gildi\"} sé ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Of lítið: gert er ráð fyrir að ${issue.origin} hafi ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Of lítið: gert er ráð fyrir að ${issue.origin} sé ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Ógildur strengur: verður að byrja á \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Ógildur strengur: verður að enda á \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ógildur strengur: verður að innihalda \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ógildur strengur: verður að fylgja mynstri ${_issue.pattern}`;\n return `Rangt ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Röng tala: verður að vera margfeldi af ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Óþekkt ${issue.keys.length > 1 ? \"ir lyklar\" : \"ur lykill\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Rangur lykill í ${issue.origin}`;\n case \"invalid_union\":\n return \"Rangt gildi\";\n case \"invalid_element\":\n return `Rangt gildi í ${issue.origin}`;\n default:\n return `Rangt gildi`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caratteri\", verb: \"avere\" },\n file: { unit: \"byte\", verb: \"avere\" },\n array: { unit: \"elementi\", verb: \"avere\" },\n set: { unit: \"elementi\", verb: \"avere\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"numero\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"vettore\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"indirizzo email\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"data e ora ISO\",\n date: \"data ISO\",\n time: \"ora ISO\",\n duration: \"durata ISO\",\n ipv4: \"indirizzo IPv4\",\n ipv6: \"indirizzo IPv6\",\n cidrv4: \"intervallo IPv4\",\n cidrv6: \"intervallo IPv6\",\n base64: \"stringa codificata in base64\",\n base64url: \"URL codificata in base64\",\n json_string: \"stringa JSON\",\n e164: \"numero E.164\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Input non valido: atteso ${issue.expected}, ricevuto ${parsedType(issue.input)}`;\n // return `Input non valido: atteso ${issue.expected}, ricevuto ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Input non valido: atteso ${util.stringifyPrimitive(issue.values[0])}`;\n return `Opzione non valida: atteso uno tra ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Troppo grande: ${issue.origin ?? \"valore\"} deve avere ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementi\"}`;\n return `Troppo grande: ${issue.origin ?? \"valore\"} deve essere ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Troppo piccolo: ${issue.origin} deve avere ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Troppo piccolo: ${issue.origin} deve essere ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Stringa non valida: deve iniziare con \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Stringa non valida: deve terminare con \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Stringa non valida: deve includere \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Stringa non valida: deve corrispondere al pattern ${_issue.pattern}`;\n return `Invalid ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Numero non valido: deve essere un multiplo di ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Chiav${issue.keys.length > 1 ? \"i\" : \"e\"} non riconosciut${issue.keys.length > 1 ? \"e\" : \"a\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Chiave non valida in ${issue.origin}`;\n case \"invalid_union\":\n return \"Input non valido\";\n case \"invalid_element\":\n return `Valore non valido in ${issue.origin}`;\n default:\n return `Input non valido`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"文字\", verb: \"である\" },\n file: { unit: \"バイト\", verb: \"である\" },\n array: { unit: \"要素\", verb: \"である\" },\n set: { unit: \"要素\", verb: \"である\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"数値\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"配列\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"入力値\",\n email: \"メールアドレス\",\n url: \"URL\",\n emoji: \"絵文字\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO日時\",\n date: \"ISO日付\",\n time: \"ISO時刻\",\n duration: \"ISO期間\",\n ipv4: \"IPv4アドレス\",\n ipv6: \"IPv6アドレス\",\n cidrv4: \"IPv4範囲\",\n cidrv6: \"IPv6範囲\",\n base64: \"base64エンコード文字列\",\n base64url: \"base64urlエンコード文字列\",\n json_string: \"JSON文字列\",\n e164: \"E.164番号\",\n jwt: \"JWT\",\n template_literal: \"入力値\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `無効な入力: ${issue.expected}が期待されましたが、${parsedType(issue.input)}が入力されました`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `無効な入力: ${util.stringifyPrimitive(issue.values[0])}が期待されました`;\n return `無効な選択: ${util.joinValues(issue.values, \"、\")}のいずれかである必要があります`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"以下である\" : \"より小さい\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `大きすぎる値: ${issue.origin ?? \"値\"}は${issue.maximum.toString()}${sizing.unit ?? \"要素\"}${adj}必要があります`;\n return `大きすぎる値: ${issue.origin ?? \"値\"}は${issue.maximum.toString()}${adj}必要があります`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"以上である\" : \"より大きい\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `小さすぎる値: ${issue.origin}は${issue.minimum.toString()}${sizing.unit}${adj}必要があります`;\n return `小さすぎる値: ${issue.origin}は${issue.minimum.toString()}${adj}必要があります`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `無効な文字列: \"${_issue.prefix}\"で始まる必要があります`;\n if (_issue.format === \"ends_with\")\n return `無効な文字列: \"${_issue.suffix}\"で終わる必要があります`;\n if (_issue.format === \"includes\")\n return `無効な文字列: \"${_issue.includes}\"を含む必要があります`;\n if (_issue.format === \"regex\")\n return `無効な文字列: パターン${_issue.pattern}に一致する必要があります`;\n return `無効な${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `無効な数値: ${issue.divisor}の倍数である必要があります`;\n case \"unrecognized_keys\":\n return `認識されていないキー${issue.keys.length > 1 ? \"群\" : \"\"}: ${util.joinValues(issue.keys, \"、\")}`;\n case \"invalid_key\":\n return `${issue.origin}内の無効なキー`;\n case \"invalid_union\":\n return \"無効な入力\";\n case \"invalid_element\":\n return `${issue.origin}内の無効な値`;\n default:\n return `無効な入力`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"რიცხვი\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"მასივი\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n const typeMap = {\n string: \"სტრინგი\",\n boolean: \"ბულეანი\",\n undefined: \"undefined\",\n bigint: \"bigint\",\n symbol: \"symbol\",\n function: \"ფუნქცია\",\n };\n return typeMap[t] ?? t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"სიმბოლო\", verb: \"უნდა შეიცავდეს\" },\n file: { unit: \"ბაიტი\", verb: \"უნდა შეიცავდეს\" },\n array: { unit: \"ელემენტი\", verb: \"უნდა შეიცავდეს\" },\n set: { unit: \"ელემენტი\", verb: \"უნდა შეიცავდეს\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"შეყვანა\",\n email: \"ელ-ფოსტის მისამართი\",\n url: \"URL\",\n emoji: \"ემოჯი\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"თარიღი-დრო\",\n date: \"თარიღი\",\n time: \"დრო\",\n duration: \"ხანგრძლივობა\",\n ipv4: \"IPv4 მისამართი\",\n ipv6: \"IPv6 მისამართი\",\n cidrv4: \"IPv4 დიაპაზონი\",\n cidrv6: \"IPv6 დიაპაზონი\",\n base64: \"base64-კოდირებული სტრინგი\",\n base64url: \"base64url-კოდირებული სტრინგი\",\n json_string: \"JSON სტრინგი\",\n e164: \"E.164 ნომერი\",\n jwt: \"JWT\",\n template_literal: \"შეყვანა\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `არასწორი შეყვანა: მოსალოდნელი ${issue.expected}, მიღებული ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `არასწორი შეყვანა: მოსალოდნელი ${util.stringifyPrimitive(issue.values[0])}`;\n return `არასწორი ვარიანტი: მოსალოდნელია ერთ-ერთი ${util.joinValues(issue.values, \"|\")}-დან`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `ზედმეტად დიდი: მოსალოდნელი ${issue.origin ?? \"მნიშვნელობა\"} ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit}`;\n return `ზედმეტად დიდი: მოსალოდნელი ${issue.origin ?? \"მნიშვნელობა\"} იყოს ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `ზედმეტად პატარა: მოსალოდნელი ${issue.origin} ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `ზედმეტად პატარა: მოსალოდნელი ${issue.origin} იყოს ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `არასწორი სტრინგი: უნდა იწყებოდეს \"${_issue.prefix}\"-ით`;\n }\n if (_issue.format === \"ends_with\")\n return `არასწორი სტრინგი: უნდა მთავრდებოდეს \"${_issue.suffix}\"-ით`;\n if (_issue.format === \"includes\")\n return `არასწორი სტრინგი: უნდა შეიცავდეს \"${_issue.includes}\"-ს`;\n if (_issue.format === \"regex\")\n return `არასწორი სტრინგი: უნდა შეესაბამებოდეს შაბლონს ${_issue.pattern}`;\n return `არასწორი ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `არასწორი რიცხვი: უნდა იყოს ${issue.divisor}-ის ჯერადი`;\n case \"unrecognized_keys\":\n return `უცნობი გასაღებ${issue.keys.length > 1 ? \"ები\" : \"ი\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `არასწორი გასაღები ${issue.origin}-ში`;\n case \"invalid_union\":\n return \"არასწორი შეყვანა\";\n case \"invalid_element\":\n return `არასწორი მნიშვნელობა ${issue.origin}-ში`;\n default:\n return `არასწორი შეყვანა`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"តួអក្សរ\", verb: \"គួរមាន\" },\n file: { unit: \"បៃ\", verb: \"គួរមាន\" },\n array: { unit: \"ធាតុ\", verb: \"គួរមាន\" },\n set: { unit: \"ធាតុ\", verb: \"គួរមាន\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"មិនមែនជាលេខ (NaN)\" : \"លេខ\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"អារេ (Array)\";\n }\n if (data === null) {\n return \"គ្មានតម្លៃ (null)\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ទិន្នន័យបញ្ចូល\",\n email: \"អាសយដ្ឋានអ៊ីមែល\",\n url: \"URL\",\n emoji: \"សញ្ញាអារម្មណ៍\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"កាលបរិច្ឆេទ និងម៉ោង ISO\",\n date: \"កាលបរិច្ឆេទ ISO\",\n time: \"ម៉ោង ISO\",\n duration: \"រយៈពេល ISO\",\n ipv4: \"អាសយដ្ឋាន IPv4\",\n ipv6: \"អាសយដ្ឋាន IPv6\",\n cidrv4: \"ដែនអាសយដ្ឋាន IPv4\",\n cidrv6: \"ដែនអាសយដ្ឋាន IPv6\",\n base64: \"ខ្សែអក្សរអ៊ិកូដ base64\",\n base64url: \"ខ្សែអក្សរអ៊ិកូដ base64url\",\n json_string: \"ខ្សែអក្សរ JSON\",\n e164: \"លេខ E.164\",\n jwt: \"JWT\",\n template_literal: \"ទិន្នន័យបញ្ចូល\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `ទិន្នន័យបញ្ចូលមិនត្រឹមត្រូវ៖ ត្រូវការ ${issue.expected} ប៉ុន្តែទទួលបាន ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `ទិន្នន័យបញ្ចូលមិនត្រឹមត្រូវ៖ ត្រូវការ ${util.stringifyPrimitive(issue.values[0])}`;\n return `ជម្រើសមិនត្រឹមត្រូវ៖ ត្រូវជាមួយក្នុងចំណោម ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `ធំពេក៖ ត្រូវការ ${issue.origin ?? \"តម្លៃ\"} ${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"ធាតុ\"}`;\n return `ធំពេក៖ ត្រូវការ ${issue.origin ?? \"តម្លៃ\"} ${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `តូចពេក៖ ត្រូវការ ${issue.origin} ${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `តូចពេក៖ ត្រូវការ ${issue.origin} ${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវចាប់ផ្តើមដោយ \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវបញ្ចប់ដោយ \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវមាន \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវតែផ្គូផ្គងនឹងទម្រង់ដែលបានកំណត់ ${_issue.pattern}`;\n return `មិនត្រឹមត្រូវ៖ ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `លេខមិនត្រឹមត្រូវ៖ ត្រូវតែជាពហុគុណនៃ ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `រកឃើញសោមិនស្គាល់៖ ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `សោមិនត្រឹមត្រូវនៅក្នុង ${issue.origin}`;\n case \"invalid_union\":\n return `ទិន្នន័យមិនត្រឹមត្រូវ`;\n case \"invalid_element\":\n return `ទិន្នន័យមិនត្រឹមត្រូវនៅក្នុង ${issue.origin}`;\n default:\n return `ទិន្នន័យមិនត្រឹមត្រូវ`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import km from \"./km.js\";\n/** @deprecated Use `km` instead. */\nexport default function () {\n return km();\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"문자\", verb: \"to have\" },\n file: { unit: \"바이트\", verb: \"to have\" },\n array: { unit: \"개\", verb: \"to have\" },\n set: { unit: \"개\", verb: \"to have\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"입력\",\n email: \"이메일 주소\",\n url: \"URL\",\n emoji: \"이모지\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO 날짜시간\",\n date: \"ISO 날짜\",\n time: \"ISO 시간\",\n duration: \"ISO 기간\",\n ipv4: \"IPv4 주소\",\n ipv6: \"IPv6 주소\",\n cidrv4: \"IPv4 범위\",\n cidrv6: \"IPv6 범위\",\n base64: \"base64 인코딩 문자열\",\n base64url: \"base64url 인코딩 문자열\",\n json_string: \"JSON 문자열\",\n e164: \"E.164 번호\",\n jwt: \"JWT\",\n template_literal: \"입력\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `잘못된 입력: 예상 타입은 ${issue.expected}, 받은 타입은 ${parsedType(issue.input)}입니다`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `잘못된 입력: 값은 ${util.stringifyPrimitive(issue.values[0])} 이어야 합니다`;\n return `잘못된 옵션: ${util.joinValues(issue.values, \"또는 \")} 중 하나여야 합니다`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"이하\" : \"미만\";\n const suffix = adj === \"미만\" ? \"이어야 합니다\" : \"여야 합니다\";\n const sizing = getSizing(issue.origin);\n const unit = sizing?.unit ?? \"요소\";\n if (sizing)\n return `${issue.origin ?? \"값\"}이 너무 큽니다: ${issue.maximum.toString()}${unit} ${adj}${suffix}`;\n return `${issue.origin ?? \"값\"}이 너무 큽니다: ${issue.maximum.toString()} ${adj}${suffix}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"이상\" : \"초과\";\n const suffix = adj === \"이상\" ? \"이어야 합니다\" : \"여야 합니다\";\n const sizing = getSizing(issue.origin);\n const unit = sizing?.unit ?? \"요소\";\n if (sizing) {\n return `${issue.origin ?? \"값\"}이 너무 작습니다: ${issue.minimum.toString()}${unit} ${adj}${suffix}`;\n }\n return `${issue.origin ?? \"값\"}이 너무 작습니다: ${issue.minimum.toString()} ${adj}${suffix}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `잘못된 문자열: \"${_issue.prefix}\"(으)로 시작해야 합니다`;\n }\n if (_issue.format === \"ends_with\")\n return `잘못된 문자열: \"${_issue.suffix}\"(으)로 끝나야 합니다`;\n if (_issue.format === \"includes\")\n return `잘못된 문자열: \"${_issue.includes}\"을(를) 포함해야 합니다`;\n if (_issue.format === \"regex\")\n return `잘못된 문자열: 정규식 ${_issue.pattern} 패턴과 일치해야 합니다`;\n return `잘못된 ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `잘못된 숫자: ${issue.divisor}의 배수여야 합니다`;\n case \"unrecognized_keys\":\n return `인식할 수 없는 키: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `잘못된 키: ${issue.origin}`;\n case \"invalid_union\":\n return `잘못된 입력`;\n case \"invalid_element\":\n return `잘못된 값: ${issue.origin}`;\n default:\n return `잘못된 입력`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n return parsedTypeFromType(t, data);\n};\nconst parsedTypeFromType = (t, data = undefined) => {\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"skaičius\";\n }\n case \"bigint\": {\n return \"sveikasis skaičius\";\n }\n case \"string\": {\n return \"eilutė\";\n }\n case \"boolean\": {\n return \"loginė reikšmė\";\n }\n case \"undefined\":\n case \"void\": {\n return \"neapibrėžta reikšmė\";\n }\n case \"function\": {\n return \"funkcija\";\n }\n case \"symbol\": {\n return \"simbolis\";\n }\n case \"object\": {\n if (data === undefined)\n return \"nežinomas objektas\";\n if (data === null)\n return \"nulinė reikšmė\";\n if (Array.isArray(data))\n return \"masyvas\";\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n return \"objektas\";\n }\n //Zod types below\n case \"null\": {\n return \"nulinė reikšmė\";\n }\n }\n return t;\n};\nconst capitalizeFirstCharacter = (text) => {\n return text.charAt(0).toUpperCase() + text.slice(1);\n};\nfunction getUnitTypeFromNumber(number) {\n const abs = Math.abs(number);\n const last = abs % 10;\n const last2 = abs % 100;\n if ((last2 >= 11 && last2 <= 19) || last === 0)\n return \"many\";\n if (last === 1)\n return \"one\";\n return \"few\";\n}\nconst error = () => {\n const Sizable = {\n string: {\n unit: {\n one: \"simbolis\",\n few: \"simboliai\",\n many: \"simbolių\",\n },\n verb: {\n smaller: {\n inclusive: \"turi būti ne ilgesnė kaip\",\n notInclusive: \"turi būti trumpesnė kaip\",\n },\n bigger: {\n inclusive: \"turi būti ne trumpesnė kaip\",\n notInclusive: \"turi būti ilgesnė kaip\",\n },\n },\n },\n file: {\n unit: {\n one: \"baitas\",\n few: \"baitai\",\n many: \"baitų\",\n },\n verb: {\n smaller: {\n inclusive: \"turi būti ne didesnis kaip\",\n notInclusive: \"turi būti mažesnis kaip\",\n },\n bigger: {\n inclusive: \"turi būti ne mažesnis kaip\",\n notInclusive: \"turi būti didesnis kaip\",\n },\n },\n },\n array: {\n unit: {\n one: \"elementą\",\n few: \"elementus\",\n many: \"elementų\",\n },\n verb: {\n smaller: {\n inclusive: \"turi turėti ne daugiau kaip\",\n notInclusive: \"turi turėti mažiau kaip\",\n },\n bigger: {\n inclusive: \"turi turėti ne mažiau kaip\",\n notInclusive: \"turi turėti daugiau kaip\",\n },\n },\n },\n set: {\n unit: {\n one: \"elementą\",\n few: \"elementus\",\n many: \"elementų\",\n },\n verb: {\n smaller: {\n inclusive: \"turi turėti ne daugiau kaip\",\n notInclusive: \"turi turėti mažiau kaip\",\n },\n bigger: {\n inclusive: \"turi turėti ne mažiau kaip\",\n notInclusive: \"turi turėti daugiau kaip\",\n },\n },\n },\n };\n function getSizing(origin, unitType, inclusive, targetShouldBe) {\n const result = Sizable[origin] ?? null;\n if (result === null)\n return result;\n return {\n unit: result.unit[unitType],\n verb: result.verb[targetShouldBe][inclusive ? \"inclusive\" : \"notInclusive\"],\n };\n }\n const Nouns = {\n regex: \"įvestis\",\n email: \"el. pašto adresas\",\n url: \"URL\",\n emoji: \"jaustukas\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO data ir laikas\",\n date: \"ISO data\",\n time: \"ISO laikas\",\n duration: \"ISO trukmė\",\n ipv4: \"IPv4 adresas\",\n ipv6: \"IPv6 adresas\",\n cidrv4: \"IPv4 tinklo prefiksas (CIDR)\",\n cidrv6: \"IPv6 tinklo prefiksas (CIDR)\",\n base64: \"base64 užkoduota eilutė\",\n base64url: \"base64url užkoduota eilutė\",\n json_string: \"JSON eilutė\",\n e164: \"E.164 numeris\",\n jwt: \"JWT\",\n template_literal: \"įvestis\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Gautas tipas ${parsedType(issue.input)}, o tikėtasi - ${parsedTypeFromType(issue.expected)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Privalo būti ${util.stringifyPrimitive(issue.values[0])}`;\n return `Privalo būti vienas iš ${util.joinValues(issue.values, \"|\")} pasirinkimų`;\n case \"too_big\": {\n const origin = parsedTypeFromType(issue.origin);\n const sizing = getSizing(issue.origin, getUnitTypeFromNumber(Number(issue.maximum)), issue.inclusive ?? false, \"smaller\");\n if (sizing?.verb)\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} ${sizing.verb} ${issue.maximum.toString()} ${sizing.unit ?? \"elementų\"}`;\n const adj = issue.inclusive ? \"ne didesnis kaip\" : \"mažesnis kaip\";\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} turi būti ${adj} ${issue.maximum.toString()} ${sizing?.unit}`;\n }\n case \"too_small\": {\n const origin = parsedTypeFromType(issue.origin);\n const sizing = getSizing(issue.origin, getUnitTypeFromNumber(Number(issue.minimum)), issue.inclusive ?? false, \"bigger\");\n if (sizing?.verb)\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} ${sizing.verb} ${issue.minimum.toString()} ${sizing.unit ?? \"elementų\"}`;\n const adj = issue.inclusive ? \"ne mažesnis kaip\" : \"didesnis kaip\";\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} turi būti ${adj} ${issue.minimum.toString()} ${sizing?.unit}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Eilutė privalo prasidėti \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Eilutė privalo pasibaigti \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Eilutė privalo įtraukti \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Eilutė privalo atitikti ${_issue.pattern}`;\n return `Neteisingas ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Skaičius privalo būti ${issue.divisor} kartotinis.`;\n case \"unrecognized_keys\":\n return `Neatpažint${issue.keys.length > 1 ? \"i\" : \"as\"} rakt${issue.keys.length > 1 ? \"ai\" : \"as\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return \"Rastas klaidingas raktas\";\n case \"invalid_union\":\n return \"Klaidinga įvestis\";\n case \"invalid_element\": {\n const origin = parsedTypeFromType(issue.origin);\n return `${capitalizeFirstCharacter(origin ?? issue.origin ?? \"reikšmė\")} turi klaidingą įvestį`;\n }\n default:\n return \"Klaidinga įvestis\";\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"знаци\", verb: \"да имаат\" },\n file: { unit: \"бајти\", verb: \"да имаат\" },\n array: { unit: \"ставки\", verb: \"да имаат\" },\n set: { unit: \"ставки\", verb: \"да имаат\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"број\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"низа\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"внес\",\n email: \"адреса на е-пошта\",\n url: \"URL\",\n emoji: \"емоџи\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO датум и време\",\n date: \"ISO датум\",\n time: \"ISO време\",\n duration: \"ISO времетраење\",\n ipv4: \"IPv4 адреса\",\n ipv6: \"IPv6 адреса\",\n cidrv4: \"IPv4 опсег\",\n cidrv6: \"IPv6 опсег\",\n base64: \"base64-енкодирана низа\",\n base64url: \"base64url-енкодирана низа\",\n json_string: \"JSON низа\",\n e164: \"E.164 број\",\n jwt: \"JWT\",\n template_literal: \"внес\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Грешен внес: се очекува ${issue.expected}, примено ${parsedType(issue.input)}`;\n // return `Invalid input: expected ${issue.expected}, received ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Invalid input: expected ${util.stringifyPrimitive(issue.values[0])}`;\n return `Грешана опција: се очекува една ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Премногу голем: се очекува ${issue.origin ?? \"вредноста\"} да има ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"елементи\"}`;\n return `Премногу голем: се очекува ${issue.origin ?? \"вредноста\"} да биде ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Премногу мал: се очекува ${issue.origin} да има ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Премногу мал: се очекува ${issue.origin} да биде ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Неважечка низа: мора да започнува со \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Неважечка низа: мора да завршува со \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Неважечка низа: мора да вклучува \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Неважечка низа: мора да одгоара на патернот ${_issue.pattern}`;\n return `Invalid ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Грешен број: мора да биде делив со ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Непрепознаени клучеви\" : \"Непрепознаен клуч\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Грешен клуч во ${issue.origin}`;\n case \"invalid_union\":\n return \"Грешен внес\";\n case \"invalid_element\":\n return `Грешна вредност во ${issue.origin}`;\n default:\n return `Грешен внес`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"aksara\", verb: \"mempunyai\" },\n file: { unit: \"bait\", verb: \"mempunyai\" },\n array: { unit: \"elemen\", verb: \"mempunyai\" },\n set: { unit: \"elemen\", verb: \"mempunyai\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"nombor\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"alamat e-mel\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"tarikh masa ISO\",\n date: \"tarikh ISO\",\n time: \"masa ISO\",\n duration: \"tempoh ISO\",\n ipv4: \"alamat IPv4\",\n ipv6: \"alamat IPv6\",\n cidrv4: \"julat IPv4\",\n cidrv6: \"julat IPv6\",\n base64: \"string dikodkan base64\",\n base64url: \"string dikodkan base64url\",\n json_string: \"string JSON\",\n e164: \"nombor E.164\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Input tidak sah: dijangka ${issue.expected}, diterima ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Input tidak sah: dijangka ${util.stringifyPrimitive(issue.values[0])}`;\n return `Pilihan tidak sah: dijangka salah satu daripada ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Terlalu besar: dijangka ${issue.origin ?? \"nilai\"} ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elemen\"}`;\n return `Terlalu besar: dijangka ${issue.origin ?? \"nilai\"} adalah ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Terlalu kecil: dijangka ${issue.origin} ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Terlalu kecil: dijangka ${issue.origin} adalah ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `String tidak sah: mesti bermula dengan \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `String tidak sah: mesti berakhir dengan \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `String tidak sah: mesti mengandungi \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `String tidak sah: mesti sepadan dengan corak ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} tidak sah`;\n }\n case \"not_multiple_of\":\n return `Nombor tidak sah: perlu gandaan ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Kunci tidak dikenali: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Kunci tidak sah dalam ${issue.origin}`;\n case \"invalid_union\":\n return \"Input tidak sah\";\n case \"invalid_element\":\n return `Nilai tidak sah dalam ${issue.origin}`;\n default:\n return `Input tidak sah`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"tekens\" },\n file: { unit: \"bytes\" },\n array: { unit: \"elementen\" },\n set: { unit: \"elementen\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"getal\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"invoer\",\n email: \"emailadres\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datum en tijd\",\n date: \"ISO datum\",\n time: \"ISO tijd\",\n duration: \"ISO duur\",\n ipv4: \"IPv4-adres\",\n ipv6: \"IPv6-adres\",\n cidrv4: \"IPv4-bereik\",\n cidrv6: \"IPv6-bereik\",\n base64: \"base64-gecodeerde tekst\",\n base64url: \"base64 URL-gecodeerde tekst\",\n json_string: \"JSON string\",\n e164: \"E.164-nummer\",\n jwt: \"JWT\",\n template_literal: \"invoer\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ongeldige invoer: verwacht ${issue.expected}, ontving ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ongeldige invoer: verwacht ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ongeldige optie: verwacht één van ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Te lang: verwacht dat ${issue.origin ?? \"waarde\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementen\"} bevat`;\n return `Te lang: verwacht dat ${issue.origin ?? \"waarde\"} ${adj}${issue.maximum.toString()} is`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Te kort: verwacht dat ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit} bevat`;\n }\n return `Te kort: verwacht dat ${issue.origin} ${adj}${issue.minimum.toString()} is`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Ongeldige tekst: moet met \"${_issue.prefix}\" beginnen`;\n }\n if (_issue.format === \"ends_with\")\n return `Ongeldige tekst: moet op \"${_issue.suffix}\" eindigen`;\n if (_issue.format === \"includes\")\n return `Ongeldige tekst: moet \"${_issue.includes}\" bevatten`;\n if (_issue.format === \"regex\")\n return `Ongeldige tekst: moet overeenkomen met patroon ${_issue.pattern}`;\n return `Ongeldig: ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ongeldig getal: moet een veelvoud van ${issue.divisor} zijn`;\n case \"unrecognized_keys\":\n return `Onbekende key${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ongeldige key in ${issue.origin}`;\n case \"invalid_union\":\n return \"Ongeldige invoer\";\n case \"invalid_element\":\n return `Ongeldige waarde in ${issue.origin}`;\n default:\n return `Ongeldige invoer`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"tegn\", verb: \"å ha\" },\n file: { unit: \"bytes\", verb: \"å ha\" },\n array: { unit: \"elementer\", verb: \"å inneholde\" },\n set: { unit: \"elementer\", verb: \"å inneholde\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"tall\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"liste\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"input\",\n email: \"e-postadresse\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO dato- og klokkeslett\",\n date: \"ISO-dato\",\n time: \"ISO-klokkeslett\",\n duration: \"ISO-varighet\",\n ipv4: \"IPv4-område\",\n ipv6: \"IPv6-område\",\n cidrv4: \"IPv4-spekter\",\n cidrv6: \"IPv6-spekter\",\n base64: \"base64-enkodet streng\",\n base64url: \"base64url-enkodet streng\",\n json_string: \"JSON-streng\",\n e164: \"E.164-nummer\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ugyldig input: forventet ${issue.expected}, fikk ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ugyldig verdi: forventet ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ugyldig valg: forventet en av ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `For stor(t): forventet ${issue.origin ?? \"value\"} til å ha ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementer\"}`;\n return `For stor(t): forventet ${issue.origin ?? \"value\"} til å ha ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `For lite(n): forventet ${issue.origin} til å ha ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `For lite(n): forventet ${issue.origin} til å ha ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Ugyldig streng: må starte med \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Ugyldig streng: må ende med \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ugyldig streng: må inneholde \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ugyldig streng: må matche mønsteret ${_issue.pattern}`;\n return `Ugyldig ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ugyldig tall: må være et multiplum av ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Ukjente nøkler\" : \"Ukjent nøkkel\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ugyldig nøkkel i ${issue.origin}`;\n case \"invalid_union\":\n return \"Ugyldig input\";\n case \"invalid_element\":\n return `Ugyldig verdi i ${issue.origin}`;\n default:\n return `Ugyldig input`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"harf\", verb: \"olmalıdır\" },\n file: { unit: \"bayt\", verb: \"olmalıdır\" },\n array: { unit: \"unsur\", verb: \"olmalıdır\" },\n set: { unit: \"unsur\", verb: \"olmalıdır\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"numara\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"saf\";\n }\n if (data === null) {\n return \"gayb\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"giren\",\n email: \"epostagâh\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO hengâmı\",\n date: \"ISO tarihi\",\n time: \"ISO zamanı\",\n duration: \"ISO müddeti\",\n ipv4: \"IPv4 nişânı\",\n ipv6: \"IPv6 nişânı\",\n cidrv4: \"IPv4 menzili\",\n cidrv6: \"IPv6 menzili\",\n base64: \"base64-şifreli metin\",\n base64url: \"base64url-şifreli metin\",\n json_string: \"JSON metin\",\n e164: \"E.164 sayısı\",\n jwt: \"JWT\",\n template_literal: \"giren\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Fâsit giren: umulan ${issue.expected}, alınan ${parsedType(issue.input)}`;\n // return `Fâsit giren: umulan ${issue.expected}, alınan ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Fâsit giren: umulan ${util.stringifyPrimitive(issue.values[0])}`;\n return `Fâsit tercih: mûteberler ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Fazla büyük: ${issue.origin ?? \"value\"}, ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"} sahip olmalıydı.`;\n return `Fazla büyük: ${issue.origin ?? \"value\"}, ${adj}${issue.maximum.toString()} olmalıydı.`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Fazla küçük: ${issue.origin}, ${adj}${issue.minimum.toString()} ${sizing.unit} sahip olmalıydı.`;\n }\n return `Fazla küçük: ${issue.origin}, ${adj}${issue.minimum.toString()} olmalıydı.`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Fâsit metin: \"${_issue.prefix}\" ile başlamalı.`;\n if (_issue.format === \"ends_with\")\n return `Fâsit metin: \"${_issue.suffix}\" ile bitmeli.`;\n if (_issue.format === \"includes\")\n return `Fâsit metin: \"${_issue.includes}\" ihtivâ etmeli.`;\n if (_issue.format === \"regex\")\n return `Fâsit metin: ${_issue.pattern} nakşına uymalı.`;\n return `Fâsit ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Fâsit sayı: ${issue.divisor} katı olmalıydı.`;\n case \"unrecognized_keys\":\n return `Tanınmayan anahtar ${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} için tanınmayan anahtar var.`;\n case \"invalid_union\":\n return \"Giren tanınamadı.\";\n case \"invalid_element\":\n return `${issue.origin} için tanınmayan kıymet var.`;\n default:\n return `Kıymet tanınamadı.`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"توکي\", verb: \"ولري\" },\n file: { unit: \"بایټس\", verb: \"ولري\" },\n array: { unit: \"توکي\", verb: \"ولري\" },\n set: { unit: \"توکي\", verb: \"ولري\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"عدد\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"ارې\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ورودي\",\n email: \"بریښنالیک\",\n url: \"یو آر ال\",\n emoji: \"ایموجي\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"نیټه او وخت\",\n date: \"نېټه\",\n time: \"وخت\",\n duration: \"موده\",\n ipv4: \"د IPv4 پته\",\n ipv6: \"د IPv6 پته\",\n cidrv4: \"د IPv4 ساحه\",\n cidrv6: \"د IPv6 ساحه\",\n base64: \"base64-encoded متن\",\n base64url: \"base64url-encoded متن\",\n json_string: \"JSON متن\",\n e164: \"د E.164 شمېره\",\n jwt: \"JWT\",\n template_literal: \"ورودي\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `ناسم ورودي: باید ${issue.expected} وای, مګر ${parsedType(issue.input)} ترلاسه شو`;\n case \"invalid_value\":\n if (issue.values.length === 1) {\n return `ناسم ورودي: باید ${util.stringifyPrimitive(issue.values[0])} وای`;\n }\n return `ناسم انتخاب: باید یو له ${util.joinValues(issue.values, \"|\")} څخه وای`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `ډیر لوی: ${issue.origin ?? \"ارزښت\"} باید ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"عنصرونه\"} ولري`;\n }\n return `ډیر لوی: ${issue.origin ?? \"ارزښت\"} باید ${adj}${issue.maximum.toString()} وي`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `ډیر کوچنی: ${issue.origin} باید ${adj}${issue.minimum.toString()} ${sizing.unit} ولري`;\n }\n return `ډیر کوچنی: ${issue.origin} باید ${adj}${issue.minimum.toString()} وي`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `ناسم متن: باید د \"${_issue.prefix}\" سره پیل شي`;\n }\n if (_issue.format === \"ends_with\") {\n return `ناسم متن: باید د \"${_issue.suffix}\" سره پای ته ورسيږي`;\n }\n if (_issue.format === \"includes\") {\n return `ناسم متن: باید \"${_issue.includes}\" ولري`;\n }\n if (_issue.format === \"regex\") {\n return `ناسم متن: باید د ${_issue.pattern} سره مطابقت ولري`;\n }\n return `${Nouns[_issue.format] ?? issue.format} ناسم دی`;\n }\n case \"not_multiple_of\":\n return `ناسم عدد: باید د ${issue.divisor} مضرب وي`;\n case \"unrecognized_keys\":\n return `ناسم ${issue.keys.length > 1 ? \"کلیډونه\" : \"کلیډ\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `ناسم کلیډ په ${issue.origin} کې`;\n case \"invalid_union\":\n return `ناسمه ورودي`;\n case \"invalid_element\":\n return `ناسم عنصر په ${issue.origin} کې`;\n default:\n return `ناسمه ورودي`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"znaków\", verb: \"mieć\" },\n file: { unit: \"bajtów\", verb: \"mieć\" },\n array: { unit: \"elementów\", verb: \"mieć\" },\n set: { unit: \"elementów\", verb: \"mieć\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"liczba\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tablica\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"wyrażenie\",\n email: \"adres email\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"data i godzina w formacie ISO\",\n date: \"data w formacie ISO\",\n time: \"godzina w formacie ISO\",\n duration: \"czas trwania ISO\",\n ipv4: \"adres IPv4\",\n ipv6: \"adres IPv6\",\n cidrv4: \"zakres IPv4\",\n cidrv6: \"zakres IPv6\",\n base64: \"ciąg znaków zakodowany w formacie base64\",\n base64url: \"ciąg znaków zakodowany w formacie base64url\",\n json_string: \"ciąg znaków w formacie JSON\",\n e164: \"liczba E.164\",\n jwt: \"JWT\",\n template_literal: \"wejście\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Nieprawidłowe dane wejściowe: oczekiwano ${issue.expected}, otrzymano ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Nieprawidłowe dane wejściowe: oczekiwano ${util.stringifyPrimitive(issue.values[0])}`;\n return `Nieprawidłowa opcja: oczekiwano jednej z wartości ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Za duża wartość: oczekiwano, że ${issue.origin ?? \"wartość\"} będzie mieć ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementów\"}`;\n }\n return `Zbyt duż(y/a/e): oczekiwano, że ${issue.origin ?? \"wartość\"} będzie wynosić ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Za mała wartość: oczekiwano, że ${issue.origin ?? \"wartość\"} będzie mieć ${adj}${issue.minimum.toString()} ${sizing.unit ?? \"elementów\"}`;\n }\n return `Zbyt mał(y/a/e): oczekiwano, że ${issue.origin ?? \"wartość\"} będzie wynosić ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Nieprawidłowy ciąg znaków: musi zaczynać się od \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Nieprawidłowy ciąg znaków: musi kończyć się na \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Nieprawidłowy ciąg znaków: musi zawierać \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Nieprawidłowy ciąg znaków: musi odpowiadać wzorcowi ${_issue.pattern}`;\n return `Nieprawidłow(y/a/e) ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Nieprawidłowa liczba: musi być wielokrotnością ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Nierozpoznane klucze${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Nieprawidłowy klucz w ${issue.origin}`;\n case \"invalid_union\":\n return \"Nieprawidłowe dane wejściowe\";\n case \"invalid_element\":\n return `Nieprawidłowa wartość w ${issue.origin}`;\n default:\n return `Nieprawidłowe dane wejściowe`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"caracteres\", verb: \"ter\" },\n file: { unit: \"bytes\", verb: \"ter\" },\n array: { unit: \"itens\", verb: \"ter\" },\n set: { unit: \"itens\", verb: \"ter\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"número\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"nulo\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"padrão\",\n email: \"endereço de e-mail\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"data e hora ISO\",\n date: \"data ISO\",\n time: \"hora ISO\",\n duration: \"duração ISO\",\n ipv4: \"endereço IPv4\",\n ipv6: \"endereço IPv6\",\n cidrv4: \"faixa de IPv4\",\n cidrv6: \"faixa de IPv6\",\n base64: \"texto codificado em base64\",\n base64url: \"URL codificada em base64\",\n json_string: \"texto JSON\",\n e164: \"número E.164\",\n jwt: \"JWT\",\n template_literal: \"entrada\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Tipo inválido: esperado ${issue.expected}, recebido ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Entrada inválida: esperado ${util.stringifyPrimitive(issue.values[0])}`;\n return `Opção inválida: esperada uma das ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Muito grande: esperado que ${issue.origin ?? \"valor\"} tivesse ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementos\"}`;\n return `Muito grande: esperado que ${issue.origin ?? \"valor\"} fosse ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Muito pequeno: esperado que ${issue.origin} tivesse ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Muito pequeno: esperado que ${issue.origin} fosse ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Texto inválido: deve começar com \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Texto inválido: deve terminar com \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Texto inválido: deve incluir \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Texto inválido: deve corresponder ao padrão ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} inválido`;\n }\n case \"not_multiple_of\":\n return `Número inválido: deve ser múltiplo de ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Chave${issue.keys.length > 1 ? \"s\" : \"\"} desconhecida${issue.keys.length > 1 ? \"s\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Chave inválida em ${issue.origin}`;\n case \"invalid_union\":\n return \"Entrada inválida\";\n case \"invalid_element\":\n return `Valor inválido em ${issue.origin}`;\n default:\n return `Campo inválido`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nfunction getRussianPlural(count, one, few, many) {\n const absCount = Math.abs(count);\n const lastDigit = absCount % 10;\n const lastTwoDigits = absCount % 100;\n if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {\n return many;\n }\n if (lastDigit === 1) {\n return one;\n }\n if (lastDigit >= 2 && lastDigit <= 4) {\n return few;\n }\n return many;\n}\nconst error = () => {\n const Sizable = {\n string: {\n unit: {\n one: \"символ\",\n few: \"символа\",\n many: \"символов\",\n },\n verb: \"иметь\",\n },\n file: {\n unit: {\n one: \"байт\",\n few: \"байта\",\n many: \"байт\",\n },\n verb: \"иметь\",\n },\n array: {\n unit: {\n one: \"элемент\",\n few: \"элемента\",\n many: \"элементов\",\n },\n verb: \"иметь\",\n },\n set: {\n unit: {\n one: \"элемент\",\n few: \"элемента\",\n many: \"элементов\",\n },\n verb: \"иметь\",\n },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"число\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"массив\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ввод\",\n email: \"email адрес\",\n url: \"URL\",\n emoji: \"эмодзи\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO дата и время\",\n date: \"ISO дата\",\n time: \"ISO время\",\n duration: \"ISO длительность\",\n ipv4: \"IPv4 адрес\",\n ipv6: \"IPv6 адрес\",\n cidrv4: \"IPv4 диапазон\",\n cidrv6: \"IPv6 диапазон\",\n base64: \"строка в формате base64\",\n base64url: \"строка в формате base64url\",\n json_string: \"JSON строка\",\n e164: \"номер E.164\",\n jwt: \"JWT\",\n template_literal: \"ввод\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Неверный ввод: ожидалось ${issue.expected}, получено ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Неверный ввод: ожидалось ${util.stringifyPrimitive(issue.values[0])}`;\n return `Неверный вариант: ожидалось одно из ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n const maxValue = Number(issue.maximum);\n const unit = getRussianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);\n return `Слишком большое значение: ожидалось, что ${issue.origin ?? \"значение\"} будет иметь ${adj}${issue.maximum.toString()} ${unit}`;\n }\n return `Слишком большое значение: ожидалось, что ${issue.origin ?? \"значение\"} будет ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n const minValue = Number(issue.minimum);\n const unit = getRussianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);\n return `Слишком маленькое значение: ожидалось, что ${issue.origin} будет иметь ${adj}${issue.minimum.toString()} ${unit}`;\n }\n return `Слишком маленькое значение: ожидалось, что ${issue.origin} будет ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Неверная строка: должна начинаться с \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Неверная строка: должна заканчиваться на \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Неверная строка: должна содержать \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Неверная строка: должна соответствовать шаблону ${_issue.pattern}`;\n return `Неверный ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Неверное число: должно быть кратным ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Нераспознанн${issue.keys.length > 1 ? \"ые\" : \"ый\"} ключ${issue.keys.length > 1 ? \"и\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Неверный ключ в ${issue.origin}`;\n case \"invalid_union\":\n return \"Неверные входные данные\";\n case \"invalid_element\":\n return `Неверное значение в ${issue.origin}`;\n default:\n return `Неверные входные данные`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"znakov\", verb: \"imeti\" },\n file: { unit: \"bajtov\", verb: \"imeti\" },\n array: { unit: \"elementov\", verb: \"imeti\" },\n set: { unit: \"elementov\", verb: \"imeti\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"število\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"tabela\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"vnos\",\n email: \"e-poštni naslov\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datum in čas\",\n date: \"ISO datum\",\n time: \"ISO čas\",\n duration: \"ISO trajanje\",\n ipv4: \"IPv4 naslov\",\n ipv6: \"IPv6 naslov\",\n cidrv4: \"obseg IPv4\",\n cidrv6: \"obseg IPv6\",\n base64: \"base64 kodiran niz\",\n base64url: \"base64url kodiran niz\",\n json_string: \"JSON niz\",\n e164: \"E.164 številka\",\n jwt: \"JWT\",\n template_literal: \"vnos\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Neveljaven vnos: pričakovano ${issue.expected}, prejeto ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Neveljaven vnos: pričakovano ${util.stringifyPrimitive(issue.values[0])}`;\n return `Neveljavna možnost: pričakovano eno izmed ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Preveliko: pričakovano, da bo ${issue.origin ?? \"vrednost\"} imelo ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elementov\"}`;\n return `Preveliko: pričakovano, da bo ${issue.origin ?? \"vrednost\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Premajhno: pričakovano, da bo ${issue.origin} imelo ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Premajhno: pričakovano, da bo ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Neveljaven niz: mora se začeti z \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Neveljaven niz: mora se končati z \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Neveljaven niz: mora vsebovati \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Neveljaven niz: mora ustrezati vzorcu ${_issue.pattern}`;\n return `Neveljaven ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Neveljavno število: mora biti večkratnik ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Neprepoznan${issue.keys.length > 1 ? \"i ključi\" : \" ključ\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Neveljaven ključ v ${issue.origin}`;\n case \"invalid_union\":\n return \"Neveljaven vnos\";\n case \"invalid_element\":\n return `Neveljavna vrednost v ${issue.origin}`;\n default:\n return \"Neveljaven vnos\";\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"tecken\", verb: \"att ha\" },\n file: { unit: \"bytes\", verb: \"att ha\" },\n array: { unit: \"objekt\", verb: \"att innehålla\" },\n set: { unit: \"objekt\", verb: \"att innehålla\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"antal\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"lista\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"reguljärt uttryck\",\n email: \"e-postadress\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO-datum och tid\",\n date: \"ISO-datum\",\n time: \"ISO-tid\",\n duration: \"ISO-varaktighet\",\n ipv4: \"IPv4-intervall\",\n ipv6: \"IPv6-intervall\",\n cidrv4: \"IPv4-spektrum\",\n cidrv6: \"IPv6-spektrum\",\n base64: \"base64-kodad sträng\",\n base64url: \"base64url-kodad sträng\",\n json_string: \"JSON-sträng\",\n e164: \"E.164-nummer\",\n jwt: \"JWT\",\n template_literal: \"mall-literal\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ogiltig inmatning: förväntat ${issue.expected}, fick ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ogiltig inmatning: förväntat ${util.stringifyPrimitive(issue.values[0])}`;\n return `Ogiltigt val: förväntade en av ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `För stor(t): förväntade ${issue.origin ?? \"värdet\"} att ha ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"element\"}`;\n }\n return `För stor(t): förväntat ${issue.origin ?? \"värdet\"} att ha ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `För lite(t): förväntade ${issue.origin ?? \"värdet\"} att ha ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `För lite(t): förväntade ${issue.origin ?? \"värdet\"} att ha ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `Ogiltig sträng: måste börja med \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `Ogiltig sträng: måste sluta med \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ogiltig sträng: måste innehålla \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ogiltig sträng: måste matcha mönstret \"${_issue.pattern}\"`;\n return `Ogiltig(t) ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Ogiltigt tal: måste vara en multipel av ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `${issue.keys.length > 1 ? \"Okända nycklar\" : \"Okänd nyckel\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Ogiltig nyckel i ${issue.origin ?? \"värdet\"}`;\n case \"invalid_union\":\n return \"Ogiltig input\";\n case \"invalid_element\":\n return `Ogiltigt värde i ${issue.origin ?? \"värdet\"}`;\n default:\n return `Ogiltig input`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"எழுத்துக்கள்\", verb: \"கொண்டிருக்க வேண்டும்\" },\n file: { unit: \"பைட்டுகள்\", verb: \"கொண்டிருக்க வேண்டும்\" },\n array: { unit: \"உறுப்புகள்\", verb: \"கொண்டிருக்க வேண்டும்\" },\n set: { unit: \"உறுப்புகள்\", verb: \"கொண்டிருக்க வேண்டும்\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"எண் அல்லாதது\" : \"எண்\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"அணி\";\n }\n if (data === null) {\n return \"வெறுமை\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"உள்ளீடு\",\n email: \"மின்னஞ்சல் முகவரி\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO தேதி நேரம்\",\n date: \"ISO தேதி\",\n time: \"ISO நேரம்\",\n duration: \"ISO கால அளவு\",\n ipv4: \"IPv4 முகவரி\",\n ipv6: \"IPv6 முகவரி\",\n cidrv4: \"IPv4 வரம்பு\",\n cidrv6: \"IPv6 வரம்பு\",\n base64: \"base64-encoded சரம்\",\n base64url: \"base64url-encoded சரம்\",\n json_string: \"JSON சரம்\",\n e164: \"E.164 எண்\",\n jwt: \"JWT\",\n template_literal: \"input\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `தவறான உள்ளீடு: எதிர்பார்க்கப்பட்டது ${issue.expected}, பெறப்பட்டது ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `தவறான உள்ளீடு: எதிர்பார்க்கப்பட்டது ${util.stringifyPrimitive(issue.values[0])}`;\n return `தவறான விருப்பம்: எதிர்பார்க்கப்பட்டது ${util.joinValues(issue.values, \"|\")} இல் ஒன்று`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `மிக பெரியது: எதிர்பார்க்கப்பட்டது ${issue.origin ?? \"மதிப்பு\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"உறுப்புகள்\"} ஆக இருக்க வேண்டும்`;\n }\n return `மிக பெரியது: எதிர்பார்க்கப்பட்டது ${issue.origin ?? \"மதிப்பு\"} ${adj}${issue.maximum.toString()} ஆக இருக்க வேண்டும்`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `மிகச் சிறியது: எதிர்பார்க்கப்பட்டது ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit} ஆக இருக்க வேண்டும்`; //\n }\n return `மிகச் சிறியது: எதிர்பார்க்கப்பட்டது ${issue.origin} ${adj}${issue.minimum.toString()} ஆக இருக்க வேண்டும்`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `தவறான சரம்: \"${_issue.prefix}\" இல் தொடங்க வேண்டும்`;\n if (_issue.format === \"ends_with\")\n return `தவறான சரம்: \"${_issue.suffix}\" இல் முடிவடைய வேண்டும்`;\n if (_issue.format === \"includes\")\n return `தவறான சரம்: \"${_issue.includes}\" ஐ உள்ளடக்க வேண்டும்`;\n if (_issue.format === \"regex\")\n return `தவறான சரம்: ${_issue.pattern} முறைபாட்டுடன் பொருந்த வேண்டும்`;\n return `தவறான ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `தவறான எண்: ${issue.divisor} இன் பலமாக இருக்க வேண்டும்`;\n case \"unrecognized_keys\":\n return `அடையாளம் தெரியாத விசை${issue.keys.length > 1 ? \"கள்\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} இல் தவறான விசை`;\n case \"invalid_union\":\n return \"தவறான உள்ளீடு\";\n case \"invalid_element\":\n return `${issue.origin} இல் தவறான மதிப்பு`;\n default:\n return `தவறான உள்ளீடு`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"ตัวอักษร\", verb: \"ควรมี\" },\n file: { unit: \"ไบต์\", verb: \"ควรมี\" },\n array: { unit: \"รายการ\", verb: \"ควรมี\" },\n set: { unit: \"รายการ\", verb: \"ควรมี\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"ไม่ใช่ตัวเลข (NaN)\" : \"ตัวเลข\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"อาร์เรย์ (Array)\";\n }\n if (data === null) {\n return \"ไม่มีค่า (null)\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ข้อมูลที่ป้อน\",\n email: \"ที่อยู่อีเมล\",\n url: \"URL\",\n emoji: \"อิโมจิ\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"วันที่เวลาแบบ ISO\",\n date: \"วันที่แบบ ISO\",\n time: \"เวลาแบบ ISO\",\n duration: \"ช่วงเวลาแบบ ISO\",\n ipv4: \"ที่อยู่ IPv4\",\n ipv6: \"ที่อยู่ IPv6\",\n cidrv4: \"ช่วง IP แบบ IPv4\",\n cidrv6: \"ช่วง IP แบบ IPv6\",\n base64: \"ข้อความแบบ Base64\",\n base64url: \"ข้อความแบบ Base64 สำหรับ URL\",\n json_string: \"ข้อความแบบ JSON\",\n e164: \"เบอร์โทรศัพท์ระหว่างประเทศ (E.164)\",\n jwt: \"โทเคน JWT\",\n template_literal: \"ข้อมูลที่ป้อน\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `ประเภทข้อมูลไม่ถูกต้อง: ควรเป็น ${issue.expected} แต่ได้รับ ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `ค่าไม่ถูกต้อง: ควรเป็น ${util.stringifyPrimitive(issue.values[0])}`;\n return `ตัวเลือกไม่ถูกต้อง: ควรเป็นหนึ่งใน ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"ไม่เกิน\" : \"น้อยกว่า\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `เกินกำหนด: ${issue.origin ?? \"ค่า\"} ควรมี${adj} ${issue.maximum.toString()} ${sizing.unit ?? \"รายการ\"}`;\n return `เกินกำหนด: ${issue.origin ?? \"ค่า\"} ควรมี${adj} ${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \"อย่างน้อย\" : \"มากกว่า\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `น้อยกว่ากำหนด: ${issue.origin} ควรมี${adj} ${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `น้อยกว่ากำหนด: ${issue.origin} ควรมี${adj} ${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `รูปแบบไม่ถูกต้อง: ข้อความต้องขึ้นต้นด้วย \"${_issue.prefix}\"`;\n }\n if (_issue.format === \"ends_with\")\n return `รูปแบบไม่ถูกต้อง: ข้อความต้องลงท้ายด้วย \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `รูปแบบไม่ถูกต้อง: ข้อความต้องมี \"${_issue.includes}\" อยู่ในข้อความ`;\n if (_issue.format === \"regex\")\n return `รูปแบบไม่ถูกต้อง: ต้องตรงกับรูปแบบที่กำหนด ${_issue.pattern}`;\n return `รูปแบบไม่ถูกต้อง: ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `ตัวเลขไม่ถูกต้อง: ต้องเป็นจำนวนที่หารด้วย ${issue.divisor} ได้ลงตัว`;\n case \"unrecognized_keys\":\n return `พบคีย์ที่ไม่รู้จัก: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `คีย์ไม่ถูกต้องใน ${issue.origin}`;\n case \"invalid_union\":\n return \"ข้อมูลไม่ถูกต้อง: ไม่ตรงกับรูปแบบยูเนียนที่กำหนดไว้\";\n case \"invalid_element\":\n return `ข้อมูลไม่ถูกต้องใน ${issue.origin}`;\n default:\n return `ข้อมูลไม่ถูกต้อง`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nexport const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n};\nconst error = () => {\n const Sizable = {\n string: { unit: \"karakter\", verb: \"olmalı\" },\n file: { unit: \"bayt\", verb: \"olmalı\" },\n array: { unit: \"öğe\", verb: \"olmalı\" },\n set: { unit: \"öğe\", verb: \"olmalı\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const Nouns = {\n regex: \"girdi\",\n email: \"e-posta adresi\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO tarih ve saat\",\n date: \"ISO tarih\",\n time: \"ISO saat\",\n duration: \"ISO süre\",\n ipv4: \"IPv4 adresi\",\n ipv6: \"IPv6 adresi\",\n cidrv4: \"IPv4 aralığı\",\n cidrv6: \"IPv6 aralığı\",\n base64: \"base64 ile şifrelenmiş metin\",\n base64url: \"base64url ile şifrelenmiş metin\",\n json_string: \"JSON dizesi\",\n e164: \"E.164 sayısı\",\n jwt: \"JWT\",\n template_literal: \"Şablon dizesi\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Geçersiz değer: beklenen ${issue.expected}, alınan ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Geçersiz değer: beklenen ${util.stringifyPrimitive(issue.values[0])}`;\n return `Geçersiz seçenek: aşağıdakilerden biri olmalı: ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Çok büyük: beklenen ${issue.origin ?? \"değer\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"öğe\"}`;\n return `Çok büyük: beklenen ${issue.origin ?? \"değer\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Çok küçük: beklenen ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Çok küçük: beklenen ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Geçersiz metin: \"${_issue.prefix}\" ile başlamalı`;\n if (_issue.format === \"ends_with\")\n return `Geçersiz metin: \"${_issue.suffix}\" ile bitmeli`;\n if (_issue.format === \"includes\")\n return `Geçersiz metin: \"${_issue.includes}\" içermeli`;\n if (_issue.format === \"regex\")\n return `Geçersiz metin: ${_issue.pattern} desenine uymalı`;\n return `Geçersiz ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Geçersiz sayı: ${issue.divisor} ile tam bölünebilmeli`;\n case \"unrecognized_keys\":\n return `Tanınmayan anahtar${issue.keys.length > 1 ? \"lar\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} içinde geçersiz anahtar`;\n case \"invalid_union\":\n return \"Geçersiz değer\";\n case \"invalid_element\":\n return `${issue.origin} içinde geçersiz değer`;\n default:\n return `Geçersiz değer`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"символів\", verb: \"матиме\" },\n file: { unit: \"байтів\", verb: \"матиме\" },\n array: { unit: \"елементів\", verb: \"матиме\" },\n set: { unit: \"елементів\", verb: \"матиме\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"число\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"масив\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"вхідні дані\",\n email: \"адреса електронної пошти\",\n url: \"URL\",\n emoji: \"емодзі\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"дата та час ISO\",\n date: \"дата ISO\",\n time: \"час ISO\",\n duration: \"тривалість ISO\",\n ipv4: \"адреса IPv4\",\n ipv6: \"адреса IPv6\",\n cidrv4: \"діапазон IPv4\",\n cidrv6: \"діапазон IPv6\",\n base64: \"рядок у кодуванні base64\",\n base64url: \"рядок у кодуванні base64url\",\n json_string: \"рядок JSON\",\n e164: \"номер E.164\",\n jwt: \"JWT\",\n template_literal: \"вхідні дані\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Неправильні вхідні дані: очікується ${issue.expected}, отримано ${parsedType(issue.input)}`;\n // return `Неправильні вхідні дані: очікується ${issue.expected}, отримано ${util.getParsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Неправильні вхідні дані: очікується ${util.stringifyPrimitive(issue.values[0])}`;\n return `Неправильна опція: очікується одне з ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Занадто велике: очікується, що ${issue.origin ?? \"значення\"} ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"елементів\"}`;\n return `Занадто велике: очікується, що ${issue.origin ?? \"значення\"} буде ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Занадто мале: очікується, що ${issue.origin} ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Занадто мале: очікується, що ${issue.origin} буде ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Неправильний рядок: повинен починатися з \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Неправильний рядок: повинен закінчуватися на \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Неправильний рядок: повинен містити \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Неправильний рядок: повинен відповідати шаблону ${_issue.pattern}`;\n return `Неправильний ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Неправильне число: повинно бути кратним ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Нерозпізнаний ключ${issue.keys.length > 1 ? \"і\" : \"\"}: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Неправильний ключ у ${issue.origin}`;\n case \"invalid_union\":\n return \"Неправильні вхідні дані\";\n case \"invalid_element\":\n return `Неправильне значення у ${issue.origin}`;\n default:\n return `Неправильні вхідні дані`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import uk from \"./uk.js\";\n/** @deprecated Use `uk` instead. */\nexport default function () {\n return uk();\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"حروف\", verb: \"ہونا\" },\n file: { unit: \"بائٹس\", verb: \"ہونا\" },\n array: { unit: \"آئٹمز\", verb: \"ہونا\" },\n set: { unit: \"آئٹمز\", verb: \"ہونا\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"نمبر\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"آرے\";\n }\n if (data === null) {\n return \"نل\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ان پٹ\",\n email: \"ای میل ایڈریس\",\n url: \"یو آر ایل\",\n emoji: \"ایموجی\",\n uuid: \"یو یو آئی ڈی\",\n uuidv4: \"یو یو آئی ڈی وی 4\",\n uuidv6: \"یو یو آئی ڈی وی 6\",\n nanoid: \"نینو آئی ڈی\",\n guid: \"جی یو آئی ڈی\",\n cuid: \"سی یو آئی ڈی\",\n cuid2: \"سی یو آئی ڈی 2\",\n ulid: \"یو ایل آئی ڈی\",\n xid: \"ایکس آئی ڈی\",\n ksuid: \"کے ایس یو آئی ڈی\",\n datetime: \"آئی ایس او ڈیٹ ٹائم\",\n date: \"آئی ایس او تاریخ\",\n time: \"آئی ایس او وقت\",\n duration: \"آئی ایس او مدت\",\n ipv4: \"آئی پی وی 4 ایڈریس\",\n ipv6: \"آئی پی وی 6 ایڈریس\",\n cidrv4: \"آئی پی وی 4 رینج\",\n cidrv6: \"آئی پی وی 6 رینج\",\n base64: \"بیس 64 ان کوڈڈ سٹرنگ\",\n base64url: \"بیس 64 یو آر ایل ان کوڈڈ سٹرنگ\",\n json_string: \"جے ایس او این سٹرنگ\",\n e164: \"ای 164 نمبر\",\n jwt: \"جے ڈبلیو ٹی\",\n template_literal: \"ان پٹ\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `غلط ان پٹ: ${issue.expected} متوقع تھا، ${parsedType(issue.input)} موصول ہوا`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `غلط ان پٹ: ${util.stringifyPrimitive(issue.values[0])} متوقع تھا`;\n return `غلط آپشن: ${util.joinValues(issue.values, \"|\")} میں سے ایک متوقع تھا`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `بہت بڑا: ${issue.origin ?? \"ویلیو\"} کے ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"عناصر\"} ہونے متوقع تھے`;\n return `بہت بڑا: ${issue.origin ?? \"ویلیو\"} کا ${adj}${issue.maximum.toString()} ہونا متوقع تھا`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `بہت چھوٹا: ${issue.origin} کے ${adj}${issue.minimum.toString()} ${sizing.unit} ہونے متوقع تھے`;\n }\n return `بہت چھوٹا: ${issue.origin} کا ${adj}${issue.minimum.toString()} ہونا متوقع تھا`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `غلط سٹرنگ: \"${_issue.prefix}\" سے شروع ہونا چاہیے`;\n }\n if (_issue.format === \"ends_with\")\n return `غلط سٹرنگ: \"${_issue.suffix}\" پر ختم ہونا چاہیے`;\n if (_issue.format === \"includes\")\n return `غلط سٹرنگ: \"${_issue.includes}\" شامل ہونا چاہیے`;\n if (_issue.format === \"regex\")\n return `غلط سٹرنگ: پیٹرن ${_issue.pattern} سے میچ ہونا چاہیے`;\n return `غلط ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `غلط نمبر: ${issue.divisor} کا مضاعف ہونا چاہیے`;\n case \"unrecognized_keys\":\n return `غیر تسلیم شدہ کی${issue.keys.length > 1 ? \"ز\" : \"\"}: ${util.joinValues(issue.keys, \"، \")}`;\n case \"invalid_key\":\n return `${issue.origin} میں غلط کی`;\n case \"invalid_union\":\n return \"غلط ان پٹ\";\n case \"invalid_element\":\n return `${issue.origin} میں غلط ویلیو`;\n default:\n return `غلط ان پٹ`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"ký tự\", verb: \"có\" },\n file: { unit: \"byte\", verb: \"có\" },\n array: { unit: \"phần tử\", verb: \"có\" },\n set: { unit: \"phần tử\", verb: \"có\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"số\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"mảng\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"đầu vào\",\n email: \"địa chỉ email\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ngày giờ ISO\",\n date: \"ngày ISO\",\n time: \"giờ ISO\",\n duration: \"khoảng thời gian ISO\",\n ipv4: \"địa chỉ IPv4\",\n ipv6: \"địa chỉ IPv6\",\n cidrv4: \"dải IPv4\",\n cidrv6: \"dải IPv6\",\n base64: \"chuỗi mã hóa base64\",\n base64url: \"chuỗi mã hóa base64url\",\n json_string: \"chuỗi JSON\",\n e164: \"số E.164\",\n jwt: \"JWT\",\n template_literal: \"đầu vào\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Đầu vào không hợp lệ: mong đợi ${issue.expected}, nhận được ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Đầu vào không hợp lệ: mong đợi ${util.stringifyPrimitive(issue.values[0])}`;\n return `Tùy chọn không hợp lệ: mong đợi một trong các giá trị ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Quá lớn: mong đợi ${issue.origin ?? \"giá trị\"} ${sizing.verb} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"phần tử\"}`;\n return `Quá lớn: mong đợi ${issue.origin ?? \"giá trị\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `Quá nhỏ: mong đợi ${issue.origin} ${sizing.verb} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `Quá nhỏ: mong đợi ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Chuỗi không hợp lệ: phải bắt đầu bằng \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Chuỗi không hợp lệ: phải kết thúc bằng \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Chuỗi không hợp lệ: phải bao gồm \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Chuỗi không hợp lệ: phải khớp với mẫu ${_issue.pattern}`;\n return `${Nouns[_issue.format] ?? issue.format} không hợp lệ`;\n }\n case \"not_multiple_of\":\n return `Số không hợp lệ: phải là bội số của ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Khóa không được nhận dạng: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Khóa không hợp lệ trong ${issue.origin}`;\n case \"invalid_union\":\n return \"Đầu vào không hợp lệ\";\n case \"invalid_element\":\n return `Giá trị không hợp lệ trong ${issue.origin}`;\n default:\n return `Đầu vào không hợp lệ`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"字符\", verb: \"包含\" },\n file: { unit: \"字节\", verb: \"包含\" },\n array: { unit: \"项\", verb: \"包含\" },\n set: { unit: \"项\", verb: \"包含\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"非数字(NaN)\" : \"数字\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"数组\";\n }\n if (data === null) {\n return \"空值(null)\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"输入\",\n email: \"电子邮件\",\n url: \"URL\",\n emoji: \"表情符号\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO日期时间\",\n date: \"ISO日期\",\n time: \"ISO时间\",\n duration: \"ISO时长\",\n ipv4: \"IPv4地址\",\n ipv6: \"IPv6地址\",\n cidrv4: \"IPv4网段\",\n cidrv6: \"IPv6网段\",\n base64: \"base64编码字符串\",\n base64url: \"base64url编码字符串\",\n json_string: \"JSON字符串\",\n e164: \"E.164号码\",\n jwt: \"JWT\",\n template_literal: \"输入\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `无效输入:期望 ${issue.expected},实际接收 ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `无效输入:期望 ${util.stringifyPrimitive(issue.values[0])}`;\n return `无效选项:期望以下之一 ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `数值过大:期望 ${issue.origin ?? \"值\"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"个元素\"}`;\n return `数值过大:期望 ${issue.origin ?? \"值\"} ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `数值过小:期望 ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `数值过小:期望 ${issue.origin} ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `无效字符串:必须以 \"${_issue.prefix}\" 开头`;\n if (_issue.format === \"ends_with\")\n return `无效字符串:必须以 \"${_issue.suffix}\" 结尾`;\n if (_issue.format === \"includes\")\n return `无效字符串:必须包含 \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `无效字符串:必须满足正则表达式 ${_issue.pattern}`;\n return `无效${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `无效数字:必须是 ${issue.divisor} 的倍数`;\n case \"unrecognized_keys\":\n return `出现未知的键(key): ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `${issue.origin} 中的键(key)无效`;\n case \"invalid_union\":\n return \"无效输入\";\n case \"invalid_element\":\n return `${issue.origin} 中包含无效值(value)`;\n default:\n return `无效输入`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"字元\", verb: \"擁有\" },\n file: { unit: \"位元組\", verb: \"擁有\" },\n array: { unit: \"項目\", verb: \"擁有\" },\n set: { unit: \"項目\", verb: \"擁有\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"number\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"array\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"輸入\",\n email: \"郵件地址\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO 日期時間\",\n date: \"ISO 日期\",\n time: \"ISO 時間\",\n duration: \"ISO 期間\",\n ipv4: \"IPv4 位址\",\n ipv6: \"IPv6 位址\",\n cidrv4: \"IPv4 範圍\",\n cidrv6: \"IPv6 範圍\",\n base64: \"base64 編碼字串\",\n base64url: \"base64url 編碼字串\",\n json_string: \"JSON 字串\",\n e164: \"E.164 數值\",\n jwt: \"JWT\",\n template_literal: \"輸入\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `無效的輸入值:預期為 ${issue.expected},但收到 ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `無效的輸入值:預期為 ${util.stringifyPrimitive(issue.values[0])}`;\n return `無效的選項:預期為以下其中之一 ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `數值過大:預期 ${issue.origin ?? \"值\"} 應為 ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"個元素\"}`;\n return `數值過大:預期 ${issue.origin ?? \"值\"} 應為 ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) {\n return `數值過小:預期 ${issue.origin} 應為 ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n }\n return `數值過小:預期 ${issue.origin} 應為 ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") {\n return `無效的字串:必須以 \"${_issue.prefix}\" 開頭`;\n }\n if (_issue.format === \"ends_with\")\n return `無效的字串:必須以 \"${_issue.suffix}\" 結尾`;\n if (_issue.format === \"includes\")\n return `無效的字串:必須包含 \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `無效的字串:必須符合格式 ${_issue.pattern}`;\n return `無效的 ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `無效的數字:必須為 ${issue.divisor} 的倍數`;\n case \"unrecognized_keys\":\n return `無法識別的鍵值${issue.keys.length > 1 ? \"們\" : \"\"}:${util.joinValues(issue.keys, \"、\")}`;\n case \"invalid_key\":\n return `${issue.origin} 中有無效的鍵值`;\n case \"invalid_union\":\n return \"無效的輸入值\";\n case \"invalid_element\":\n return `${issue.origin} 中有無效的值`;\n default:\n return `無效的輸入值`;\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","import * as util from \"../core/util.js\";\nconst error = () => {\n const Sizable = {\n string: { unit: \"àmi\", verb: \"ní\" },\n file: { unit: \"bytes\", verb: \"ní\" },\n array: { unit: \"nkan\", verb: \"ní\" },\n set: { unit: \"nkan\", verb: \"ní\" },\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const parsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"number\": {\n return Number.isNaN(data) ? \"NaN\" : \"nọ́mbà\";\n }\n case \"object\": {\n if (Array.isArray(data)) {\n return \"akopọ\";\n }\n if (data === null) {\n return \"null\";\n }\n if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {\n return data.constructor.name;\n }\n }\n }\n return t;\n };\n const Nouns = {\n regex: \"ẹ̀rọ ìbáwọlé\",\n email: \"àdírẹ́sì ìmẹ́lì\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"àkókò ISO\",\n date: \"ọjọ́ ISO\",\n time: \"àkókò ISO\",\n duration: \"àkókò tó pé ISO\",\n ipv4: \"àdírẹ́sì IPv4\",\n ipv6: \"àdírẹ́sì IPv6\",\n cidrv4: \"àgbègbè IPv4\",\n cidrv6: \"àgbègbè IPv6\",\n base64: \"ọ̀rọ̀ tí a kọ́ ní base64\",\n base64url: \"ọ̀rọ̀ base64url\",\n json_string: \"ọ̀rọ̀ JSON\",\n e164: \"nọ́mbà E.164\",\n jwt: \"JWT\",\n template_literal: \"ẹ̀rọ ìbáwọlé\",\n };\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\":\n return `Ìbáwọlé aṣìṣe: a ní láti fi ${issue.expected}, àmọ̀ a rí ${parsedType(issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1)\n return `Ìbáwọlé aṣìṣe: a ní láti fi ${util.stringifyPrimitive(issue.values[0])}`;\n return `Àṣàyàn aṣìṣe: yan ọ̀kan lára ${util.joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Tó pọ̀ jù: a ní láti jẹ́ pé ${issue.origin ?? \"iye\"} ${sizing.verb} ${adj}${issue.maximum} ${sizing.unit}`;\n return `Tó pọ̀ jù: a ní láti jẹ́ ${adj}${issue.maximum}`;\n }\n case \"too_small\": {\n const adj = issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing)\n return `Kéré ju: a ní láti jẹ́ pé ${issue.origin} ${sizing.verb} ${adj}${issue.minimum} ${sizing.unit}`;\n return `Kéré ju: a ní láti jẹ́ ${adj}${issue.minimum}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\")\n return `Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ bẹ̀rẹ̀ pẹ̀lú \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\")\n return `Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ parí pẹ̀lú \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\")\n return `Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ ní \"${_issue.includes}\"`;\n if (_issue.format === \"regex\")\n return `Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ bá àpẹẹrẹ mu ${_issue.pattern}`;\n return `Aṣìṣe: ${Nouns[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\":\n return `Nọ́mbà aṣìṣe: gbọ́dọ̀ jẹ́ èyà pípín ti ${issue.divisor}`;\n case \"unrecognized_keys\":\n return `Bọtìnì àìmọ̀: ${util.joinValues(issue.keys, \", \")}`;\n case \"invalid_key\":\n return `Bọtìnì aṣìṣe nínú ${issue.origin}`;\n case \"invalid_union\":\n return \"Ìbáwọlé aṣìṣe\";\n case \"invalid_element\":\n return `Iye aṣìṣe nínú ${issue.origin}`;\n default:\n return \"Ìbáwọlé aṣìṣe\";\n }\n };\n};\nexport default function () {\n return {\n localeError: error(),\n };\n}\n","export const $output = Symbol(\"ZodOutput\");\nexport const $input = Symbol(\"ZodInput\");\nexport class $ZodRegistry {\n constructor() {\n this._map = new WeakMap();\n this._idmap = new Map();\n }\n add(schema, ..._meta) {\n const meta = _meta[0];\n this._map.set(schema, meta);\n if (meta && typeof meta === \"object\" && \"id\" in meta) {\n if (this._idmap.has(meta.id)) {\n throw new Error(`ID ${meta.id} already exists in the registry`);\n }\n this._idmap.set(meta.id, schema);\n }\n return this;\n }\n clear() {\n this._map = new WeakMap();\n this._idmap = new Map();\n return this;\n }\n remove(schema) {\n const meta = this._map.get(schema);\n if (meta && typeof meta === \"object\" && \"id\" in meta) {\n this._idmap.delete(meta.id);\n }\n this._map.delete(schema);\n return this;\n }\n get(schema) {\n // return this._map.get(schema) as any;\n // inherit metadata\n const p = schema._zod.parent;\n if (p) {\n const pm = { ...(this.get(p) ?? {}) };\n delete pm.id; // do not inherit id\n const f = { ...pm, ...this._map.get(schema) };\n return Object.keys(f).length ? f : undefined;\n }\n return this._map.get(schema);\n }\n has(schema) {\n return this._map.has(schema);\n }\n}\n// registries\nexport function registry() {\n return new $ZodRegistry();\n}\nexport const globalRegistry = /*@__PURE__*/ registry();\n","import * as checks from \"./checks.js\";\nimport * as schemas from \"./schemas.js\";\nimport * as util from \"./util.js\";\nexport function _string(Class, params) {\n return new Class({\n type: \"string\",\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedString(Class, params) {\n return new Class({\n type: \"string\",\n coerce: true,\n ...util.normalizeParams(params),\n });\n}\nexport function _email(Class, params) {\n return new Class({\n type: \"string\",\n format: \"email\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _guid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"guid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _uuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _uuidv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v4\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uuidv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v6\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uuidv7(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v7\",\n ...util.normalizeParams(params),\n });\n}\nexport function _url(Class, params) {\n return new Class({\n type: \"string\",\n format: \"url\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _emoji(Class, params) {\n return new Class({\n type: \"string\",\n format: \"emoji\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _nanoid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"nanoid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _cuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cuid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _cuid2(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cuid2\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _ulid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ulid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _xid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"xid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _ksuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ksuid\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _ipv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ipv4\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _ipv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ipv6\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _cidrv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cidrv4\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _cidrv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cidrv6\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _base64(Class, params) {\n return new Class({\n type: \"string\",\n format: \"base64\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _base64url(Class, params) {\n return new Class({\n type: \"string\",\n format: \"base64url\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _e164(Class, params) {\n return new Class({\n type: \"string\",\n format: \"e164\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport function _jwt(Class, params) {\n return new Class({\n type: \"string\",\n format: \"jwt\",\n check: \"string_format\",\n abort: false,\n ...util.normalizeParams(params),\n });\n}\nexport const TimePrecision = {\n Any: null,\n Minute: -1,\n Second: 0,\n Millisecond: 3,\n Microsecond: 6,\n};\nexport function _isoDateTime(Class, params) {\n return new Class({\n type: \"string\",\n format: \"datetime\",\n check: \"string_format\",\n offset: false,\n local: false,\n precision: null,\n ...util.normalizeParams(params),\n });\n}\nexport function _isoDate(Class, params) {\n return new Class({\n type: \"string\",\n format: \"date\",\n check: \"string_format\",\n ...util.normalizeParams(params),\n });\n}\nexport function _isoTime(Class, params) {\n return new Class({\n type: \"string\",\n format: \"time\",\n check: \"string_format\",\n precision: null,\n ...util.normalizeParams(params),\n });\n}\nexport function _isoDuration(Class, params) {\n return new Class({\n type: \"string\",\n format: \"duration\",\n check: \"string_format\",\n ...util.normalizeParams(params),\n });\n}\nexport function _number(Class, params) {\n return new Class({\n type: \"number\",\n checks: [],\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedNumber(Class, params) {\n return new Class({\n type: \"number\",\n coerce: true,\n checks: [],\n ...util.normalizeParams(params),\n });\n}\nexport function _int(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"safeint\",\n ...util.normalizeParams(params),\n });\n}\nexport function _float32(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"float32\",\n ...util.normalizeParams(params),\n });\n}\nexport function _float64(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"float64\",\n ...util.normalizeParams(params),\n });\n}\nexport function _int32(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"int32\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uint32(Class, params) {\n return new Class({\n type: \"number\",\n check: \"number_format\",\n abort: false,\n format: \"uint32\",\n ...util.normalizeParams(params),\n });\n}\nexport function _boolean(Class, params) {\n return new Class({\n type: \"boolean\",\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedBoolean(Class, params) {\n return new Class({\n type: \"boolean\",\n coerce: true,\n ...util.normalizeParams(params),\n });\n}\nexport function _bigint(Class, params) {\n return new Class({\n type: \"bigint\",\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedBigint(Class, params) {\n return new Class({\n type: \"bigint\",\n coerce: true,\n ...util.normalizeParams(params),\n });\n}\nexport function _int64(Class, params) {\n return new Class({\n type: \"bigint\",\n check: \"bigint_format\",\n abort: false,\n format: \"int64\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uint64(Class, params) {\n return new Class({\n type: \"bigint\",\n check: \"bigint_format\",\n abort: false,\n format: \"uint64\",\n ...util.normalizeParams(params),\n });\n}\nexport function _symbol(Class, params) {\n return new Class({\n type: \"symbol\",\n ...util.normalizeParams(params),\n });\n}\nexport function _undefined(Class, params) {\n return new Class({\n type: \"undefined\",\n ...util.normalizeParams(params),\n });\n}\nexport function _null(Class, params) {\n return new Class({\n type: \"null\",\n ...util.normalizeParams(params),\n });\n}\nexport function _any(Class) {\n return new Class({\n type: \"any\",\n });\n}\nexport function _unknown(Class) {\n return new Class({\n type: \"unknown\",\n });\n}\nexport function _never(Class, params) {\n return new Class({\n type: \"never\",\n ...util.normalizeParams(params),\n });\n}\nexport function _void(Class, params) {\n return new Class({\n type: \"void\",\n ...util.normalizeParams(params),\n });\n}\nexport function _date(Class, params) {\n return new Class({\n type: \"date\",\n ...util.normalizeParams(params),\n });\n}\nexport function _coercedDate(Class, params) {\n return new Class({\n type: \"date\",\n coerce: true,\n ...util.normalizeParams(params),\n });\n}\nexport function _nan(Class, params) {\n return new Class({\n type: \"nan\",\n ...util.normalizeParams(params),\n });\n}\nexport function _lt(value, params) {\n return new checks.$ZodCheckLessThan({\n check: \"less_than\",\n ...util.normalizeParams(params),\n value,\n inclusive: false,\n });\n}\nexport function _lte(value, params) {\n return new checks.$ZodCheckLessThan({\n check: \"less_than\",\n ...util.normalizeParams(params),\n value,\n inclusive: true,\n });\n}\nexport { \n/** @deprecated Use `z.lte()` instead. */\n_lte as _max, };\nexport function _gt(value, params) {\n return new checks.$ZodCheckGreaterThan({\n check: \"greater_than\",\n ...util.normalizeParams(params),\n value,\n inclusive: false,\n });\n}\nexport function _gte(value, params) {\n return new checks.$ZodCheckGreaterThan({\n check: \"greater_than\",\n ...util.normalizeParams(params),\n value,\n inclusive: true,\n });\n}\nexport { \n/** @deprecated Use `z.gte()` instead. */\n_gte as _min, };\nexport function _positive(params) {\n return _gt(0, params);\n}\n// negative\nexport function _negative(params) {\n return _lt(0, params);\n}\n// nonpositive\nexport function _nonpositive(params) {\n return _lte(0, params);\n}\n// nonnegative\nexport function _nonnegative(params) {\n return _gte(0, params);\n}\nexport function _multipleOf(value, params) {\n return new checks.$ZodCheckMultipleOf({\n check: \"multiple_of\",\n ...util.normalizeParams(params),\n value,\n });\n}\nexport function _maxSize(maximum, params) {\n return new checks.$ZodCheckMaxSize({\n check: \"max_size\",\n ...util.normalizeParams(params),\n maximum,\n });\n}\nexport function _minSize(minimum, params) {\n return new checks.$ZodCheckMinSize({\n check: \"min_size\",\n ...util.normalizeParams(params),\n minimum,\n });\n}\nexport function _size(size, params) {\n return new checks.$ZodCheckSizeEquals({\n check: \"size_equals\",\n ...util.normalizeParams(params),\n size,\n });\n}\nexport function _maxLength(maximum, params) {\n const ch = new checks.$ZodCheckMaxLength({\n check: \"max_length\",\n ...util.normalizeParams(params),\n maximum,\n });\n return ch;\n}\nexport function _minLength(minimum, params) {\n return new checks.$ZodCheckMinLength({\n check: \"min_length\",\n ...util.normalizeParams(params),\n minimum,\n });\n}\nexport function _length(length, params) {\n return new checks.$ZodCheckLengthEquals({\n check: \"length_equals\",\n ...util.normalizeParams(params),\n length,\n });\n}\nexport function _regex(pattern, params) {\n return new checks.$ZodCheckRegex({\n check: \"string_format\",\n format: \"regex\",\n ...util.normalizeParams(params),\n pattern,\n });\n}\nexport function _lowercase(params) {\n return new checks.$ZodCheckLowerCase({\n check: \"string_format\",\n format: \"lowercase\",\n ...util.normalizeParams(params),\n });\n}\nexport function _uppercase(params) {\n return new checks.$ZodCheckUpperCase({\n check: \"string_format\",\n format: \"uppercase\",\n ...util.normalizeParams(params),\n });\n}\nexport function _includes(includes, params) {\n return new checks.$ZodCheckIncludes({\n check: \"string_format\",\n format: \"includes\",\n ...util.normalizeParams(params),\n includes,\n });\n}\nexport function _startsWith(prefix, params) {\n return new checks.$ZodCheckStartsWith({\n check: \"string_format\",\n format: \"starts_with\",\n ...util.normalizeParams(params),\n prefix,\n });\n}\nexport function _endsWith(suffix, params) {\n return new checks.$ZodCheckEndsWith({\n check: \"string_format\",\n format: \"ends_with\",\n ...util.normalizeParams(params),\n suffix,\n });\n}\nexport function _property(property, schema, params) {\n return new checks.$ZodCheckProperty({\n check: \"property\",\n property,\n schema,\n ...util.normalizeParams(params),\n });\n}\nexport function _mime(types, params) {\n return new checks.$ZodCheckMimeType({\n check: \"mime_type\",\n mime: types,\n ...util.normalizeParams(params),\n });\n}\nexport function _overwrite(tx) {\n return new checks.$ZodCheckOverwrite({\n check: \"overwrite\",\n tx,\n });\n}\n// normalize\nexport function _normalize(form) {\n return _overwrite((input) => input.normalize(form));\n}\n// trim\nexport function _trim() {\n return _overwrite((input) => input.trim());\n}\n// toLowerCase\nexport function _toLowerCase() {\n return _overwrite((input) => input.toLowerCase());\n}\n// toUpperCase\nexport function _toUpperCase() {\n return _overwrite((input) => input.toUpperCase());\n}\nexport function _array(Class, element, params) {\n return new Class({\n type: \"array\",\n element,\n // get element() {\n // return element;\n // },\n ...util.normalizeParams(params),\n });\n}\nexport function _union(Class, options, params) {\n return new Class({\n type: \"union\",\n options,\n ...util.normalizeParams(params),\n });\n}\nexport function _discriminatedUnion(Class, discriminator, options, params) {\n return new Class({\n type: \"union\",\n options,\n discriminator,\n ...util.normalizeParams(params),\n });\n}\nexport function _intersection(Class, left, right) {\n return new Class({\n type: \"intersection\",\n left,\n right,\n });\n}\n// export function _tuple(\n// Class: util.SchemaClass,\n// items: [],\n// params?: string | $ZodTupleParams\n// ): schemas.$ZodTuple<[], null>;\nexport function _tuple(Class, items, _paramsOrRest, _params) {\n const hasRest = _paramsOrRest instanceof schemas.$ZodType;\n const params = hasRest ? _params : _paramsOrRest;\n const rest = hasRest ? _paramsOrRest : null;\n return new Class({\n type: \"tuple\",\n items,\n rest,\n ...util.normalizeParams(params),\n });\n}\nexport function _record(Class, keyType, valueType, params) {\n return new Class({\n type: \"record\",\n keyType,\n valueType,\n ...util.normalizeParams(params),\n });\n}\nexport function _map(Class, keyType, valueType, params) {\n return new Class({\n type: \"map\",\n keyType,\n valueType,\n ...util.normalizeParams(params),\n });\n}\nexport function _set(Class, valueType, params) {\n return new Class({\n type: \"set\",\n valueType,\n ...util.normalizeParams(params),\n });\n}\nexport function _enum(Class, values, params) {\n const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;\n // if (Array.isArray(values)) {\n // for (const value of values) {\n // entries[value] = value;\n // }\n // } else {\n // Object.assign(entries, values);\n // }\n // const entries: util.EnumLike = {};\n // for (const val of values) {\n // entries[val] = val;\n // }\n return new Class({\n type: \"enum\",\n entries,\n ...util.normalizeParams(params),\n });\n}\n/** @deprecated This API has been merged into `z.enum()`. Use `z.enum()` instead.\n *\n * ```ts\n * enum Colors { red, green, blue }\n * z.enum(Colors);\n * ```\n */\nexport function _nativeEnum(Class, entries, params) {\n return new Class({\n type: \"enum\",\n entries,\n ...util.normalizeParams(params),\n });\n}\nexport function _literal(Class, value, params) {\n return new Class({\n type: \"literal\",\n values: Array.isArray(value) ? value : [value],\n ...util.normalizeParams(params),\n });\n}\nexport function _file(Class, params) {\n return new Class({\n type: \"file\",\n ...util.normalizeParams(params),\n });\n}\nexport function _transform(Class, fn) {\n return new Class({\n type: \"transform\",\n transform: fn,\n });\n}\nexport function _optional(Class, innerType) {\n return new Class({\n type: \"optional\",\n innerType,\n });\n}\nexport function _nullable(Class, innerType) {\n return new Class({\n type: \"nullable\",\n innerType,\n });\n}\nexport function _default(Class, innerType, defaultValue) {\n return new Class({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : util.shallowClone(defaultValue);\n },\n });\n}\nexport function _nonoptional(Class, innerType, params) {\n return new Class({\n type: \"nonoptional\",\n innerType,\n ...util.normalizeParams(params),\n });\n}\nexport function _success(Class, innerType) {\n return new Class({\n type: \"success\",\n innerType,\n });\n}\nexport function _catch(Class, innerType, catchValue) {\n return new Class({\n type: \"catch\",\n innerType,\n catchValue: (typeof catchValue === \"function\" ? catchValue : () => catchValue),\n });\n}\nexport function _pipe(Class, in_, out) {\n return new Class({\n type: \"pipe\",\n in: in_,\n out,\n });\n}\nexport function _readonly(Class, innerType) {\n return new Class({\n type: \"readonly\",\n innerType,\n });\n}\nexport function _templateLiteral(Class, parts, params) {\n return new Class({\n type: \"template_literal\",\n parts,\n ...util.normalizeParams(params),\n });\n}\nexport function _lazy(Class, getter) {\n return new Class({\n type: \"lazy\",\n getter,\n });\n}\nexport function _promise(Class, innerType) {\n return new Class({\n type: \"promise\",\n innerType,\n });\n}\nexport function _custom(Class, fn, _params) {\n const norm = util.normalizeParams(_params);\n norm.abort ?? (norm.abort = true); // default to abort:false\n const schema = new Class({\n type: \"custom\",\n check: \"custom\",\n fn: fn,\n ...norm,\n });\n return schema;\n}\n// same as _custom but defaults to abort:false\nexport function _refine(Class, fn, _params) {\n const schema = new Class({\n type: \"custom\",\n check: \"custom\",\n fn: fn,\n ...util.normalizeParams(_params),\n });\n return schema;\n}\nexport function _superRefine(fn) {\n const ch = _check((payload) => {\n payload.addIssue = (issue) => {\n if (typeof issue === \"string\") {\n payload.issues.push(util.issue(issue, payload.value, ch._zod.def));\n }\n else {\n // for Zod 3 backwards compatibility\n const _issue = issue;\n if (_issue.fatal)\n _issue.continue = false;\n _issue.code ?? (_issue.code = \"custom\");\n _issue.input ?? (_issue.input = payload.value);\n _issue.inst ?? (_issue.inst = ch);\n _issue.continue ?? (_issue.continue = !ch._zod.def.abort); // abort is always undefined, so this is always true...\n payload.issues.push(util.issue(_issue));\n }\n };\n return fn(payload.value, payload);\n });\n return ch;\n}\nexport function _check(fn, params) {\n const ch = new checks.$ZodCheck({\n check: \"custom\",\n ...util.normalizeParams(params),\n });\n ch._zod.check = fn;\n return ch;\n}\nexport function _stringbool(Classes, _params) {\n const params = util.normalizeParams(_params);\n let truthyArray = params.truthy ?? [\"true\", \"1\", \"yes\", \"on\", \"y\", \"enabled\"];\n let falsyArray = params.falsy ?? [\"false\", \"0\", \"no\", \"off\", \"n\", \"disabled\"];\n if (params.case !== \"sensitive\") {\n truthyArray = truthyArray.map((v) => (typeof v === \"string\" ? v.toLowerCase() : v));\n falsyArray = falsyArray.map((v) => (typeof v === \"string\" ? v.toLowerCase() : v));\n }\n const truthySet = new Set(truthyArray);\n const falsySet = new Set(falsyArray);\n const _Codec = Classes.Codec ?? schemas.$ZodCodec;\n const _Boolean = Classes.Boolean ?? schemas.$ZodBoolean;\n const _String = Classes.String ?? schemas.$ZodString;\n const stringSchema = new _String({ type: \"string\", error: params.error });\n const booleanSchema = new _Boolean({ type: \"boolean\", error: params.error });\n const codec = new _Codec({\n type: \"pipe\",\n in: stringSchema,\n out: booleanSchema,\n transform: ((input, payload) => {\n let data = input;\n if (params.case !== \"sensitive\")\n data = data.toLowerCase();\n if (truthySet.has(data)) {\n return true;\n }\n else if (falsySet.has(data)) {\n return false;\n }\n else {\n payload.issues.push({\n code: \"invalid_value\",\n expected: \"stringbool\",\n values: [...truthySet, ...falsySet],\n input: payload.value,\n inst: codec,\n continue: false,\n });\n return {};\n }\n }),\n reverseTransform: ((input, _payload) => {\n if (input === true) {\n return truthyArray[0] || \"true\";\n }\n else {\n return falsyArray[0] || \"false\";\n }\n }),\n error: params.error,\n });\n return codec;\n}\nexport function _stringFormat(Class, format, fnOrRegex, _params = {}) {\n const params = util.normalizeParams(_params);\n const def = {\n ...util.normalizeParams(_params),\n check: \"string_format\",\n type: \"string\",\n format,\n fn: typeof fnOrRegex === \"function\" ? fnOrRegex : (val) => fnOrRegex.test(val),\n ...params,\n };\n if (fnOrRegex instanceof RegExp) {\n def.pattern = fnOrRegex;\n }\n const inst = new Class(def);\n return inst;\n}\n","import { $ZodRegistry, globalRegistry } from \"./registries.js\";\nimport { getEnumValues } from \"./util.js\";\nexport class JSONSchemaGenerator {\n constructor(params) {\n this.counter = 0;\n this.metadataRegistry = params?.metadata ?? globalRegistry;\n this.target = params?.target ?? \"draft-2020-12\";\n this.unrepresentable = params?.unrepresentable ?? \"throw\";\n this.override = params?.override ?? (() => { });\n this.io = params?.io ?? \"output\";\n this.seen = new Map();\n }\n process(schema, _params = { path: [], schemaPath: [] }) {\n var _a;\n const def = schema._zod.def;\n const formatMap = {\n guid: \"uuid\",\n url: \"uri\",\n datetime: \"date-time\",\n json_string: \"json-string\",\n regex: \"\", // do not set\n };\n // check for schema in seens\n const seen = this.seen.get(schema);\n if (seen) {\n seen.count++;\n // check if cycle\n const isCycle = _params.schemaPath.includes(schema);\n if (isCycle) {\n seen.cycle = _params.path;\n }\n return seen.schema;\n }\n // initialize\n const result = { schema: {}, count: 1, cycle: undefined, path: _params.path };\n this.seen.set(schema, result);\n // custom method overrides default behavior\n const overrideSchema = schema._zod.toJSONSchema?.();\n if (overrideSchema) {\n result.schema = overrideSchema;\n }\n else {\n const params = {\n ..._params,\n schemaPath: [..._params.schemaPath, schema],\n path: _params.path,\n };\n const parent = schema._zod.parent;\n if (parent) {\n // schema was cloned from another schema\n result.ref = parent;\n this.process(parent, params);\n this.seen.get(parent).isParent = true;\n }\n else {\n const _json = result.schema;\n switch (def.type) {\n case \"string\": {\n const json = _json;\n json.type = \"string\";\n const { minimum, maximum, format, patterns, contentEncoding } = schema._zod\n .bag;\n if (typeof minimum === \"number\")\n json.minLength = minimum;\n if (typeof maximum === \"number\")\n json.maxLength = maximum;\n // custom pattern overrides format\n if (format) {\n json.format = formatMap[format] ?? format;\n if (json.format === \"\")\n delete json.format; // empty format is not valid\n }\n if (contentEncoding)\n json.contentEncoding = contentEncoding;\n if (patterns && patterns.size > 0) {\n const regexes = [...patterns];\n if (regexes.length === 1)\n json.pattern = regexes[0].source;\n else if (regexes.length > 1) {\n result.schema.allOf = [\n ...regexes.map((regex) => ({\n ...(this.target === \"draft-7\" || this.target === \"draft-4\" || this.target === \"openapi-3.0\"\n ? { type: \"string\" }\n : {}),\n pattern: regex.source,\n })),\n ];\n }\n }\n break;\n }\n case \"number\": {\n const json = _json;\n const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;\n if (typeof format === \"string\" && format.includes(\"int\"))\n json.type = \"integer\";\n else\n json.type = \"number\";\n if (typeof exclusiveMinimum === \"number\") {\n if (this.target === \"draft-4\" || this.target === \"openapi-3.0\") {\n json.minimum = exclusiveMinimum;\n json.exclusiveMinimum = true;\n }\n else {\n json.exclusiveMinimum = exclusiveMinimum;\n }\n }\n if (typeof minimum === \"number\") {\n json.minimum = minimum;\n if (typeof exclusiveMinimum === \"number\" && this.target !== \"draft-4\") {\n if (exclusiveMinimum >= minimum)\n delete json.minimum;\n else\n delete json.exclusiveMinimum;\n }\n }\n if (typeof exclusiveMaximum === \"number\") {\n if (this.target === \"draft-4\" || this.target === \"openapi-3.0\") {\n json.maximum = exclusiveMaximum;\n json.exclusiveMaximum = true;\n }\n else {\n json.exclusiveMaximum = exclusiveMaximum;\n }\n }\n if (typeof maximum === \"number\") {\n json.maximum = maximum;\n if (typeof exclusiveMaximum === \"number\" && this.target !== \"draft-4\") {\n if (exclusiveMaximum <= maximum)\n delete json.maximum;\n else\n delete json.exclusiveMaximum;\n }\n }\n if (typeof multipleOf === \"number\")\n json.multipleOf = multipleOf;\n break;\n }\n case \"boolean\": {\n const json = _json;\n json.type = \"boolean\";\n break;\n }\n case \"bigint\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"BigInt cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"symbol\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Symbols cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"null\": {\n if (this.target === \"openapi-3.0\") {\n _json.type = \"string\";\n _json.nullable = true;\n _json.enum = [null];\n }\n else\n _json.type = \"null\";\n break;\n }\n case \"any\": {\n break;\n }\n case \"unknown\": {\n break;\n }\n case \"undefined\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Undefined cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"void\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Void cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"never\": {\n _json.not = {};\n break;\n }\n case \"date\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Date cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"array\": {\n const json = _json;\n const { minimum, maximum } = schema._zod.bag;\n if (typeof minimum === \"number\")\n json.minItems = minimum;\n if (typeof maximum === \"number\")\n json.maxItems = maximum;\n json.type = \"array\";\n json.items = this.process(def.element, { ...params, path: [...params.path, \"items\"] });\n break;\n }\n case \"object\": {\n const json = _json;\n json.type = \"object\";\n json.properties = {};\n const shape = def.shape; // params.shapeCache.get(schema)!;\n for (const key in shape) {\n json.properties[key] = this.process(shape[key], {\n ...params,\n path: [...params.path, \"properties\", key],\n });\n }\n // required keys\n const allKeys = new Set(Object.keys(shape));\n // const optionalKeys = new Set(def.optional);\n const requiredKeys = new Set([...allKeys].filter((key) => {\n const v = def.shape[key]._zod;\n if (this.io === \"input\") {\n return v.optin === undefined;\n }\n else {\n return v.optout === undefined;\n }\n }));\n if (requiredKeys.size > 0) {\n json.required = Array.from(requiredKeys);\n }\n // catchall\n if (def.catchall?._zod.def.type === \"never\") {\n // strict\n json.additionalProperties = false;\n }\n else if (!def.catchall) {\n // regular\n if (this.io === \"output\")\n json.additionalProperties = false;\n }\n else if (def.catchall) {\n json.additionalProperties = this.process(def.catchall, {\n ...params,\n path: [...params.path, \"additionalProperties\"],\n });\n }\n break;\n }\n case \"union\": {\n const json = _json;\n const options = def.options.map((x, i) => this.process(x, {\n ...params,\n path: [...params.path, \"anyOf\", i],\n }));\n json.anyOf = options;\n break;\n }\n case \"intersection\": {\n const json = _json;\n const a = this.process(def.left, {\n ...params,\n path: [...params.path, \"allOf\", 0],\n });\n const b = this.process(def.right, {\n ...params,\n path: [...params.path, \"allOf\", 1],\n });\n const isSimpleIntersection = (val) => \"allOf\" in val && Object.keys(val).length === 1;\n const allOf = [\n ...(isSimpleIntersection(a) ? a.allOf : [a]),\n ...(isSimpleIntersection(b) ? b.allOf : [b]),\n ];\n json.allOf = allOf;\n break;\n }\n case \"tuple\": {\n const json = _json;\n json.type = \"array\";\n const prefixPath = this.target === \"draft-2020-12\" ? \"prefixItems\" : \"items\";\n const restPath = this.target === \"draft-2020-12\" ? \"items\" : this.target === \"openapi-3.0\" ? \"items\" : \"additionalItems\";\n const prefixItems = def.items.map((x, i) => this.process(x, {\n ...params,\n path: [...params.path, prefixPath, i],\n }));\n const rest = def.rest\n ? this.process(def.rest, {\n ...params,\n path: [...params.path, restPath, ...(this.target === \"openapi-3.0\" ? [def.items.length] : [])],\n })\n : null;\n if (this.target === \"draft-2020-12\") {\n json.prefixItems = prefixItems;\n if (rest) {\n json.items = rest;\n }\n }\n else if (this.target === \"openapi-3.0\") {\n json.items = {\n anyOf: prefixItems,\n };\n if (rest) {\n json.items.anyOf.push(rest);\n }\n json.minItems = prefixItems.length;\n if (!rest) {\n json.maxItems = prefixItems.length;\n }\n }\n else {\n json.items = prefixItems;\n if (rest) {\n json.additionalItems = rest;\n }\n }\n // length\n const { minimum, maximum } = schema._zod.bag;\n if (typeof minimum === \"number\")\n json.minItems = minimum;\n if (typeof maximum === \"number\")\n json.maxItems = maximum;\n break;\n }\n case \"record\": {\n const json = _json;\n json.type = \"object\";\n if (this.target === \"draft-7\" || this.target === \"draft-2020-12\") {\n json.propertyNames = this.process(def.keyType, {\n ...params,\n path: [...params.path, \"propertyNames\"],\n });\n }\n json.additionalProperties = this.process(def.valueType, {\n ...params,\n path: [...params.path, \"additionalProperties\"],\n });\n break;\n }\n case \"map\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Map cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"set\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Set cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"enum\": {\n const json = _json;\n const values = getEnumValues(def.entries);\n // Number enums can have both string and number values\n if (values.every((v) => typeof v === \"number\"))\n json.type = \"number\";\n if (values.every((v) => typeof v === \"string\"))\n json.type = \"string\";\n json.enum = values;\n break;\n }\n case \"literal\": {\n const json = _json;\n const vals = [];\n for (const val of def.values) {\n if (val === undefined) {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Literal `undefined` cannot be represented in JSON Schema\");\n }\n else {\n // do not add to vals\n }\n }\n else if (typeof val === \"bigint\") {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"BigInt literals cannot be represented in JSON Schema\");\n }\n else {\n vals.push(Number(val));\n }\n }\n else {\n vals.push(val);\n }\n }\n if (vals.length === 0) {\n // do nothing (an undefined literal was stripped)\n }\n else if (vals.length === 1) {\n const val = vals[0];\n json.type = val === null ? \"null\" : typeof val;\n if (this.target === \"draft-4\" || this.target === \"openapi-3.0\") {\n json.enum = [val];\n }\n else {\n json.const = val;\n }\n }\n else {\n if (vals.every((v) => typeof v === \"number\"))\n json.type = \"number\";\n if (vals.every((v) => typeof v === \"string\"))\n json.type = \"string\";\n if (vals.every((v) => typeof v === \"boolean\"))\n json.type = \"string\";\n if (vals.every((v) => v === null))\n json.type = \"null\";\n json.enum = vals;\n }\n break;\n }\n case \"file\": {\n const json = _json;\n const file = {\n type: \"string\",\n format: \"binary\",\n contentEncoding: \"binary\",\n };\n const { minimum, maximum, mime } = schema._zod.bag;\n if (minimum !== undefined)\n file.minLength = minimum;\n if (maximum !== undefined)\n file.maxLength = maximum;\n if (mime) {\n if (mime.length === 1) {\n file.contentMediaType = mime[0];\n Object.assign(json, file);\n }\n else {\n json.anyOf = mime.map((m) => {\n const mFile = { ...file, contentMediaType: m };\n return mFile;\n });\n }\n }\n else {\n Object.assign(json, file);\n }\n // if (this.unrepresentable === \"throw\") {\n // throw new Error(\"File cannot be represented in JSON Schema\");\n // }\n break;\n }\n case \"transform\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Transforms cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"nullable\": {\n const inner = this.process(def.innerType, params);\n if (this.target === \"openapi-3.0\") {\n result.ref = def.innerType;\n _json.nullable = true;\n }\n else {\n _json.anyOf = [inner, { type: \"null\" }];\n }\n break;\n }\n case \"nonoptional\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n break;\n }\n case \"success\": {\n const json = _json;\n json.type = \"boolean\";\n break;\n }\n case \"default\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n _json.default = JSON.parse(JSON.stringify(def.defaultValue));\n break;\n }\n case \"prefault\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n if (this.io === \"input\")\n _json._prefault = JSON.parse(JSON.stringify(def.defaultValue));\n break;\n }\n case \"catch\": {\n // use conditionals\n this.process(def.innerType, params);\n result.ref = def.innerType;\n let catchValue;\n try {\n catchValue = def.catchValue(undefined);\n }\n catch {\n throw new Error(\"Dynamic catch values are not supported in JSON Schema\");\n }\n _json.default = catchValue;\n break;\n }\n case \"nan\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"NaN cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"template_literal\": {\n const json = _json;\n const pattern = schema._zod.pattern;\n if (!pattern)\n throw new Error(\"Pattern not found in template literal\");\n json.type = \"string\";\n json.pattern = pattern.source;\n break;\n }\n case \"pipe\": {\n const innerType = this.io === \"input\" ? (def.in._zod.def.type === \"transform\" ? def.out : def.in) : def.out;\n this.process(innerType, params);\n result.ref = innerType;\n break;\n }\n case \"readonly\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n _json.readOnly = true;\n break;\n }\n // passthrough types\n case \"promise\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n break;\n }\n case \"optional\": {\n this.process(def.innerType, params);\n result.ref = def.innerType;\n break;\n }\n case \"lazy\": {\n const innerType = schema._zod.innerType;\n this.process(innerType, params);\n result.ref = innerType;\n break;\n }\n case \"custom\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Custom types cannot be represented in JSON Schema\");\n }\n break;\n }\n case \"function\": {\n if (this.unrepresentable === \"throw\") {\n throw new Error(\"Function types cannot be represented in JSON Schema\");\n }\n break;\n }\n default: {\n def;\n }\n }\n }\n }\n // metadata\n const meta = this.metadataRegistry.get(schema);\n if (meta)\n Object.assign(result.schema, meta);\n if (this.io === \"input\" && isTransforming(schema)) {\n // examples/defaults only apply to output type of pipe\n delete result.schema.examples;\n delete result.schema.default;\n }\n // set prefault as default\n if (this.io === \"input\" && result.schema._prefault)\n (_a = result.schema).default ?? (_a.default = result.schema._prefault);\n delete result.schema._prefault;\n // pulling fresh from this.seen in case it was overwritten\n const _result = this.seen.get(schema);\n return _result.schema;\n }\n emit(schema, _params) {\n const params = {\n cycles: _params?.cycles ?? \"ref\",\n reused: _params?.reused ?? \"inline\",\n // unrepresentable: _params?.unrepresentable ?? \"throw\",\n // uri: _params?.uri ?? ((id) => `${id}`),\n external: _params?.external ?? undefined,\n };\n // iterate over seen map;\n const root = this.seen.get(schema);\n if (!root)\n throw new Error(\"Unprocessed schema. This is a bug in Zod.\");\n // initialize result with root schema fields\n // Object.assign(result, seen.cached);\n // returns a ref to the schema\n // defId will be empty if the ref points to an external schema (or #)\n const makeURI = (entry) => {\n // comparing the seen objects because sometimes\n // multiple schemas map to the same seen object.\n // e.g. lazy\n // external is configured\n const defsSegment = this.target === \"draft-2020-12\" ? \"$defs\" : \"definitions\";\n if (params.external) {\n const externalId = params.external.registry.get(entry[0])?.id; // ?? \"__shared\";// `__schema${this.counter++}`;\n // check if schema is in the external registry\n const uriGenerator = params.external.uri ?? ((id) => id);\n if (externalId) {\n return { ref: uriGenerator(externalId) };\n }\n // otherwise, add to __shared\n const id = entry[1].defId ?? entry[1].schema.id ?? `schema${this.counter++}`;\n entry[1].defId = id; // set defId so it will be reused if needed\n return { defId: id, ref: `${uriGenerator(\"__shared\")}#/${defsSegment}/${id}` };\n }\n if (entry[1] === root) {\n return { ref: \"#\" };\n }\n // self-contained schema\n const uriPrefix = `#`;\n const defUriPrefix = `${uriPrefix}/${defsSegment}/`;\n const defId = entry[1].schema.id ?? `__schema${this.counter++}`;\n return { defId, ref: defUriPrefix + defId };\n };\n // stored cached version in `def` property\n // remove all properties, set $ref\n const extractToDef = (entry) => {\n // if the schema is already a reference, do not extract it\n if (entry[1].schema.$ref) {\n return;\n }\n const seen = entry[1];\n const { ref, defId } = makeURI(entry);\n seen.def = { ...seen.schema };\n // defId won't be set if the schema is a reference to an external schema\n if (defId)\n seen.defId = defId;\n // wipe away all properties except $ref\n const schema = seen.schema;\n for (const key in schema) {\n delete schema[key];\n }\n schema.$ref = ref;\n };\n // throw on cycles\n // break cycles\n if (params.cycles === \"throw\") {\n for (const entry of this.seen.entries()) {\n const seen = entry[1];\n if (seen.cycle) {\n throw new Error(\"Cycle detected: \" +\n `#/${seen.cycle?.join(\"/\")}/` +\n '\\n\\nSet the `cycles` parameter to `\"ref\"` to resolve cyclical schemas with defs.');\n }\n }\n }\n // extract schemas into $defs\n for (const entry of this.seen.entries()) {\n const seen = entry[1];\n // convert root schema to # $ref\n if (schema === entry[0]) {\n extractToDef(entry); // this has special handling for the root schema\n continue;\n }\n // extract schemas that are in the external registry\n if (params.external) {\n const ext = params.external.registry.get(entry[0])?.id;\n if (schema !== entry[0] && ext) {\n extractToDef(entry);\n continue;\n }\n }\n // extract schemas with `id` meta\n const id = this.metadataRegistry.get(entry[0])?.id;\n if (id) {\n extractToDef(entry);\n continue;\n }\n // break cycles\n if (seen.cycle) {\n // any\n extractToDef(entry);\n continue;\n }\n // extract reused schemas\n if (seen.count > 1) {\n if (params.reused === \"ref\") {\n extractToDef(entry);\n // biome-ignore lint:\n continue;\n }\n }\n }\n // flatten _refs\n const flattenRef = (zodSchema, params) => {\n const seen = this.seen.get(zodSchema);\n const schema = seen.def ?? seen.schema;\n const _cached = { ...schema };\n // already seen\n if (seen.ref === null) {\n return;\n }\n // flatten ref if defined\n const ref = seen.ref;\n seen.ref = null; // prevent recursion\n if (ref) {\n flattenRef(ref, params);\n // merge referenced schema into current\n const refSchema = this.seen.get(ref).schema;\n if (refSchema.$ref &&\n (params.target === \"draft-7\" || params.target === \"draft-4\" || params.target === \"openapi-3.0\")) {\n schema.allOf = schema.allOf ?? [];\n schema.allOf.push(refSchema);\n }\n else {\n Object.assign(schema, refSchema);\n Object.assign(schema, _cached); // prevent overwriting any fields in the original schema\n }\n }\n // execute overrides\n if (!seen.isParent)\n this.override({\n zodSchema: zodSchema,\n jsonSchema: schema,\n path: seen.path ?? [],\n });\n };\n for (const entry of [...this.seen.entries()].reverse()) {\n flattenRef(entry[0], { target: this.target });\n }\n const result = {};\n if (this.target === \"draft-2020-12\") {\n result.$schema = \"https://json-schema.org/draft/2020-12/schema\";\n }\n else if (this.target === \"draft-7\") {\n result.$schema = \"http://json-schema.org/draft-07/schema#\";\n }\n else if (this.target === \"draft-4\") {\n result.$schema = \"http://json-schema.org/draft-04/schema#\";\n }\n else if (this.target === \"openapi-3.0\") {\n // OpenAPI 3.0 schema objects should not include a $schema property\n }\n else {\n // @ts-ignore\n console.warn(`Invalid target: ${this.target}`);\n }\n if (params.external?.uri) {\n const id = params.external.registry.get(schema)?.id;\n if (!id)\n throw new Error(\"Schema is missing an `id` property\");\n result.$id = params.external.uri(id);\n }\n Object.assign(result, root.def);\n // build defs object\n const defs = params.external?.defs ?? {};\n for (const entry of this.seen.entries()) {\n const seen = entry[1];\n if (seen.def && seen.defId) {\n defs[seen.defId] = seen.def;\n }\n }\n // set definitions in result\n if (params.external) {\n }\n else {\n if (Object.keys(defs).length > 0) {\n if (this.target === \"draft-2020-12\") {\n result.$defs = defs;\n }\n else {\n result.definitions = defs;\n }\n }\n }\n try {\n // this \"finalizes\" this schema and ensures all cycles are removed\n // each call to .emit() is functionally independent\n // though the seen map is shared\n return JSON.parse(JSON.stringify(result));\n }\n catch (_err) {\n throw new Error(\"Error converting schema to JSON.\");\n }\n }\n}\nexport function toJSONSchema(input, _params) {\n if (input instanceof $ZodRegistry) {\n const gen = new JSONSchemaGenerator(_params);\n const defs = {};\n for (const entry of input._idmap.entries()) {\n const [_, schema] = entry;\n gen.process(schema);\n }\n const schemas = {};\n const external = {\n registry: input,\n uri: _params?.uri,\n defs,\n };\n for (const entry of input._idmap.entries()) {\n const [key, schema] = entry;\n schemas[key] = gen.emit(schema, {\n ..._params,\n external,\n });\n }\n if (Object.keys(defs).length > 0) {\n const defsSegment = gen.target === \"draft-2020-12\" ? \"$defs\" : \"definitions\";\n schemas.__shared = {\n [defsSegment]: defs,\n };\n }\n return { schemas };\n }\n const gen = new JSONSchemaGenerator(_params);\n gen.process(input);\n return gen.emit(input, _params);\n}\nfunction isTransforming(_schema, _ctx) {\n const ctx = _ctx ?? { seen: new Set() };\n if (ctx.seen.has(_schema))\n return false;\n ctx.seen.add(_schema);\n const schema = _schema;\n const def = schema._zod.def;\n switch (def.type) {\n case \"string\":\n case \"number\":\n case \"bigint\":\n case \"boolean\":\n case \"date\":\n case \"symbol\":\n case \"undefined\":\n case \"null\":\n case \"any\":\n case \"unknown\":\n case \"never\":\n case \"void\":\n case \"literal\":\n case \"enum\":\n case \"nan\":\n case \"file\":\n case \"template_literal\":\n return false;\n case \"array\": {\n return isTransforming(def.element, ctx);\n }\n case \"object\": {\n for (const key in def.shape) {\n if (isTransforming(def.shape[key], ctx))\n return true;\n }\n return false;\n }\n case \"union\": {\n for (const option of def.options) {\n if (isTransforming(option, ctx))\n return true;\n }\n return false;\n }\n case \"intersection\": {\n return isTransforming(def.left, ctx) || isTransforming(def.right, ctx);\n }\n case \"tuple\": {\n for (const item of def.items) {\n if (isTransforming(item, ctx))\n return true;\n }\n if (def.rest && isTransforming(def.rest, ctx))\n return true;\n return false;\n }\n case \"record\": {\n return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx);\n }\n case \"map\": {\n return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx);\n }\n case \"set\": {\n return isTransforming(def.valueType, ctx);\n }\n // inner types\n case \"promise\":\n case \"optional\":\n case \"nonoptional\":\n case \"nullable\":\n case \"readonly\":\n return isTransforming(def.innerType, ctx);\n case \"lazy\":\n return isTransforming(def.getter(), ctx);\n case \"default\": {\n return isTransforming(def.innerType, ctx);\n }\n case \"prefault\": {\n return isTransforming(def.innerType, ctx);\n }\n case \"custom\": {\n return false;\n }\n case \"transform\": {\n return true;\n }\n case \"pipe\": {\n return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);\n }\n case \"success\": {\n return false;\n }\n case \"catch\": {\n return false;\n }\n case \"function\": {\n return false;\n }\n default:\n def;\n }\n throw new Error(`Unknown schema type: ${def.type}`);\n}\n","export {};\n","import * as core from \"../core/index.js\";\nimport * as schemas from \"./schemas.js\";\nexport const ZodISODateTime = /*@__PURE__*/ core.$constructor(\"ZodISODateTime\", (inst, def) => {\n core.$ZodISODateTime.init(inst, def);\n schemas.ZodStringFormat.init(inst, def);\n});\nexport function datetime(params) {\n return core._isoDateTime(ZodISODateTime, params);\n}\nexport const ZodISODate = /*@__PURE__*/ core.$constructor(\"ZodISODate\", (inst, def) => {\n core.$ZodISODate.init(inst, def);\n schemas.ZodStringFormat.init(inst, def);\n});\nexport function date(params) {\n return core._isoDate(ZodISODate, params);\n}\nexport const ZodISOTime = /*@__PURE__*/ core.$constructor(\"ZodISOTime\", (inst, def) => {\n core.$ZodISOTime.init(inst, def);\n schemas.ZodStringFormat.init(inst, def);\n});\nexport function time(params) {\n return core._isoTime(ZodISOTime, params);\n}\nexport const ZodISODuration = /*@__PURE__*/ core.$constructor(\"ZodISODuration\", (inst, def) => {\n core.$ZodISODuration.init(inst, def);\n schemas.ZodStringFormat.init(inst, def);\n});\nexport function duration(params) {\n return core._isoDuration(ZodISODuration, params);\n}\n","import * as core from \"../core/index.js\";\nimport { $ZodError } from \"../core/index.js\";\nimport * as util from \"../core/util.js\";\nconst initializer = (inst, issues) => {\n $ZodError.init(inst, issues);\n inst.name = \"ZodError\";\n Object.defineProperties(inst, {\n format: {\n value: (mapper) => core.formatError(inst, mapper),\n // enumerable: false,\n },\n flatten: {\n value: (mapper) => core.flattenError(inst, mapper),\n // enumerable: false,\n },\n addIssue: {\n value: (issue) => {\n inst.issues.push(issue);\n inst.message = JSON.stringify(inst.issues, util.jsonStringifyReplacer, 2);\n },\n // enumerable: false,\n },\n addIssues: {\n value: (issues) => {\n inst.issues.push(...issues);\n inst.message = JSON.stringify(inst.issues, util.jsonStringifyReplacer, 2);\n },\n // enumerable: false,\n },\n isEmpty: {\n get() {\n return inst.issues.length === 0;\n },\n // enumerable: false,\n },\n });\n // Object.defineProperty(inst, \"isEmpty\", {\n // get() {\n // return inst.issues.length === 0;\n // },\n // });\n};\nexport const ZodError = core.$constructor(\"ZodError\", initializer);\nexport const ZodRealError = core.$constructor(\"ZodError\", initializer, {\n Parent: Error,\n});\n// /** @deprecated Use `z.core.$ZodErrorMapCtx` instead. */\n// export type ErrorMapCtx = core.$ZodErrorMapCtx;\n","import * as core from \"../core/index.js\";\nimport { ZodRealError } from \"./errors.js\";\nexport const parse = /* @__PURE__ */ core._parse(ZodRealError);\nexport const parseAsync = /* @__PURE__ */ core._parseAsync(ZodRealError);\nexport const safeParse = /* @__PURE__ */ core._safeParse(ZodRealError);\nexport const safeParseAsync = /* @__PURE__ */ core._safeParseAsync(ZodRealError);\n// Codec functions\nexport const encode = /* @__PURE__ */ core._encode(ZodRealError);\nexport const decode = /* @__PURE__ */ core._decode(ZodRealError);\nexport const encodeAsync = /* @__PURE__ */ core._encodeAsync(ZodRealError);\nexport const decodeAsync = /* @__PURE__ */ core._decodeAsync(ZodRealError);\nexport const safeEncode = /* @__PURE__ */ core._safeEncode(ZodRealError);\nexport const safeDecode = /* @__PURE__ */ core._safeDecode(ZodRealError);\nexport const safeEncodeAsync = /* @__PURE__ */ core._safeEncodeAsync(ZodRealError);\nexport const safeDecodeAsync = /* @__PURE__ */ core._safeDecodeAsync(ZodRealError);\n","import * as core from \"../core/index.js\";\nimport { util } from \"../core/index.js\";\nimport * as checks from \"./checks.js\";\nimport * as iso from \"./iso.js\";\nimport * as parse from \"./parse.js\";\nexport const ZodType = /*@__PURE__*/ core.$constructor(\"ZodType\", (inst, def) => {\n core.$ZodType.init(inst, def);\n inst.def = def;\n inst.type = def.type;\n Object.defineProperty(inst, \"_def\", { value: def });\n // base methods\n inst.check = (...checks) => {\n return inst.clone({\n ...def,\n checks: [\n ...(def.checks ?? []),\n ...checks.map((ch) => typeof ch === \"function\" ? { _zod: { check: ch, def: { check: \"custom\" }, onattach: [] } } : ch),\n ],\n }\n // { parent: true }\n );\n };\n inst.clone = (def, params) => core.clone(inst, def, params);\n inst.brand = () => inst;\n inst.register = ((reg, meta) => {\n reg.add(inst, meta);\n return inst;\n });\n // parsing\n inst.parse = (data, params) => parse.parse(inst, data, params, { callee: inst.parse });\n inst.safeParse = (data, params) => parse.safeParse(inst, data, params);\n inst.parseAsync = async (data, params) => parse.parseAsync(inst, data, params, { callee: inst.parseAsync });\n inst.safeParseAsync = async (data, params) => parse.safeParseAsync(inst, data, params);\n inst.spa = inst.safeParseAsync;\n // encoding/decoding\n inst.encode = (data, params) => parse.encode(inst, data, params);\n inst.decode = (data, params) => parse.decode(inst, data, params);\n inst.encodeAsync = async (data, params) => parse.encodeAsync(inst, data, params);\n inst.decodeAsync = async (data, params) => parse.decodeAsync(inst, data, params);\n inst.safeEncode = (data, params) => parse.safeEncode(inst, data, params);\n inst.safeDecode = (data, params) => parse.safeDecode(inst, data, params);\n inst.safeEncodeAsync = async (data, params) => parse.safeEncodeAsync(inst, data, params);\n inst.safeDecodeAsync = async (data, params) => parse.safeDecodeAsync(inst, data, params);\n // refinements\n inst.refine = (check, params) => inst.check(refine(check, params));\n inst.superRefine = (refinement) => inst.check(superRefine(refinement));\n inst.overwrite = (fn) => inst.check(checks.overwrite(fn));\n // wrappers\n inst.optional = () => optional(inst);\n inst.nullable = () => nullable(inst);\n inst.nullish = () => optional(nullable(inst));\n inst.nonoptional = (params) => nonoptional(inst, params);\n inst.array = () => array(inst);\n inst.or = (arg) => union([inst, arg]);\n inst.and = (arg) => intersection(inst, arg);\n inst.transform = (tx) => pipe(inst, transform(tx));\n inst.default = (def) => _default(inst, def);\n inst.prefault = (def) => prefault(inst, def);\n // inst.coalesce = (def, params) => coalesce(inst, def, params);\n inst.catch = (params) => _catch(inst, params);\n inst.pipe = (target) => pipe(inst, target);\n inst.readonly = () => readonly(inst);\n // meta\n inst.describe = (description) => {\n const cl = inst.clone();\n core.globalRegistry.add(cl, { description });\n return cl;\n };\n Object.defineProperty(inst, \"description\", {\n get() {\n return core.globalRegistry.get(inst)?.description;\n },\n configurable: true,\n });\n inst.meta = (...args) => {\n if (args.length === 0) {\n return core.globalRegistry.get(inst);\n }\n const cl = inst.clone();\n core.globalRegistry.add(cl, args[0]);\n return cl;\n };\n // helpers\n inst.isOptional = () => inst.safeParse(undefined).success;\n inst.isNullable = () => inst.safeParse(null).success;\n return inst;\n});\n/** @internal */\nexport const _ZodString = /*@__PURE__*/ core.$constructor(\"_ZodString\", (inst, def) => {\n core.$ZodString.init(inst, def);\n ZodType.init(inst, def);\n const bag = inst._zod.bag;\n inst.format = bag.format ?? null;\n inst.minLength = bag.minimum ?? null;\n inst.maxLength = bag.maximum ?? null;\n // validations\n inst.regex = (...args) => inst.check(checks.regex(...args));\n inst.includes = (...args) => inst.check(checks.includes(...args));\n inst.startsWith = (...args) => inst.check(checks.startsWith(...args));\n inst.endsWith = (...args) => inst.check(checks.endsWith(...args));\n inst.min = (...args) => inst.check(checks.minLength(...args));\n inst.max = (...args) => inst.check(checks.maxLength(...args));\n inst.length = (...args) => inst.check(checks.length(...args));\n inst.nonempty = (...args) => inst.check(checks.minLength(1, ...args));\n inst.lowercase = (params) => inst.check(checks.lowercase(params));\n inst.uppercase = (params) => inst.check(checks.uppercase(params));\n // transforms\n inst.trim = () => inst.check(checks.trim());\n inst.normalize = (...args) => inst.check(checks.normalize(...args));\n inst.toLowerCase = () => inst.check(checks.toLowerCase());\n inst.toUpperCase = () => inst.check(checks.toUpperCase());\n});\nexport const ZodString = /*@__PURE__*/ core.$constructor(\"ZodString\", (inst, def) => {\n core.$ZodString.init(inst, def);\n _ZodString.init(inst, def);\n inst.email = (params) => inst.check(core._email(ZodEmail, params));\n inst.url = (params) => inst.check(core._url(ZodURL, params));\n inst.jwt = (params) => inst.check(core._jwt(ZodJWT, params));\n inst.emoji = (params) => inst.check(core._emoji(ZodEmoji, params));\n inst.guid = (params) => inst.check(core._guid(ZodGUID, params));\n inst.uuid = (params) => inst.check(core._uuid(ZodUUID, params));\n inst.uuidv4 = (params) => inst.check(core._uuidv4(ZodUUID, params));\n inst.uuidv6 = (params) => inst.check(core._uuidv6(ZodUUID, params));\n inst.uuidv7 = (params) => inst.check(core._uuidv7(ZodUUID, params));\n inst.nanoid = (params) => inst.check(core._nanoid(ZodNanoID, params));\n inst.guid = (params) => inst.check(core._guid(ZodGUID, params));\n inst.cuid = (params) => inst.check(core._cuid(ZodCUID, params));\n inst.cuid2 = (params) => inst.check(core._cuid2(ZodCUID2, params));\n inst.ulid = (params) => inst.check(core._ulid(ZodULID, params));\n inst.base64 = (params) => inst.check(core._base64(ZodBase64, params));\n inst.base64url = (params) => inst.check(core._base64url(ZodBase64URL, params));\n inst.xid = (params) => inst.check(core._xid(ZodXID, params));\n inst.ksuid = (params) => inst.check(core._ksuid(ZodKSUID, params));\n inst.ipv4 = (params) => inst.check(core._ipv4(ZodIPv4, params));\n inst.ipv6 = (params) => inst.check(core._ipv6(ZodIPv6, params));\n inst.cidrv4 = (params) => inst.check(core._cidrv4(ZodCIDRv4, params));\n inst.cidrv6 = (params) => inst.check(core._cidrv6(ZodCIDRv6, params));\n inst.e164 = (params) => inst.check(core._e164(ZodE164, params));\n // iso\n inst.datetime = (params) => inst.check(iso.datetime(params));\n inst.date = (params) => inst.check(iso.date(params));\n inst.time = (params) => inst.check(iso.time(params));\n inst.duration = (params) => inst.check(iso.duration(params));\n});\nexport function string(params) {\n return core._string(ZodString, params);\n}\nexport const ZodStringFormat = /*@__PURE__*/ core.$constructor(\"ZodStringFormat\", (inst, def) => {\n core.$ZodStringFormat.init(inst, def);\n _ZodString.init(inst, def);\n});\nexport const ZodEmail = /*@__PURE__*/ core.$constructor(\"ZodEmail\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodEmail.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function email(params) {\n return core._email(ZodEmail, params);\n}\nexport const ZodGUID = /*@__PURE__*/ core.$constructor(\"ZodGUID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodGUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function guid(params) {\n return core._guid(ZodGUID, params);\n}\nexport const ZodUUID = /*@__PURE__*/ core.$constructor(\"ZodUUID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodUUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function uuid(params) {\n return core._uuid(ZodUUID, params);\n}\nexport function uuidv4(params) {\n return core._uuidv4(ZodUUID, params);\n}\n// ZodUUIDv6\nexport function uuidv6(params) {\n return core._uuidv6(ZodUUID, params);\n}\n// ZodUUIDv7\nexport function uuidv7(params) {\n return core._uuidv7(ZodUUID, params);\n}\nexport const ZodURL = /*@__PURE__*/ core.$constructor(\"ZodURL\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodURL.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function url(params) {\n return core._url(ZodURL, params);\n}\nexport function httpUrl(params) {\n return core._url(ZodURL, {\n protocol: /^https?$/,\n hostname: core.regexes.domain,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodEmoji = /*@__PURE__*/ core.$constructor(\"ZodEmoji\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodEmoji.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function emoji(params) {\n return core._emoji(ZodEmoji, params);\n}\nexport const ZodNanoID = /*@__PURE__*/ core.$constructor(\"ZodNanoID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodNanoID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function nanoid(params) {\n return core._nanoid(ZodNanoID, params);\n}\nexport const ZodCUID = /*@__PURE__*/ core.$constructor(\"ZodCUID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodCUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function cuid(params) {\n return core._cuid(ZodCUID, params);\n}\nexport const ZodCUID2 = /*@__PURE__*/ core.$constructor(\"ZodCUID2\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodCUID2.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function cuid2(params) {\n return core._cuid2(ZodCUID2, params);\n}\nexport const ZodULID = /*@__PURE__*/ core.$constructor(\"ZodULID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodULID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function ulid(params) {\n return core._ulid(ZodULID, params);\n}\nexport const ZodXID = /*@__PURE__*/ core.$constructor(\"ZodXID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodXID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function xid(params) {\n return core._xid(ZodXID, params);\n}\nexport const ZodKSUID = /*@__PURE__*/ core.$constructor(\"ZodKSUID\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodKSUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function ksuid(params) {\n return core._ksuid(ZodKSUID, params);\n}\nexport const ZodIPv4 = /*@__PURE__*/ core.$constructor(\"ZodIPv4\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodIPv4.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function ipv4(params) {\n return core._ipv4(ZodIPv4, params);\n}\nexport const ZodIPv6 = /*@__PURE__*/ core.$constructor(\"ZodIPv6\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodIPv6.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function ipv6(params) {\n return core._ipv6(ZodIPv6, params);\n}\nexport const ZodCIDRv4 = /*@__PURE__*/ core.$constructor(\"ZodCIDRv4\", (inst, def) => {\n core.$ZodCIDRv4.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function cidrv4(params) {\n return core._cidrv4(ZodCIDRv4, params);\n}\nexport const ZodCIDRv6 = /*@__PURE__*/ core.$constructor(\"ZodCIDRv6\", (inst, def) => {\n core.$ZodCIDRv6.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function cidrv6(params) {\n return core._cidrv6(ZodCIDRv6, params);\n}\nexport const ZodBase64 = /*@__PURE__*/ core.$constructor(\"ZodBase64\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodBase64.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function base64(params) {\n return core._base64(ZodBase64, params);\n}\nexport const ZodBase64URL = /*@__PURE__*/ core.$constructor(\"ZodBase64URL\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodBase64URL.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function base64url(params) {\n return core._base64url(ZodBase64URL, params);\n}\nexport const ZodE164 = /*@__PURE__*/ core.$constructor(\"ZodE164\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodE164.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function e164(params) {\n return core._e164(ZodE164, params);\n}\nexport const ZodJWT = /*@__PURE__*/ core.$constructor(\"ZodJWT\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodJWT.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function jwt(params) {\n return core._jwt(ZodJWT, params);\n}\nexport const ZodCustomStringFormat = /*@__PURE__*/ core.$constructor(\"ZodCustomStringFormat\", (inst, def) => {\n // ZodStringFormat.init(inst, def);\n core.$ZodCustomStringFormat.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nexport function stringFormat(format, fnOrRegex, _params = {}) {\n return core._stringFormat(ZodCustomStringFormat, format, fnOrRegex, _params);\n}\nexport function hostname(_params) {\n return core._stringFormat(ZodCustomStringFormat, \"hostname\", core.regexes.hostname, _params);\n}\nexport function hex(_params) {\n return core._stringFormat(ZodCustomStringFormat, \"hex\", core.regexes.hex, _params);\n}\nexport function hash(alg, params) {\n const enc = params?.enc ?? \"hex\";\n const format = `${alg}_${enc}`;\n const regex = core.regexes[format];\n if (!regex)\n throw new Error(`Unrecognized hash format: ${format}`);\n return core._stringFormat(ZodCustomStringFormat, format, regex, params);\n}\nexport const ZodNumber = /*@__PURE__*/ core.$constructor(\"ZodNumber\", (inst, def) => {\n core.$ZodNumber.init(inst, def);\n ZodType.init(inst, def);\n inst.gt = (value, params) => inst.check(checks.gt(value, params));\n inst.gte = (value, params) => inst.check(checks.gte(value, params));\n inst.min = (value, params) => inst.check(checks.gte(value, params));\n inst.lt = (value, params) => inst.check(checks.lt(value, params));\n inst.lte = (value, params) => inst.check(checks.lte(value, params));\n inst.max = (value, params) => inst.check(checks.lte(value, params));\n inst.int = (params) => inst.check(int(params));\n inst.safe = (params) => inst.check(int(params));\n inst.positive = (params) => inst.check(checks.gt(0, params));\n inst.nonnegative = (params) => inst.check(checks.gte(0, params));\n inst.negative = (params) => inst.check(checks.lt(0, params));\n inst.nonpositive = (params) => inst.check(checks.lte(0, params));\n inst.multipleOf = (value, params) => inst.check(checks.multipleOf(value, params));\n inst.step = (value, params) => inst.check(checks.multipleOf(value, params));\n // inst.finite = (params) => inst.check(core.finite(params));\n inst.finite = () => inst;\n const bag = inst._zod.bag;\n inst.minValue =\n Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null;\n inst.maxValue =\n Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null;\n inst.isInt = (bag.format ?? \"\").includes(\"int\") || Number.isSafeInteger(bag.multipleOf ?? 0.5);\n inst.isFinite = true;\n inst.format = bag.format ?? null;\n});\nexport function number(params) {\n return core._number(ZodNumber, params);\n}\nexport const ZodNumberFormat = /*@__PURE__*/ core.$constructor(\"ZodNumberFormat\", (inst, def) => {\n core.$ZodNumberFormat.init(inst, def);\n ZodNumber.init(inst, def);\n});\nexport function int(params) {\n return core._int(ZodNumberFormat, params);\n}\nexport function float32(params) {\n return core._float32(ZodNumberFormat, params);\n}\nexport function float64(params) {\n return core._float64(ZodNumberFormat, params);\n}\nexport function int32(params) {\n return core._int32(ZodNumberFormat, params);\n}\nexport function uint32(params) {\n return core._uint32(ZodNumberFormat, params);\n}\nexport const ZodBoolean = /*@__PURE__*/ core.$constructor(\"ZodBoolean\", (inst, def) => {\n core.$ZodBoolean.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function boolean(params) {\n return core._boolean(ZodBoolean, params);\n}\nexport const ZodBigInt = /*@__PURE__*/ core.$constructor(\"ZodBigInt\", (inst, def) => {\n core.$ZodBigInt.init(inst, def);\n ZodType.init(inst, def);\n inst.gte = (value, params) => inst.check(checks.gte(value, params));\n inst.min = (value, params) => inst.check(checks.gte(value, params));\n inst.gt = (value, params) => inst.check(checks.gt(value, params));\n inst.gte = (value, params) => inst.check(checks.gte(value, params));\n inst.min = (value, params) => inst.check(checks.gte(value, params));\n inst.lt = (value, params) => inst.check(checks.lt(value, params));\n inst.lte = (value, params) => inst.check(checks.lte(value, params));\n inst.max = (value, params) => inst.check(checks.lte(value, params));\n inst.positive = (params) => inst.check(checks.gt(BigInt(0), params));\n inst.negative = (params) => inst.check(checks.lt(BigInt(0), params));\n inst.nonpositive = (params) => inst.check(checks.lte(BigInt(0), params));\n inst.nonnegative = (params) => inst.check(checks.gte(BigInt(0), params));\n inst.multipleOf = (value, params) => inst.check(checks.multipleOf(value, params));\n const bag = inst._zod.bag;\n inst.minValue = bag.minimum ?? null;\n inst.maxValue = bag.maximum ?? null;\n inst.format = bag.format ?? null;\n});\nexport function bigint(params) {\n return core._bigint(ZodBigInt, params);\n}\nexport const ZodBigIntFormat = /*@__PURE__*/ core.$constructor(\"ZodBigIntFormat\", (inst, def) => {\n core.$ZodBigIntFormat.init(inst, def);\n ZodBigInt.init(inst, def);\n});\n// int64\nexport function int64(params) {\n return core._int64(ZodBigIntFormat, params);\n}\n// uint64\nexport function uint64(params) {\n return core._uint64(ZodBigIntFormat, params);\n}\nexport const ZodSymbol = /*@__PURE__*/ core.$constructor(\"ZodSymbol\", (inst, def) => {\n core.$ZodSymbol.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function symbol(params) {\n return core._symbol(ZodSymbol, params);\n}\nexport const ZodUndefined = /*@__PURE__*/ core.$constructor(\"ZodUndefined\", (inst, def) => {\n core.$ZodUndefined.init(inst, def);\n ZodType.init(inst, def);\n});\nfunction _undefined(params) {\n return core._undefined(ZodUndefined, params);\n}\nexport { _undefined as undefined };\nexport const ZodNull = /*@__PURE__*/ core.$constructor(\"ZodNull\", (inst, def) => {\n core.$ZodNull.init(inst, def);\n ZodType.init(inst, def);\n});\nfunction _null(params) {\n return core._null(ZodNull, params);\n}\nexport { _null as null };\nexport const ZodAny = /*@__PURE__*/ core.$constructor(\"ZodAny\", (inst, def) => {\n core.$ZodAny.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function any() {\n return core._any(ZodAny);\n}\nexport const ZodUnknown = /*@__PURE__*/ core.$constructor(\"ZodUnknown\", (inst, def) => {\n core.$ZodUnknown.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function unknown() {\n return core._unknown(ZodUnknown);\n}\nexport const ZodNever = /*@__PURE__*/ core.$constructor(\"ZodNever\", (inst, def) => {\n core.$ZodNever.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function never(params) {\n return core._never(ZodNever, params);\n}\nexport const ZodVoid = /*@__PURE__*/ core.$constructor(\"ZodVoid\", (inst, def) => {\n core.$ZodVoid.init(inst, def);\n ZodType.init(inst, def);\n});\nfunction _void(params) {\n return core._void(ZodVoid, params);\n}\nexport { _void as void };\nexport const ZodDate = /*@__PURE__*/ core.$constructor(\"ZodDate\", (inst, def) => {\n core.$ZodDate.init(inst, def);\n ZodType.init(inst, def);\n inst.min = (value, params) => inst.check(checks.gte(value, params));\n inst.max = (value, params) => inst.check(checks.lte(value, params));\n const c = inst._zod.bag;\n inst.minDate = c.minimum ? new Date(c.minimum) : null;\n inst.maxDate = c.maximum ? new Date(c.maximum) : null;\n});\nexport function date(params) {\n return core._date(ZodDate, params);\n}\nexport const ZodArray = /*@__PURE__*/ core.$constructor(\"ZodArray\", (inst, def) => {\n core.$ZodArray.init(inst, def);\n ZodType.init(inst, def);\n inst.element = def.element;\n inst.min = (minLength, params) => inst.check(checks.minLength(minLength, params));\n inst.nonempty = (params) => inst.check(checks.minLength(1, params));\n inst.max = (maxLength, params) => inst.check(checks.maxLength(maxLength, params));\n inst.length = (len, params) => inst.check(checks.length(len, params));\n inst.unwrap = () => inst.element;\n});\nexport function array(element, params) {\n return core._array(ZodArray, element, params);\n}\n// .keyof\nexport function keyof(schema) {\n const shape = schema._zod.def.shape;\n return _enum(Object.keys(shape));\n}\nexport const ZodObject = /*@__PURE__*/ core.$constructor(\"ZodObject\", (inst, def) => {\n core.$ZodObjectJIT.init(inst, def);\n ZodType.init(inst, def);\n util.defineLazy(inst, \"shape\", () => def.shape);\n inst.keyof = () => _enum(Object.keys(inst._zod.def.shape));\n inst.catchall = (catchall) => inst.clone({ ...inst._zod.def, catchall: catchall });\n inst.passthrough = () => inst.clone({ ...inst._zod.def, catchall: unknown() });\n inst.loose = () => inst.clone({ ...inst._zod.def, catchall: unknown() });\n inst.strict = () => inst.clone({ ...inst._zod.def, catchall: never() });\n inst.strip = () => inst.clone({ ...inst._zod.def, catchall: undefined });\n inst.extend = (incoming) => {\n return util.extend(inst, incoming);\n };\n inst.safeExtend = (incoming) => {\n return util.safeExtend(inst, incoming);\n };\n inst.merge = (other) => util.merge(inst, other);\n inst.pick = (mask) => util.pick(inst, mask);\n inst.omit = (mask) => util.omit(inst, mask);\n inst.partial = (...args) => util.partial(ZodOptional, inst, args[0]);\n inst.required = (...args) => util.required(ZodNonOptional, inst, args[0]);\n});\nexport function object(shape, params) {\n const def = {\n type: \"object\",\n get shape() {\n util.assignProp(this, \"shape\", shape ? util.objectClone(shape) : {});\n return this.shape;\n },\n ...util.normalizeParams(params),\n };\n return new ZodObject(def);\n}\n// strictObject\nexport function strictObject(shape, params) {\n return new ZodObject({\n type: \"object\",\n get shape() {\n util.assignProp(this, \"shape\", util.objectClone(shape));\n return this.shape;\n },\n catchall: never(),\n ...util.normalizeParams(params),\n });\n}\n// looseObject\nexport function looseObject(shape, params) {\n return new ZodObject({\n type: \"object\",\n get shape() {\n util.assignProp(this, \"shape\", util.objectClone(shape));\n return this.shape;\n },\n catchall: unknown(),\n ...util.normalizeParams(params),\n });\n}\nexport const ZodUnion = /*@__PURE__*/ core.$constructor(\"ZodUnion\", (inst, def) => {\n core.$ZodUnion.init(inst, def);\n ZodType.init(inst, def);\n inst.options = def.options;\n});\nexport function union(options, params) {\n return new ZodUnion({\n type: \"union\",\n options: options,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodDiscriminatedUnion = /*@__PURE__*/ core.$constructor(\"ZodDiscriminatedUnion\", (inst, def) => {\n ZodUnion.init(inst, def);\n core.$ZodDiscriminatedUnion.init(inst, def);\n});\nexport function discriminatedUnion(discriminator, options, params) {\n // const [options, params] = args;\n return new ZodDiscriminatedUnion({\n type: \"union\",\n options,\n discriminator,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodIntersection = /*@__PURE__*/ core.$constructor(\"ZodIntersection\", (inst, def) => {\n core.$ZodIntersection.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function intersection(left, right) {\n return new ZodIntersection({\n type: \"intersection\",\n left: left,\n right: right,\n });\n}\nexport const ZodTuple = /*@__PURE__*/ core.$constructor(\"ZodTuple\", (inst, def) => {\n core.$ZodTuple.init(inst, def);\n ZodType.init(inst, def);\n inst.rest = (rest) => inst.clone({\n ...inst._zod.def,\n rest: rest,\n });\n});\nexport function tuple(items, _paramsOrRest, _params) {\n const hasRest = _paramsOrRest instanceof core.$ZodType;\n const params = hasRest ? _params : _paramsOrRest;\n const rest = hasRest ? _paramsOrRest : null;\n return new ZodTuple({\n type: \"tuple\",\n items: items,\n rest,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodRecord = /*@__PURE__*/ core.$constructor(\"ZodRecord\", (inst, def) => {\n core.$ZodRecord.init(inst, def);\n ZodType.init(inst, def);\n inst.keyType = def.keyType;\n inst.valueType = def.valueType;\n});\nexport function record(keyType, valueType, params) {\n return new ZodRecord({\n type: \"record\",\n keyType,\n valueType: valueType,\n ...util.normalizeParams(params),\n });\n}\n// type alksjf = core.output;\nexport function partialRecord(keyType, valueType, params) {\n const k = core.clone(keyType);\n k._zod.values = undefined;\n return new ZodRecord({\n type: \"record\",\n keyType: k,\n valueType: valueType,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodMap = /*@__PURE__*/ core.$constructor(\"ZodMap\", (inst, def) => {\n core.$ZodMap.init(inst, def);\n ZodType.init(inst, def);\n inst.keyType = def.keyType;\n inst.valueType = def.valueType;\n});\nexport function map(keyType, valueType, params) {\n return new ZodMap({\n type: \"map\",\n keyType: keyType,\n valueType: valueType,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodSet = /*@__PURE__*/ core.$constructor(\"ZodSet\", (inst, def) => {\n core.$ZodSet.init(inst, def);\n ZodType.init(inst, def);\n inst.min = (...args) => inst.check(core._minSize(...args));\n inst.nonempty = (params) => inst.check(core._minSize(1, params));\n inst.max = (...args) => inst.check(core._maxSize(...args));\n inst.size = (...args) => inst.check(core._size(...args));\n});\nexport function set(valueType, params) {\n return new ZodSet({\n type: \"set\",\n valueType: valueType,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodEnum = /*@__PURE__*/ core.$constructor(\"ZodEnum\", (inst, def) => {\n core.$ZodEnum.init(inst, def);\n ZodType.init(inst, def);\n inst.enum = def.entries;\n inst.options = Object.values(def.entries);\n const keys = new Set(Object.keys(def.entries));\n inst.extract = (values, params) => {\n const newEntries = {};\n for (const value of values) {\n if (keys.has(value)) {\n newEntries[value] = def.entries[value];\n }\n else\n throw new Error(`Key ${value} not found in enum`);\n }\n return new ZodEnum({\n ...def,\n checks: [],\n ...util.normalizeParams(params),\n entries: newEntries,\n });\n };\n inst.exclude = (values, params) => {\n const newEntries = { ...def.entries };\n for (const value of values) {\n if (keys.has(value)) {\n delete newEntries[value];\n }\n else\n throw new Error(`Key ${value} not found in enum`);\n }\n return new ZodEnum({\n ...def,\n checks: [],\n ...util.normalizeParams(params),\n entries: newEntries,\n });\n };\n});\nfunction _enum(values, params) {\n const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;\n return new ZodEnum({\n type: \"enum\",\n entries,\n ...util.normalizeParams(params),\n });\n}\nexport { _enum as enum };\n/** @deprecated This API has been merged into `z.enum()`. Use `z.enum()` instead.\n *\n * ```ts\n * enum Colors { red, green, blue }\n * z.enum(Colors);\n * ```\n */\nexport function nativeEnum(entries, params) {\n return new ZodEnum({\n type: \"enum\",\n entries,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodLiteral = /*@__PURE__*/ core.$constructor(\"ZodLiteral\", (inst, def) => {\n core.$ZodLiteral.init(inst, def);\n ZodType.init(inst, def);\n inst.values = new Set(def.values);\n Object.defineProperty(inst, \"value\", {\n get() {\n if (def.values.length > 1) {\n throw new Error(\"This schema contains multiple valid literal values. Use `.values` instead.\");\n }\n return def.values[0];\n },\n });\n});\nexport function literal(value, params) {\n return new ZodLiteral({\n type: \"literal\",\n values: Array.isArray(value) ? value : [value],\n ...util.normalizeParams(params),\n });\n}\nexport const ZodFile = /*@__PURE__*/ core.$constructor(\"ZodFile\", (inst, def) => {\n core.$ZodFile.init(inst, def);\n ZodType.init(inst, def);\n inst.min = (size, params) => inst.check(core._minSize(size, params));\n inst.max = (size, params) => inst.check(core._maxSize(size, params));\n inst.mime = (types, params) => inst.check(core._mime(Array.isArray(types) ? types : [types], params));\n});\nexport function file(params) {\n return core._file(ZodFile, params);\n}\nexport const ZodTransform = /*@__PURE__*/ core.$constructor(\"ZodTransform\", (inst, def) => {\n core.$ZodTransform.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n if (_ctx.direction === \"backward\") {\n throw new core.$ZodEncodeError(inst.constructor.name);\n }\n payload.addIssue = (issue) => {\n if (typeof issue === \"string\") {\n payload.issues.push(util.issue(issue, payload.value, def));\n }\n else {\n // for Zod 3 backwards compatibility\n const _issue = issue;\n if (_issue.fatal)\n _issue.continue = false;\n _issue.code ?? (_issue.code = \"custom\");\n _issue.input ?? (_issue.input = payload.value);\n _issue.inst ?? (_issue.inst = inst);\n // _issue.continue ??= true;\n payload.issues.push(util.issue(_issue));\n }\n };\n const output = def.transform(payload.value, payload);\n if (output instanceof Promise) {\n return output.then((output) => {\n payload.value = output;\n return payload;\n });\n }\n payload.value = output;\n return payload;\n };\n});\nexport function transform(fn) {\n return new ZodTransform({\n type: \"transform\",\n transform: fn,\n });\n}\nexport const ZodOptional = /*@__PURE__*/ core.$constructor(\"ZodOptional\", (inst, def) => {\n core.$ZodOptional.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function optional(innerType) {\n return new ZodOptional({\n type: \"optional\",\n innerType: innerType,\n });\n}\nexport const ZodNullable = /*@__PURE__*/ core.$constructor(\"ZodNullable\", (inst, def) => {\n core.$ZodNullable.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function nullable(innerType) {\n return new ZodNullable({\n type: \"nullable\",\n innerType: innerType,\n });\n}\n// nullish\nexport function nullish(innerType) {\n return optional(nullable(innerType));\n}\nexport const ZodDefault = /*@__PURE__*/ core.$constructor(\"ZodDefault\", (inst, def) => {\n core.$ZodDefault.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n inst.removeDefault = inst.unwrap;\n});\nexport function _default(innerType, defaultValue) {\n return new ZodDefault({\n type: \"default\",\n innerType: innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : util.shallowClone(defaultValue);\n },\n });\n}\nexport const ZodPrefault = /*@__PURE__*/ core.$constructor(\"ZodPrefault\", (inst, def) => {\n core.$ZodPrefault.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function prefault(innerType, defaultValue) {\n return new ZodPrefault({\n type: \"prefault\",\n innerType: innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : util.shallowClone(defaultValue);\n },\n });\n}\nexport const ZodNonOptional = /*@__PURE__*/ core.$constructor(\"ZodNonOptional\", (inst, def) => {\n core.$ZodNonOptional.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function nonoptional(innerType, params) {\n return new ZodNonOptional({\n type: \"nonoptional\",\n innerType: innerType,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodSuccess = /*@__PURE__*/ core.$constructor(\"ZodSuccess\", (inst, def) => {\n core.$ZodSuccess.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function success(innerType) {\n return new ZodSuccess({\n type: \"success\",\n innerType: innerType,\n });\n}\nexport const ZodCatch = /*@__PURE__*/ core.$constructor(\"ZodCatch\", (inst, def) => {\n core.$ZodCatch.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n inst.removeCatch = inst.unwrap;\n});\nfunction _catch(innerType, catchValue) {\n return new ZodCatch({\n type: \"catch\",\n innerType: innerType,\n catchValue: (typeof catchValue === \"function\" ? catchValue : () => catchValue),\n });\n}\nexport { _catch as catch };\nexport const ZodNaN = /*@__PURE__*/ core.$constructor(\"ZodNaN\", (inst, def) => {\n core.$ZodNaN.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function nan(params) {\n return core._nan(ZodNaN, params);\n}\nexport const ZodPipe = /*@__PURE__*/ core.$constructor(\"ZodPipe\", (inst, def) => {\n core.$ZodPipe.init(inst, def);\n ZodType.init(inst, def);\n inst.in = def.in;\n inst.out = def.out;\n});\nexport function pipe(in_, out) {\n return new ZodPipe({\n type: \"pipe\",\n in: in_,\n out: out,\n // ...util.normalizeParams(params),\n });\n}\nexport const ZodCodec = /*@__PURE__*/ core.$constructor(\"ZodCodec\", (inst, def) => {\n ZodPipe.init(inst, def);\n core.$ZodCodec.init(inst, def);\n});\nexport function codec(in_, out, params) {\n return new ZodCodec({\n type: \"pipe\",\n in: in_,\n out: out,\n transform: params.decode,\n reverseTransform: params.encode,\n });\n}\nexport const ZodReadonly = /*@__PURE__*/ core.$constructor(\"ZodReadonly\", (inst, def) => {\n core.$ZodReadonly.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function readonly(innerType) {\n return new ZodReadonly({\n type: \"readonly\",\n innerType: innerType,\n });\n}\nexport const ZodTemplateLiteral = /*@__PURE__*/ core.$constructor(\"ZodTemplateLiteral\", (inst, def) => {\n core.$ZodTemplateLiteral.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function templateLiteral(parts, params) {\n return new ZodTemplateLiteral({\n type: \"template_literal\",\n parts,\n ...util.normalizeParams(params),\n });\n}\nexport const ZodLazy = /*@__PURE__*/ core.$constructor(\"ZodLazy\", (inst, def) => {\n core.$ZodLazy.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.getter();\n});\nexport function lazy(getter) {\n return new ZodLazy({\n type: \"lazy\",\n getter: getter,\n });\n}\nexport const ZodPromise = /*@__PURE__*/ core.$constructor(\"ZodPromise\", (inst, def) => {\n core.$ZodPromise.init(inst, def);\n ZodType.init(inst, def);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nexport function promise(innerType) {\n return new ZodPromise({\n type: \"promise\",\n innerType: innerType,\n });\n}\nexport const ZodFunction = /*@__PURE__*/ core.$constructor(\"ZodFunction\", (inst, def) => {\n core.$ZodFunction.init(inst, def);\n ZodType.init(inst, def);\n});\nexport function _function(params) {\n return new ZodFunction({\n type: \"function\",\n input: Array.isArray(params?.input) ? tuple(params?.input) : (params?.input ?? array(unknown())),\n output: params?.output ?? unknown(),\n });\n}\nexport { _function as function };\nexport const ZodCustom = /*@__PURE__*/ core.$constructor(\"ZodCustom\", (inst, def) => {\n core.$ZodCustom.init(inst, def);\n ZodType.init(inst, def);\n});\n// custom checks\nexport function check(fn) {\n const ch = new core.$ZodCheck({\n check: \"custom\",\n // ...util.normalizeParams(params),\n });\n ch._zod.check = fn;\n return ch;\n}\nexport function custom(fn, _params) {\n return core._custom(ZodCustom, fn ?? (() => true), _params);\n}\nexport function refine(fn, _params = {}) {\n return core._refine(ZodCustom, fn, _params);\n}\n// superRefine\nexport function superRefine(fn) {\n return core._superRefine(fn);\n}\nfunction _instanceof(cls, params = {\n error: `Input not instance of ${cls.name}`,\n}) {\n const inst = new ZodCustom({\n type: \"custom\",\n check: \"custom\",\n fn: (data) => data instanceof cls,\n abort: true,\n ...util.normalizeParams(params),\n });\n inst._zod.bag.Class = cls;\n return inst;\n}\nexport { _instanceof as instanceof };\n// stringbool\nexport const stringbool = (...args) => core._stringbool({\n Codec: ZodCodec,\n Boolean: ZodBoolean,\n String: ZodString,\n}, ...args);\nexport function json(params) {\n const jsonSchema = lazy(() => {\n return union([string(params), number(), boolean(), _null(), array(jsonSchema), record(string(), jsonSchema)]);\n });\n return jsonSchema;\n}\n// preprocess\n// /** @deprecated Use `z.pipe()` and `z.transform()` instead. */\nexport function preprocess(fn, schema) {\n return pipe(transform(fn), schema);\n}\n","// Zod 3 compat layer\nimport * as core from \"../core/index.js\";\n/** @deprecated Use the raw string literal codes instead, e.g. \"invalid_type\". */\nexport const ZodIssueCode = {\n invalid_type: \"invalid_type\",\n too_big: \"too_big\",\n too_small: \"too_small\",\n invalid_format: \"invalid_format\",\n not_multiple_of: \"not_multiple_of\",\n unrecognized_keys: \"unrecognized_keys\",\n invalid_union: \"invalid_union\",\n invalid_key: \"invalid_key\",\n invalid_element: \"invalid_element\",\n invalid_value: \"invalid_value\",\n custom: \"custom\",\n};\nexport { $brand, config } from \"../core/index.js\";\n/** @deprecated Use `z.config(params)` instead. */\nexport function setErrorMap(map) {\n core.config({\n customError: map,\n });\n}\n/** @deprecated Use `z.config()` instead. */\nexport function getErrorMap() {\n return core.config().customError;\n}\n/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */\nexport var ZodFirstPartyTypeKind;\n(function (ZodFirstPartyTypeKind) {\n})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));\n","import * as core from \"../core/index.js\";\nimport * as schemas from \"./schemas.js\";\nexport function string(params) {\n return core._coercedString(schemas.ZodString, params);\n}\nexport function number(params) {\n return core._coercedNumber(schemas.ZodNumber, params);\n}\nexport function boolean(params) {\n return core._coercedBoolean(schemas.ZodBoolean, params);\n}\nexport function bigint(params) {\n return core._coercedBigint(schemas.ZodBigInt, params);\n}\nexport function date(params) {\n return core._coercedDate(schemas.ZodDate, params);\n}\n","import { z } from \"zod\";\nexport function tool(input) {\n return input;\n}\ntool.schema = z;\n","import {\n AGY_CLIENT_ID,\n AGY_CLIENT_SECRET,\n AGY_PROVIDER_ID\n} from '../constants';\nimport { agyFetch } from '../fetch';\nimport { formatRefreshParts, parseRefreshParts } from './auth';\nimport { clearCachedAuth, storeCachedAuth } from './cache';\nimport { invalidateProjectContextCache } from './project';\nimport {\n DEFAULT_MAX_ATTEMPTS,\n getExponentialDelayWithJitter,\n isRetryableNetworkError,\n isRetryableStatus,\n resolveRetryDelayMs,\n wait\n} from '../sdk/retry/helpers';\nimport type { OAuthAuthDetails, PluginClient, RefreshParts } from './types';\n\ninterface OAuthErrorPayload {\n error?:\n | string\n | {\n code?: string;\n status?: string;\n message?: string;\n };\n error_description?: string;\n}\n\n/**\n * NOTE: Special Design - Concurrent Refresh Lock Mechanism\n * When the IDE starts, or multiple requests concurrently call the Agy service and detect the current Access Token has expired,\n * a Token refresh process is triggered. If each concurrent request independently sends a refresh_token request to the Google API, it causes:\n * 1. Redundant network requests;\n * 2. \"Race conditions\" (a later refresh invalidates an earlier one, causing other concurrent requests to fail).\n *\n * Here, the `refreshInFlight` map is used to store ongoing refresh Promises. For the same refresh token,\n * only one actual refresh network request is made. Other concurrent requests wait on the shared Promise until it succeeds,\n * avoiding race conditions and greatly improving plugin robustness under high concurrency.\n */\nconst refreshInFlight = new Map>();\n\nfunction parseOAuthErrorPayload(text: string | undefined): { code?: string; description?: string } {\n if (!text) {\n return {};\n }\n\n try {\n const payload = JSON.parse(text) as OAuthErrorPayload;\n if (!payload || typeof payload !== 'object') {\n return { description: text };\n }\n\n let code: string | undefined;\n if (typeof payload.error === 'string') {\n code = payload.error;\n } else if (payload.error && typeof payload.error === 'object') {\n code = payload.error.status ?? payload.error.code;\n if (!payload.error_description && payload.error.message) {\n return { code, description: payload.error.message };\n }\n }\n\n const description = payload.error_description;\n if (description) {\n return { code, description };\n }\n\n if (payload.error && typeof payload.error === 'object' && payload.error.message) {\n return { code, description: payload.error.message };\n }\n\n return { code };\n } catch {\n return { description: text };\n }\n}\n\nexport async function refreshAccessToken(\n auth: OAuthAuthDetails,\n client: PluginClient\n): Promise {\n const parts = parseRefreshParts(auth.refresh);\n if (!parts.refreshToken) {\n return undefined;\n }\n\n const pending = refreshInFlight.get(parts.refreshToken);\n if (pending) {\n return pending;\n }\n\n const refreshPromise = refreshAccessTokenInternal(auth, client, parts);\n refreshInFlight.set(parts.refreshToken, refreshPromise);\n\n try {\n return await refreshPromise;\n } finally {\n refreshInFlight.delete(parts.refreshToken);\n }\n}\n\nasync function refreshAccessTokenInternal(\n auth: OAuthAuthDetails,\n client: PluginClient,\n parts: RefreshParts\n): Promise {\n try {\n const response = await fetchTokenRefresh(parts.refreshToken);\n\n if (!response.ok) {\n let errorText: string | undefined;\n try {\n errorText = await response.text();\n } catch {\n errorText = undefined;\n }\n\n const { code, description } = parseOAuthErrorPayload(errorText);\n const details = [code, description ?? errorText].filter(Boolean).join(': ');\n const baseMessage = `Antigravity token refresh failed (${response.status} ${response.statusText})`;\n console.warn(`[OAuth] ${details ? `${baseMessage} - ${details}` : baseMessage}`);\n\n if (code === 'invalid_grant') {\n console.warn(\n `[OAuth] Google revoked the stored refresh token. Run \\`opencode auth login\\` and reauthenticate.`\n );\n clearCachedAuth(auth.refresh);\n invalidateProjectContextCache(auth.refresh);\n try {\n const clearedAuth = {\n type: 'oauth' as const,\n refresh: formatRefreshParts({\n refreshToken: '',\n projectId: parts.projectId,\n managedProjectId: parts.managedProjectId\n }),\n access: '',\n expires: 0\n } as OAuthAuthDetails;\n await client.auth.set({\n path: { id: AGY_PROVIDER_ID },\n body: clearedAuth\n });\n } catch (storeError) {\n const errStr = storeError instanceof Error ? storeError.stack || storeError.message : String(storeError);\n console.warn(`[Agy Auth] Failed to clear stored Antigravity OAuth credentials: ${errStr}`);\n }\n }\n\n return undefined;\n }\n\n const payload = (await response.json()) as {\n access_token: string;\n expires_in: number;\n refresh_token?: string;\n };\n\n const refreshedParts: RefreshParts = {\n refreshToken: payload.refresh_token ?? parts.refreshToken,\n projectId: parts.projectId,\n managedProjectId: parts.managedProjectId\n };\n\n const updatedAuth: OAuthAuthDetails = {\n ...auth,\n access: payload.access_token,\n expires: Date.now() + payload.expires_in * 1000,\n refresh: formatRefreshParts(refreshedParts)\n };\n\n clearCachedAuth(auth.refresh);\n storeCachedAuth(updatedAuth);\n invalidateProjectContextCache(auth.refresh);\n\n if (refreshedParts.refreshToken !== parts.refreshToken) {\n try {\n await client.auth.set({\n path: { id: AGY_PROVIDER_ID },\n body: updatedAuth\n });\n } catch (storeError) {\n const errStr = storeError instanceof Error ? storeError.stack || storeError.message : String(storeError);\n console.warn(`[Agy Auth] Failed to persist refreshed Antigravity OAuth credentials: ${errStr}`);\n }\n }\n\n return updatedAuth;\n } catch (error) {\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to refresh Antigravity access token due to an unexpected error: ${errStr}`);\n return undefined;\n }\n}\n\nasync function fetchTokenRefresh(refreshToken: string): Promise {\n const tokenUrl = 'https://oauth2.googleapis.com/token';\n const clientId = AGY_CLIENT_ID;\n const clientSecret = AGY_CLIENT_SECRET;\n\n const init: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret\n })\n };\n\n let attempt = 1;\n while (attempt <= DEFAULT_MAX_ATTEMPTS) {\n try {\n const response = await agyFetch(tokenUrl, init);\n if (!isRetryableStatus(response.status) || attempt >= DEFAULT_MAX_ATTEMPTS) {\n return response;\n }\n\n const delayMs = await resolveRetryDelayMs(response, attempt);\n if (delayMs > 0) {\n await wait(delayMs);\n }\n attempt += 1;\n continue;\n } catch (error) {\n if (attempt >= DEFAULT_MAX_ATTEMPTS || !isRetryableNetworkError(error)) {\n throw error;\n }\n await wait(getExponentialDelayWithJitter(attempt));\n attempt += 1;\n }\n }\n\n return agyFetch(tokenUrl, init);\n}\n","export function clamp(value: number, min: number, max: number): number {\n if (Number.isNaN(value)) {\n return min;\n }\n if (value < min) {\n return min;\n }\n if (value > max) {\n return max;\n }\n return value;\n}\n\nexport function pad(value: string, width: number): string {\n if (value.length >= width) {\n return value;\n }\n return value.padEnd(width, \" \");\n}\n\nexport function buildProgressBar(fraction: number, width = 20): string {\n const clamped = clamp(fraction, 0, 1);\n const filled = clamped >= 1\n ? width\n : Math.max(0, Math.min(width, Math.max(clamped > 0 ? 1 : 0, Math.floor(clamped * width))));\n const empty = width - filled;\n return `${\"▓\".repeat(filled)}${\"░\".repeat(empty)}`;\n}\n\nexport function formatRemainingAmount(value: string | undefined): string | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n return value;\n }\n return parsed.toLocaleString(\"en-US\");\n}\n\nexport function formatRelativeResetTime(resetTime: string | undefined): string | undefined {\n if (!resetTime) {\n return undefined;\n }\n\n const resetAt = new Date(resetTime).getTime();\n if (Number.isNaN(resetAt)) {\n return undefined;\n }\n\n const diffMs = resetAt - Date.now();\n if (diffMs <= 0) {\n return \"reset pending\";\n }\n\n const totalMinutes = Math.ceil(diffMs / (1000 * 60));\n const hours = Math.floor(totalMinutes / 60);\n const minutes = totalMinutes % 60;\n\n if (hours > 0 && minutes > 0) {\n return `resets in ${hours}h ${minutes}m`;\n }\n if (hours > 0) {\n return `resets in ${hours}h`;\n }\n return `resets in ${minutes}m`;\n}\n","import { tool } from \"@opencode-ai/plugin\";\nimport { accessTokenExpired, isOAuthAuth, parseRefreshParts } from \"./auth\";\nimport { resolveCachedAuth } from \"./cache\";\nimport { ensureProjectContext, retrieveUserQuota } from \"./project\";\nimport type { RetrieveUserQuotaBucket } from \"./project/types\";\nimport { refreshAccessToken } from \"./token\";\nimport type { GetAuth, PluginClient } from \"./types\";\nimport { buildProgressBar, clamp, formatRemainingAmount, formatRelativeResetTime, pad } from \"./quota-utils\";\n\nexport const AGY_QUOTA_TOOL_NAME = \"agy_quota\";\nexport const AGY_V2_QUOTA_COMMAND = \"agy-quota\";\nexport const AGY_V2_QUOTA_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota usage for the current authenticated account.\n\nImmediately call \\`${AGY_QUOTA_TOOL_NAME}\\` with no arguments and return its output verbatim.\nDo not call other tools.\n`;\n\ninterface AgyQuotaToolDependencies {\n client: PluginClient;\n getAuthResolver: () => GetAuth | undefined;\n getConfiguredProjectId: () => string | undefined;\n getUserAgentModel: () => string | undefined;\n}\n\nexport function createAgyQuotaTool({\n client,\n getAuthResolver,\n getConfiguredProjectId,\n getUserAgentModel,\n}: AgyQuotaToolDependencies) {\n return tool({\n description:\n \"Retrieve current Agy Code Assist quota usage for the authenticated user and project.\",\n args: {},\n async execute() {\n const getAuth = getAuthResolver();\n if (!getAuth) {\n return \"Agy quota is unavailable before Google auth is initialized. Authenticate with the Google provider and retry.\";\n }\n\n const auth = await getAuth();\n if (!isOAuthAuth(auth)) {\n return \"Agy quota requires OAuth with Google. Run `opencode auth login` and choose `Google OAuth (Antigravity CLI)` or `Google OAuth (Gemini CLI)`.\";\n }\n\n let authRecord = resolveCachedAuth(auth);\n if (accessTokenExpired(authRecord)) {\n const refreshed = await refreshAccessToken(authRecord, client);\n if (!refreshed?.access) {\n return \"Agy quota lookup failed because the access token could not be refreshed. Re-authenticate and retry.\";\n }\n authRecord = refreshed;\n }\n\n if (!authRecord.access) {\n return \"Agy quota lookup failed because no access token is available. Re-authenticate and retry.\";\n }\n\n try {\n const projectContext = await ensureProjectContext(\n authRecord,\n client,\n getConfiguredProjectId(),\n getUserAgentModel(),\n );\n if (!projectContext.effectiveProjectId) {\n return \"Agy quota lookup failed because no Google Cloud project could be resolved.\";\n }\n\n const quota = await retrieveUserQuota(\n authRecord.access,\n projectContext.effectiveProjectId,\n getUserAgentModel(),\n );\n if (!quota?.buckets?.length) {\n return `No Agy quota buckets were returned for project \\`${projectContext.effectiveProjectId}\\`.`;\n }\n\n return formatAgyQuotaOutput(\n projectContext.effectiveProjectId,\n quota.buckets,\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : \"unknown error\";\n return `Agy quota lookup failed: ${message}`;\n }\n },\n });\n}\n\nfunction formatAgyQuotaOutput(\n projectId: string,\n buckets: RetrieveUserQuotaBucket[],\n): string {\n const sortedBuckets = [...buckets].sort(compareQuotaBuckets);\n const groupedRows = groupQuotaRows(sortedBuckets);\n const versionGroups = groupByVersion(groupedRows);\n const variantWidth = Math.max(\n \"Variant\".length,\n ...versionGroups.flatMap((group) =>\n group.models.flatMap((model) => model.rows.map((row) => row.variant.length))\n ),\n );\n const tokenTypeValues = [...new Set(versionGroups.flatMap((group) =>\n group.models.flatMap((model) => model.rows.map((row) => row.tokenType))\n ))];\n const showTokenType = tokenTypeValues.length > 1 || tokenTypeValues[0] !== \"REQUESTS\";\n const lines = [\n `Agy quota usage for project \\`${projectId}\\``,\n \"\",\n showTokenType\n ? ` ↳ ${pad(\"Variant\", variantWidth)} Remaining Reset Type`\n : ` ↳ ${pad(\"Variant\", variantWidth)} Remaining Reset`,\n ];\n\n for (let index = 0; index < versionGroups.length; index += 1) {\n const versionGroup = versionGroups[index];\n if (!versionGroup) {\n continue;\n }\n if (index > 0) {\n lines.push(\"\");\n }\n lines.push(formatVersionGroupTitle(versionGroup));\n for (const model of versionGroup.models) {\n lines.push(model.baseModel);\n for (const row of model.rows) {\n lines.push(\n showTokenType\n ? ` ↳ ${pad(row.variant, variantWidth)} ${pad(row.usageRemaining, 27)} ${pad(row.resetValue, 8)} ${row.tokenType}`\n : ` ↳ ${pad(row.variant, variantWidth)} ${pad(row.usageRemaining, 27)} ${row.resetValue}`,\n );\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n\nfunction compareQuotaBuckets(\n left: RetrieveUserQuotaBucket,\n right: RetrieveUserQuotaBucket,\n): number {\n const leftModel = left.modelId ?? \"\";\n const rightModel = right.modelId ?? \"\";\n if (leftModel !== rightModel) {\n return leftModel.localeCompare(rightModel);\n }\n\n const leftTokenType = left.tokenType ?? \"\";\n const rightTokenType = right.tokenType ?? \"\";\n if (leftTokenType !== rightTokenType) {\n return leftTokenType.localeCompare(rightTokenType);\n }\n\n return (left.resetTime ?? \"\").localeCompare(right.resetTime ?? \"\");\n}\n\nfunction formatUsageRemaining(bucket: RetrieveUserQuotaBucket): string {\n const remainingAmount = formatRemainingAmount(bucket.remainingAmount);\n const remainingFraction = bucket.remainingFraction;\n const hasFraction =\n typeof remainingFraction === \"number\" && Number.isFinite(remainingFraction);\n\n if (hasFraction) {\n const clamped = clamp(remainingFraction, 0, 1);\n const percent = (clamped * 100).toFixed(1);\n const bar = buildProgressBar(clamped);\n return remainingAmount\n ? `${bar} ${percent}% (${remainingAmount} left)`\n : `${bar} ${percent}%`;\n }\n\n if (remainingAmount) {\n return remainingAmount;\n }\n\n return \"unknown\";\n}\n\nfunction normalizeTokenType(bucket: RetrieveUserQuotaBucket): string {\n const value = bucket.tokenType?.trim();\n return value ? value.toUpperCase() : \"REQUESTS\";\n}\n\ninterface GroupedQuotaRow {\n variant: string;\n usageRemaining: string;\n resetValue: string;\n tokenType: string;\n}\n\ninterface GroupedQuotaModel {\n baseModel: string;\n version: string | undefined;\n rows: GroupedQuotaRow[];\n}\n\nfunction groupQuotaRows(sortedBuckets: RetrieveUserQuotaBucket[]): GroupedQuotaModel[] {\n const groups = new Map();\n\n for (const bucket of sortedBuckets) {\n const modelId = bucket.modelId?.trim() || \"unknown-model\";\n const { baseModel, variant } = splitModelVariant(modelId);\n const usageRemaining = formatUsageRemaining(bucket);\n const resetLabel = formatRelativeResetTime(bucket.resetTime);\n const resetValue = resetLabel?.replace(\"resets in \", \"\") ?? \"-\";\n const tokenType = normalizeTokenType(bucket);\n\n const existing = groups.get(baseModel);\n if (existing) {\n existing.rows.push({\n variant,\n usageRemaining,\n resetValue,\n tokenType,\n });\n continue;\n }\n\n groups.set(baseModel, {\n baseModel,\n version: extractModelVersion(baseModel),\n rows: [{\n variant,\n usageRemaining,\n resetValue,\n tokenType,\n }],\n });\n }\n\n return [...groups.values()];\n}\n\ninterface VersionQuotaGroup {\n title: string;\n version: string | undefined;\n models: GroupedQuotaModel[];\n}\n\nfunction groupByVersion(models: GroupedQuotaModel[]): VersionQuotaGroup[] {\n const groups = new Map();\n\n for (const model of models) {\n const key = model.version ?? \"__unknown__\";\n const existing = groups.get(key);\n if (existing) {\n existing.models.push(model);\n continue;\n }\n\n groups.set(key, {\n title: model.version ? `Gemini ${model.version}` : \"Other\",\n version: model.version,\n models: [model],\n });\n }\n\n const ordered = [...groups.values()].sort((left, right) =>\n compareVersionDesc(left.version, right.version),\n );\n\n for (const group of ordered) {\n group.models.sort((left, right) => left.baseModel.localeCompare(right.baseModel));\n }\n\n return ordered;\n}\n\nfunction extractModelVersion(modelId: string): string | undefined {\n const match = modelId.match(/^gemini-([0-9]+(?:\\.[0-9]+)*)-/i);\n return match?.[1];\n}\n\nfunction compareVersionDesc(left: string | undefined, right: string | undefined): number {\n if (!left && !right) {\n return 0;\n }\n if (!left) {\n return 1;\n }\n if (!right) {\n return -1;\n }\n\n const leftSegments = left.split(\".\").map((part) => Number.parseInt(part, 10));\n const rightSegments = right.split(\".\").map((part) => Number.parseInt(part, 10));\n const max = Math.max(leftSegments.length, rightSegments.length);\n\n for (let index = 0; index < max; index += 1) {\n const l = leftSegments[index] ?? 0;\n const r = rightSegments[index] ?? 0;\n if (Number.isNaN(l) || Number.isNaN(r)) {\n break;\n }\n if (l > r) {\n return -1;\n }\n if (l < r) {\n return 1;\n }\n }\n\n return right.localeCompare(left);\n}\n\nfunction formatVersionGroupTitle(group: VersionQuotaGroup): string {\n const modelCount = group.models.length;\n const bucketCount = group.models.reduce((count, model) => count + model.rows.length, 0);\n const modelLabel = modelCount === 1 ? \"model\" : \"models\";\n const bucketLabel = bucketCount === 1 ? \"bucket\" : \"buckets\";\n return `${group.title} (${modelCount} ${modelLabel}, ${bucketCount} ${bucketLabel})`;\n}\n\nfunction splitModelVariant(modelId: string): { baseModel: string; variant: string } {\n const vertexSuffix = \"_vertex\";\n if (modelId.endsWith(vertexSuffix)) {\n return {\n baseModel: modelId.slice(0, -vertexSuffix.length),\n variant: \"vertex\",\n };\n }\n return {\n baseModel: modelId,\n variant: \"default\",\n };\n}\n","import { tool } from \"@opencode-ai/plugin\";\nimport { accessTokenExpired, isOAuthAuth, parseRefreshParts } from \"./auth\";\nimport { resolveCachedAuth } from \"./cache\";\nimport { ensureProjectContext, retrieveUserQuotaSummary } from \"./project\";\nimport type { QuotaSummaryBucket, QuotaSummaryGroup } from \"./project/types\";\nimport { refreshAccessToken } from \"./token\";\nimport type { GetAuth, PluginClient } from \"./types\";\nimport { buildProgressBar, clamp, formatRemainingAmount, formatRelativeResetTime } from \"./quota-utils\";\n\nexport const AGY_QUOTA_SUMMARY_TOOL_NAME = \"agy_quota_summary\";\nexport const AGY_V2_QUOTA_SUMMARY_COMMAND = \"agy-quota-summary\";\nexport const AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota summary (weekly and 5-hour limits by model group) for the current authenticated account.\n\nImmediately call \\`${AGY_QUOTA_SUMMARY_TOOL_NAME}\\` with no arguments and return its output verbatim.\nDo not call other tools.\n`;\n\ninterface AgyQuotaSummaryToolDependencies {\n client: PluginClient;\n getAuthResolver: () => GetAuth | undefined;\n getConfiguredProjectId: () => string | undefined;\n getUserAgentModel: () => string | undefined;\n}\n\nexport function createAgyQuotaSummaryTool({\n client,\n getAuthResolver,\n getConfiguredProjectId,\n getUserAgentModel,\n}: AgyQuotaSummaryToolDependencies) {\n return tool({\n description:\n \"Retrieve Agy Code Assist quota summary with weekly and 5-hour limits grouped by model family.\",\n args: {},\n async execute() {\n const getAuth = getAuthResolver();\n if (!getAuth) {\n return \"Agy quota summary is unavailable before Google auth is initialized. Authenticate with the Google provider and retry.\";\n }\n\n const auth = await getAuth();\n if (!isOAuthAuth(auth)) {\n return \"Agy quota summary requires OAuth with Google. Run `opencode auth login` and choose `Google OAuth (Antigravity CLI)` or `Google OAuth (Gemini CLI)`.\";\n }\n\n let authRecord = resolveCachedAuth(auth);\n if (accessTokenExpired(authRecord)) {\n const refreshed = await refreshAccessToken(authRecord, client);\n if (!refreshed?.access) {\n return \"Agy quota summary lookup failed because the access token could not be refreshed. Re-authenticate and retry.\";\n }\n authRecord = refreshed;\n }\n\n if (!authRecord.access) {\n return \"Agy quota summary lookup failed because no access token is available. Re-authenticate and retry.\";\n }\n\n try {\n const projectContext = await ensureProjectContext(\n authRecord,\n client,\n getConfiguredProjectId(),\n getUserAgentModel(),\n );\n if (!projectContext.effectiveProjectId) {\n return \"Agy quota summary lookup failed because no Google Cloud project could be resolved.\";\n }\n\n const summary = await retrieveUserQuotaSummary(\n authRecord.access,\n projectContext.effectiveProjectId,\n getUserAgentModel(),\n );\n if (!summary) {\n return `No Agy quota summary available for project \\`${projectContext.effectiveProjectId}\\`.`;\n }\n\n return formatAgyQuotaSummaryOutput(\n projectContext.effectiveProjectId,\n summary,\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : \"unknown error\";\n return `Agy quota summary lookup failed: ${message}`;\n }\n },\n });\n}\n\nconst BAR_WIDTH = 50;\n\nfunction windowLabel(window: string | undefined): string {\n switch (window?.toUpperCase()) {\n case \"WEEKLY\":\n return \"Weekly Limit\";\n case \"FIVE_HOUR\":\n return \"Five Hour Limit\";\n default:\n return \"Other Limit\";\n }\n}\n\nfunction formatSummaryBucket(bucket: QuotaSummaryBucket, indent: string): string[] {\n if (bucket.disabled) {\n const defaultDesc = `${windowLabel(bucket.window).toLowerCase()} exhausted`;\n const desc = bucket.description?.trim() || defaultDesc;\n return [`${indent}Disabled: ${desc}`];\n }\n\n const lines: string[] = [];\n const fraction = bucket.remainingFraction;\n const hasFraction = typeof fraction === \"number\" && Number.isFinite(fraction);\n\n if (hasFraction) {\n const clamped = clamp(fraction, 0, 1);\n const percent = (clamped * 100).toFixed(2);\n const bar = buildProgressBar(clamped, BAR_WIDTH);\n lines.push(`${indent}[${bar}] ${percent}%`);\n\n const remaining = formatRemainingAmount(bucket.remainingAmount);\n if (remaining) {\n const pctWhole = (clamped * 100).toFixed(0);\n lines.push(`${indent}${pctWhole}% remaining \\u00b7 ${remaining} left`);\n } else {\n const pctWhole = (clamped * 100).toFixed(0);\n lines.push(`${indent}${pctWhole}% remaining`);\n }\n } else {\n const remaining = formatRemainingAmount(bucket.remainingAmount);\n lines.push(remaining ? `${indent}${remaining} remaining` : `${indent}unknown remaining`);\n }\n\n const resetLabel = formatRelativeResetTime(bucket.resetTime);\n if (resetLabel) {\n const formattedReset = resetLabel.startsWith(\"resets in \")\n ? resetLabel.replace(\"resets in \", \"Refreshes in \")\n : resetLabel.charAt(0).toUpperCase() + resetLabel.slice(1);\n lines.push(`${indent}${formattedReset}`);\n }\n\n return lines;\n}\n\nfunction groupBucketsByWindow(buckets: QuotaSummaryBucket[]): Map {\n const groups = new Map();\n const order = [\"WEEKLY\", \"FIVE_HOUR\"];\n\n for (const bucket of buckets) {\n const key = bucket.window?.toUpperCase() || \"UNKNOWN\";\n const existing = groups.get(key);\n if (existing) {\n existing.push(bucket);\n } else {\n groups.set(key, [bucket]);\n }\n }\n\n const sorted = new Map();\n for (const key of order) {\n const group = groups.get(key);\n if (group) {\n sorted.set(key, group);\n }\n }\n for (const [key, group] of groups) {\n if (!sorted.has(key)) {\n sorted.set(key, group);\n }\n }\n\n return sorted;\n}\n\nfunction formatSummaryGroup(group: QuotaSummaryGroup): string[] {\n const lines: string[] = [];\n const name = group.displayName?.trim();\n if (name) {\n lines.push(name);\n }\n\n const desc = group.description?.trim();\n if (desc) {\n lines.push(` Models within this group: ${desc}`);\n }\n\n const buckets = group.buckets;\n if (buckets?.length) {\n const windowGroups = groupBucketsByWindow(buckets);\n let firstWindow = true;\n for (const [, windowBuckets] of windowGroups) {\n if (!firstWindow) {\n lines.push(\"\");\n }\n for (const bucket of windowBuckets) {\n const baseLabel = windowLabel(bucket.window);\n const label = bucket.displayName ? `${baseLabel} (${bucket.displayName})` : baseLabel;\n lines.push(` ${label}`);\n lines.push(...formatSummaryBucket(bucket, \" \"));\n firstWindow = false;\n }\n }\n }\n\n return lines;\n}\n\nfunction formatTopLevelBuckets(buckets: QuotaSummaryBucket[]): string[] {\n const lines: string[] = [];\n const windowGroups = groupBucketsByWindow(buckets);\n\n let firstWindow = true;\n for (const [, windowBuckets] of windowGroups) {\n if (!firstWindow) {\n lines.push(\"\");\n }\n for (const bucket of windowBuckets) {\n const baseLabel = windowLabel(bucket.window);\n const label = bucket.displayName ? `${baseLabel} (${bucket.displayName})` : baseLabel;\n lines.push(label);\n lines.push(...formatSummaryBucket(bucket, \" \"));\n firstWindow = false;\n }\n }\n\n return lines;\n}\n\nfunction formatAgyQuotaSummaryOutput(\n projectId: string,\n summary: { groups?: QuotaSummaryGroup[]; buckets?: QuotaSummaryBucket[]; description?: string },\n): string {\n const lines = [\n `Agy quota summary for project \\`${projectId}\\``,\n \"\",\n ];\n\n const groups = summary.groups;\n if (groups?.length) {\n for (let i = 0; i < groups.length; i++) {\n const group = groups[i];\n if (!group) {\n continue;\n }\n if (i > 0) {\n lines.push(\"\");\n }\n lines.push(...formatSummaryGroup(group));\n }\n } else if (summary.buckets?.length) {\n lines.push(...formatTopLevelBuckets(summary.buckets));\n } else {\n lines.push(\"No quota information available.\");\n }\n\n return lines.join(\"\\n\");\n}\n","import { classifyQuotaResponse } from \"../sdk/retry/quota\";\nimport type { PluginClient } from \"./types\";\n\nconst MODEL_CAPACITY_TOAST_COOLDOWN_MS = 30_000;\nconst modelCapacityToastCooldownByKey = new Map();\nconst TEST_TOAST_FLAG = \"OPENCODE_AGY_TEST_TOAST\";\nconst testToastShownByProject = new Set();\n\n/**\n * Shows a Toast notification to the user when the server-side Agy model capacity is exhausted.\n */\nexport async function maybeShowAgyCapacityToast(\n client: PluginClient,\n response: Response,\n projectId: string,\n requestedModel?: string,\n): Promise {\n if (response.status !== 429 || !client.tui?.showToast) {\n return;\n }\n\n const quotaContext = await classifyQuotaResponse(response);\n if (quotaContext?.reason !== \"MODEL_CAPACITY_EXHAUSTED\") {\n return;\n }\n\n const model = requestedModel ?? \"the selected model\";\n const toastKey = `${projectId}|${model}|MODEL_CAPACITY_EXHAUSTED`;\n const now = Date.now();\n const cooldownUntil = modelCapacityToastCooldownByKey.get(toastKey) ?? 0;\n if (cooldownUntil > now) {\n return;\n }\n modelCapacityToastCooldownByKey.set(toastKey, now + MODEL_CAPACITY_TOAST_COOLDOWN_MS);\n\n await client.tui.showToast({\n body: {\n title: \"Agy Capacity Unavailable\",\n message: `Google reports temporary server capacity limits for ${model}. Please retry in a few seconds.`,\n variant: \"warning\",\n duration: 7000,\n },\n });\n}\n\n/**\n * Temporary smoke test Toast, only enabled when OPENCODE_AGY_TEST_TOAST=1.\n */\nexport async function maybeShowAgyTestToast(\n client: PluginClient,\n projectId: string,\n): Promise {\n if (process.env[TEST_TOAST_FLAG]?.trim() !== \"1\" || !client.tui?.showToast) {\n return;\n }\n\n const key = projectId || \"global\";\n if (testToastShownByProject.has(key)) {\n return;\n }\n testToastShownByProject.add(key);\n\n await client.tui.showToast({\n body: {\n title: \"Agy Toast Test\",\n message: \"Temporary test toast from opencode-agy-auth.\",\n variant: \"info\",\n duration: 5000,\n },\n });\n}\n","import { randomUUID } from 'node:crypto';\nimport * as os from 'node:os';\nimport { AGY_CODE_ASSIST_ENDPOINT } from '../constants';\nimport { agyFetch } from '../fetch';\nimport { isRetryableStatus, getExponentialDelayWithJitter, wait } from '../sdk/retry/helpers';\nimport { getAgyCliVersion, buildAgyCliUserAgent } from '../sdk/user-agent';\n\nconst MAX_SEND_ATTEMPTS = 2;\n\nasync function sendWithRetry(label: string, url: string, init: RequestInit): Promise {\n for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt++) {\n try {\n const response = await agyFetch(url, init);\n if (response.ok) return;\n if (!isRetryableStatus(response.status) || attempt === MAX_SEND_ATTEMPTS - 1) {\n if (response.status >= 500 && response.status < 600) {\n console.debug(`[Agy] ${label} failed: ${response.status} (transient, suppressed)`);\n } else {\n console.warn(`[Agy] ${label} failed: ${response.status}`);\n }\n return;\n }\n } catch (error) {\n if (attempt === MAX_SEND_ATTEMPTS - 1) {\n console.debug(`[Agy] ${label} failed with network error: ${error} (suppressed)`);\n return;\n }\n }\n await wait(getExponentialDelayWithJitter(attempt + 1));\n }\n}\n\nlet lastTrafficTime = 0;\nconst TRAFFIC_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Simulates the experimental fetch and metric telemetry traffic sent periodically by the official Agy client in the background to prevent API bans or anomaly detection.\n */\nexport function simulateClientBackgroundTraffic(accessToken: string, projectId: string, userAgentModel?: string): void {\n const now = Date.now();\n if (now - lastTrafficTime < TRAFFIC_INTERVAL_MS) {\n return;\n }\n lastTrafficTime = now;\n\n // Asynchronous silent send, never blocking the core generation flow of the editor.\n Promise.all([\n sendListExperiments(accessToken, userAgentModel),\n sendCodeAssistMetrics(accessToken, projectId, userAgentModel),\n sendTrajectoryAnalytics(accessToken, userAgentModel)\n ]).catch((err) => {\n console.warn(`[Agy] Failed to send background traffic: ${err}`);\n });\n}\n\nasync function sendListExperiments(accessToken: string, userAgentModel?: string) {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:listExperiments`;\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n\n await sendWithRetry('listExperiments', url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': userAgent,\n Authorization: `Bearer ${accessToken}`\n },\n body: '{}'\n });\n}\n\nasync function sendCodeAssistMetrics(accessToken: string, projectId: string, userAgentModel?: string) {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:recordCodeAssistMetrics`;\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n\n const plat = os.platform();\n const arch = os.arch();\n\n const platMap: Record = {\n darwin: 'DARWIN',\n linux: 'LINUX',\n win32: 'WINDOWS'\n };\n const archMap: Record = {\n x64: 'AMD64',\n arm64: 'ARM64'\n };\n\n const p = platMap[plat] || 'UNSPECIFIED';\n const a = archMap[arch] || 'UNSPECIFIED';\n const platform = `${p}_${a}`;\n\n const traceId = randomUUID().replace(/-/g, '').slice(0, 16);\n const trajectoryId = randomUUID();\n const requestId = randomUUID();\n\n const body = {\n project: projectId,\n requestId,\n metadata: {\n ideType: 'ANTIGRAVITY',\n ideVersion: getAgyCliVersion(),\n platform\n },\n metrics: [\n {\n timestamp: new Date().toISOString(),\n conversationOffered: {\n status: 'ACTION_STATUS_NO_ERROR',\n traceId,\n streamingLatency: {\n firstMessageLatency: '0.5s',\n totalLatency: '1.0s'\n },\n isAgentic: true,\n initiationMethod: 'AGENT',\n trajectoryId,\n language: 'unspecified'\n }\n }\n ]\n };\n\n await sendWithRetry('recordCodeAssistMetrics', url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': userAgent,\n Authorization: `Bearer ${accessToken}`\n },\n body: JSON.stringify(body)\n });\n}\n\nasync function sendTrajectoryAnalytics(accessToken: string, userAgentModel?: string) {\n const url = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:recordTrajectoryAnalytics`;\n const userAgent = buildAgyCliUserAgent(userAgentModel);\n\n const plat = os.platform();\n const arch = os.arch();\n\n const platMap: Record = {\n darwin: 'DARWIN',\n linux: 'LINUX',\n win32: 'WINDOWS'\n };\n const archMap: Record = {\n x64: 'AMD64',\n arm64: 'ARM64'\n };\n\n const p = platMap[plat] || 'UNSPECIFIED';\n const a = archMap[arch] || 'UNSPECIFIED';\n const platform = `${p}_${a}`;\n\n const body = buildTrajectoryAnalyticsBody(randomUUID(), platform);\n\n await sendWithRetry('recordTrajectoryAnalytics', url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': userAgent,\n Authorization: `Bearer ${accessToken}`\n },\n body: JSON.stringify(body)\n });\n}\n\nexport function buildTrajectoryAnalyticsBody(cascadeId = randomUUID(), platform = 'DARWIN_AMD64') {\n return {\n trajectory: {\n cascadeId,\n executorMetadatas: [\n {\n cascadeConfig: {\n agentApiConfig: {\n enabled: false\n },\n checkpointConfig: {\n checkpointModel: 'MODEL_PLACEHOLDER_M50',\n strategy: 'CHECKPOINT_STRATEGY_SINGLE_PROMPT',\n maxTokenLimit: '128000',\n tokenThreshold: '50000',\n maxOverheadRatio: '0.15',\n movingWindowSize: '1',\n enabled: true,\n maxOutputTokens: '16384',\n useLastPlannerModel: false,\n isSync: false,\n maxUserRequests: 10,\n includeLastUserMessage: false,\n includeConversationLog: true,\n includeRunningTaskSnapshots: true,\n includeSubagentSnapshots: true,\n includeArtifactSnapshots: true,\n retryConfig: {\n maxRetries: 0,\n initialSleepDurationMs: 1000,\n exponentialMultiplier: 2,\n includeErrorFeedback: false\n }\n }\n }\n }\n ]\n },\n mendelExperimentIds: [],\n metadata: {\n ideType: 'ANTIGRAVITY',\n ideVersion: getAgyCliVersion(),\n platform\n },\n startStepIndex: '0'\n };\n}\n","import { randomUUID } from \"node:crypto\";\n\nimport { AGY_CODE_ASSIST_ENDPOINT } from \"../../constants\";\nimport modelsJson from \"../../../models.json\";\nimport { normalizeThinkingConfig } from \"../request-helpers\";\nimport { buildAgyCliUserAgent } from \"../user-agent\";\nimport { normalizeRequestPayloadIdentifiers, normalizeWrappedIdentifiers } from \"./identifiers\";\nimport { addThoughtSignaturesToFunctionCalls, transformOpenAIToolCalls } from \"./openai\";\nimport { isGenerativeLanguageRequest, parseGenerativeLanguageRequest } from \"./shared\";\nimport { getLatestSignature } from \"../../plugin/cache\";\nimport { closeToolLoopForThinking } from \"./thinking\";\nimport { getTurnStateTracker } from \"./turn-state-tracker\";\nimport { getToolMapper, sanitizeToolName, type ToolMapper } from \"./tool-mapper\";\n\nconst STREAM_ACTION = \"streamGenerateContent\";\n\nexport interface ThinkingConfigDefaults {\n provider?: unknown;\n models?: Record;\n}\n\n/**\n * Rewrites OpenAI-style requests into the format for Gemini Code Assist requests.\n */\nexport function prepareAgyRequest(\n input: RequestInfo,\n init: RequestInit | undefined,\n accessToken: string,\n projectId: string,\n thinkingConfigDefaults?: ThinkingConfigDefaults,\n): {\n request: RequestInfo;\n init: RequestInit;\n streaming: boolean;\n requestedModel?: string;\n sessionId?: string;\n} {\n const baseInit: RequestInit = { ...init };\n const headers = new Headers(init?.headers ?? {});\n\n if (!isGenerativeLanguageRequest(input)) {\n return {\n request: input,\n init: { ...baseInit, headers },\n streaming: false,\n };\n }\n\n const requestTarget = parseGenerativeLanguageRequest(input);\n if (!requestTarget) {\n return {\n request: input,\n init: { ...baseInit, headers },\n streaming: false,\n };\n }\n\n headers.set(\"Authorization\", `Bearer ${accessToken}`);\n headers.delete(\"x-api-key\");\n headers.delete(\"x-goog-api-key\");\n\n const { requestedModel: rawModel, effectiveModel, action: rawAction } = requestTarget;\n const streaming = rawAction === STREAM_ACTION;\n\n const transformedUrl = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:${rawAction}${\n streaming ? \"?alt=sse\" : \"\"\n }`;\n\n let body = baseInit.body;\n let sessionId: string | undefined;\n\n if (typeof baseInit.body === \"string\" && baseInit.body) {\n const transformed = transformRequestBody(\n baseInit.body,\n projectId,\n effectiveModel,\n rawModel,\n thinkingConfigDefaults,\n );\n if (transformed.body) {\n body = transformed.body;\n }\n sessionId = transformed.sessionId;\n }\n\n if (streaming) {\n headers.set(\"Accept\", \"text/event-stream\");\n }\n\n const userAgent = buildAgyCliUserAgent(effectiveModel);\n headers.set(\"User-Agent\", userAgent);\n\n return {\n request: transformedUrl,\n init: {\n ...baseInit,\n headers,\n body,\n },\n streaming,\n requestedModel: rawModel,\n sessionId,\n };\n}\n\nfunction getModelEnum(modelName: string): string {\n const deprecated = (modelsJson as any).deprecatedModelIds;\n if (deprecated && deprecated[modelName] && deprecated[modelName].newModelEnum) {\n return deprecated[modelName].newModelEnum;\n }\n const models = (modelsJson as any).models;\n if (models && models[modelName] && models[modelName].model) {\n return models[modelName].model;\n }\n if (deprecated && deprecated[modelName] && deprecated[modelName].oldModelEnum) {\n return deprecated[modelName].oldModelEnum;\n }\n return \"MODEL_PLACEHOLDER_M16\";\n}\n\nfunction transformRequestBody(\n body: string,\n projectId: string,\n effectiveModel: string,\n requestedModel: string,\n thinkingConfigDefaults?: ThinkingConfigDefaults,\n): { body?: string; userPromptId: string; sessionId?: string } {\n const fallbackId = randomUUID();\n try {\n const parsedBody = JSON.parse(body) as Record;\n const isWrapped = typeof parsedBody.project === \"string\" && \"request\" in parsedBody;\n\n if (isWrapped) {\n const wrappedBody = {\n ...parsedBody,\n model: effectiveModel,\n } as Record;\n\n const wrappedModel = (wrappedBody.model as string) || \"\";\n if (wrappedModel.includes(\"-image\") && !wrappedBody.requestType) {\n wrappedBody.requestType = \"image_gen\";\n wrappedBody.userAgent = wrappedBody.userAgent || \"antigravity\";\n }\n\n const { userPromptId, sessionId, requestId } = normalizeWrappedIdentifiers(wrappedBody);\n const toolMapper = getToolMapper(sessionId);\n\n const requestPayloadInside = wrappedBody.request as Record | undefined;\n if (requestPayloadInside) {\n toolMapper.registerFromFunctionDeclarations(requestPayloadInside.tools);\n toolMapper.registerFromContents(requestPayloadInside.contents);\n\n normalizeThinking(\n requestPayloadInside,\n resolveDefaultThinkingConfig(thinkingConfigDefaults, requestedModel, effectiveModel),\n thinkingConfigDefaults?.provider,\n );\n }\n if (requestPayloadInside && !requestPayloadInside.labels) {\n requestPayloadInside.labels = {\n last_execution_id: randomUUID(),\n last_step_index: \"0\",\n model_enum: getModelEnum(effectiveModel),\n trajectory_id: randomUUID(),\n used_claude: \"false\",\n used_claude_conservative: \"false\"\n };\n }\n\n if (requestPayloadInside && Array.isArray(requestPayloadInside.tools)) {\n normalizeToolSchemaTypes(requestPayloadInside.tools, toolMapper);\n }\n if (requestPayloadInside) {\n normalizeToolConfig(requestPayloadInside, toolMapper);\n }\n if (requestPayloadInside && Array.isArray(requestPayloadInside.contents)) {\n let contents = requestPayloadInside.contents;\n\n normalizeToolNamesInContents(contents, toolMapper);\n injectMissingToolCallIds(contents);\n fixOrphanedFunctionResponses(contents);\n\n const tracker = getTurnStateTracker();\n let needsRecovery = false;\n if (sessionId && tracker) {\n const state = tracker.getState(sessionId) ?? tracker.recoverFromContents(sessionId, contents);\n needsRecovery = state.inToolLoop && !state.turnHasThinking;\n }\n if (needsRecovery) {\n contents = closeToolLoopForThinking(contents);\n }\n\n contents = normalizeContentsSequence(contents);\n contents = ensureTrailingUserTurn(contents);\n\n const latestSig = getLatestSignature(sessionId);\n applyLatestSignature(contents, latestSig);\n requestPayloadInside.contents = contents;\n }\n\n return { body: JSON.stringify(wrappedBody), userPromptId, sessionId };\n }\n\n const requestPayload = { ...parsedBody };\n const { userPromptId, sessionId, requestId } = normalizeRequestPayloadIdentifiers(requestPayload);\n const toolMapper = getToolMapper(sessionId);\n\n toolMapper.registerFromOpenAITools(requestPayload.tools);\n toolMapper.registerFromFunctionDeclarations(requestPayload.tools);\n toolMapper.registerFromContents(requestPayload.contents);\n\n if (Array.isArray(requestPayload.tools)) {\n normalizeToolSchemaTypes(requestPayload.tools, toolMapper);\n }\n normalizeToolConfig(requestPayload, toolMapper);\n transformOpenAIToolCalls(requestPayload, toolMapper);\n addThoughtSignaturesToFunctionCalls(requestPayload);\n normalizeThinking(\n requestPayload,\n resolveDefaultThinkingConfig(thinkingConfigDefaults, requestedModel, effectiveModel),\n thinkingConfigDefaults?.provider,\n );\n normalizeSystemInstruction(requestPayload);\n normalizeCachedContent(requestPayload);\n\n let contents = requestPayload.contents as any[];\n if (Array.isArray(contents)) {\n normalizeToolNamesInContents(contents, toolMapper);\n injectMissingToolCallIds(contents);\n fixOrphanedFunctionResponses(contents);\n\n const tracker = getTurnStateTracker();\n let needsRecovery = false;\n if (sessionId && tracker) {\n const state = tracker.getState(sessionId) ?? tracker.recoverFromContents(sessionId, contents);\n needsRecovery = state.inToolLoop && !state.turnHasThinking;\n }\n if (needsRecovery) {\n contents = closeToolLoopForThinking(contents);\n }\n\n contents = normalizeContentsSequence(contents);\n contents = ensureTrailingUserTurn(contents);\n\n const latestSig = getLatestSignature(sessionId);\n applyLatestSignature(contents, latestSig);\n requestPayload.contents = contents;\n }\n\n if (\"model\" in requestPayload) {\n delete requestPayload.model;\n }\n\n if (!requestPayload.labels) {\n requestPayload.labels = {\n last_execution_id: randomUUID(),\n last_step_index: \"0\",\n model_enum: getModelEnum(effectiveModel),\n trajectory_id: randomUUID(),\n used_claude: \"false\",\n used_claude_conservative: \"false\"\n };\n }\n\n const isImageGen =\n effectiveModel.includes(\"-image\") ||\n requestedModel.includes(\"-image\") ||\n (typeof requestPayload.generationConfig === \"object\" &&\n requestPayload.generationConfig !== null &&\n \"imageConfig\" in (requestPayload.generationConfig as Record));\n\n const wrappedBody: Record = {\n project: projectId,\n model: effectiveModel,\n requestId,\n request: requestPayload,\n userAgent: \"antigravity\"\n };\n\n if (isImageGen) {\n wrappedBody.requestType = \"image_gen\";\n } else if (\n effectiveModel.includes(\"gemini-3.5-flash-lite\") ||\n effectiveModel.includes(\"gemini-3.1-flash-lite\")\n ) {\n wrappedBody.requestType = \"checkpoint\";\n } else if (effectiveModel.includes(\"gemini-2.5-flash-lite\")) {\n wrappedBody.requestType = \"chat\";\n } else if (effectiveModel.includes(\"-lite\")) {\n wrappedBody.requestType = \"web_search\";\n } else {\n wrappedBody.requestType = \"agent\";\n }\n\n return { body: JSON.stringify(wrappedBody), userPromptId, sessionId };\n } catch (error) {\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to transform Gemini request body: ${errStr}`);\n return { userPromptId: fallbackId };\n }\n}\n\nfunction resolveDefaultThinkingConfig(\n thinkingConfigDefaults: ThinkingConfigDefaults | undefined,\n requestedModel: string,\n effectiveModel: string,\n): unknown {\n const configured = thinkingConfigDefaults?.models\n ? thinkingConfigDefaults.models[requestedModel] ?? thinkingConfigDefaults.models[effectiveModel]\n : undefined;\n if (configured !== undefined) {\n return configured;\n }\n\n return getImplicitThinkingConfigForModel(requestedModel) ?? getImplicitThinkingConfigForModel(effectiveModel);\n}\n\nfunction normalizeThinking(\n requestPayload: Record,\n modelThinkingConfig: unknown,\n providerThinkingConfig: unknown,\n): void {\n const rawGenerationConfig = isRecord(requestPayload.generationConfig)\n ? { ...requestPayload.generationConfig }\n : undefined;\n const mergedThinkingConfig = mergeThinkingConfigs(\n providerThinkingConfig,\n modelThinkingConfig,\n requestPayload.thinkingConfig,\n rawGenerationConfig?.thinkingConfig,\n );\n\n if (Object.prototype.hasOwnProperty.call(requestPayload, \"thinkingConfig\")) {\n delete requestPayload.thinkingConfig;\n }\n\n const normalizedThinkingConfig = normalizeThinkingConfig(mergedThinkingConfig);\n\n if (!normalizedThinkingConfig) {\n if (rawGenerationConfig) {\n requestPayload.generationConfig = rawGenerationConfig;\n }\n return;\n }\n\n requestPayload.generationConfig = {\n ...(rawGenerationConfig ?? {}),\n thinkingConfig: normalizedThinkingConfig,\n };\n}\n\nfunction getImplicitThinkingConfigForModel(modelId: string): unknown {\n const normalizedModelId = modelId.toLowerCase();\n if (!normalizedModelId.startsWith(\"gemini-\") || normalizedModelId.includes(\"image\")) {\n return undefined;\n }\n\n return {\n thinkingBudget: normalizedModelId.endsWith(\"-low\") ? 1000 : 10001,\n includeThoughts: true,\n };\n}\n\nfunction mergeThinkingConfigs(...configs: unknown[]): Record | undefined {\n const merged: Record = {};\n for (const config of configs) {\n const normalized = normalizeThinkingConfig(config);\n if (!normalized) {\n continue;\n }\n for (const [key, value] of Object.entries(normalized)) {\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n }\n return Object.keys(merged).length > 0 ? merged : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction normalizeToolSchemaTypes(tools: unknown, toolMapper?: ToolMapper): void {\n if (!Array.isArray(tools)) return;\n\n const validSchemaKeys = new Set([\n \"type\", \"description\", \"properties\", \"items\",\n \"required\", \"enum\", \"nullable\", \"format\"\n ]);\n\n const sanitizeSchema = (obj: any) => {\n if (!obj || typeof obj !== \"object\") return;\n if (Array.isArray(obj)) {\n obj.forEach(sanitizeSchema);\n return;\n }\n\n if (!obj.type && (Array.isArray(obj.anyOf) || Array.isArray(obj.oneOf))) {\n const arr = obj.anyOf || obj.oneOf;\n if (arr.length > 0 && arr[0] && typeof arr[0].type === \"string\") {\n obj.type = arr[0].type;\n if (arr[0].items) {\n obj.items = arr[0].items;\n }\n } else {\n obj.type = \"STRING\";\n }\n }\n\n if (typeof obj.type === \"string\") {\n const t = obj.type.toLowerCase();\n if ([\"string\", \"number\", \"integer\", \"boolean\", \"array\", \"object\"].includes(t)) {\n obj.type = t.toUpperCase();\n }\n }\n\n if (Array.isArray(obj.enum)) {\n const sanitizedEnum = Array.from(\n new Set(\n obj.enum\n .filter((v: unknown) => v !== null && v !== undefined)\n .map((v: unknown) => String(v))\n )\n );\n if (sanitizedEnum.length > 0) {\n obj.enum = sanitizedEnum;\n // Gemini Protobuf Schema requires enum elements to be strings.\n // If the property was typed as BOOLEAN, adjust type to STRING so Gemini's schema validation accepts it.\n if (obj.type === \"BOOLEAN\") {\n obj.type = \"STRING\";\n }\n } else {\n delete obj.enum;\n }\n } else if (\"enum\" in obj) {\n delete obj.enum;\n }\n\n if (obj.properties && typeof obj.properties === \"object\") {\n Object.values(obj.properties).forEach(sanitizeSchema);\n }\n if (obj.items && typeof obj.items === \"object\") {\n sanitizeSchema(obj.items);\n }\n\n for (const key of Object.keys(obj)) {\n if (!validSchemaKeys.has(key)) {\n delete obj[key];\n }\n }\n };\n\n for (const tool of tools) {\n if (tool && Array.isArray(tool.functionDeclarations)) {\n for (const fn of tool.functionDeclarations) {\n if (fn && typeof fn.name === \"string\") {\n fn.name = toolMapper ? toolMapper.toGemini(fn.name) : sanitizeToolName(fn.name);\n }\n if (fn) {\n if (!fn.parameters) {\n fn.parameters = fn.parameters_json_schema ?? fn.parametersJsonSchema;\n }\n if (!fn.parameters) {\n fn.parameters = { type: \"OBJECT\", properties: {} };\n }\n delete (fn as Record).parameters_json_schema;\n delete (fn as Record).parametersJsonSchema;\n sanitizeSchema(fn.parameters);\n }\n }\n }\n }\n}\n\nfunction normalizeToolConfig(requestPayload: Record, toolMapper: ToolMapper): void {\n const toolConfig = (requestPayload.toolConfig ?? requestPayload.tool_config) as Record | undefined;\n if (!toolConfig || typeof toolConfig !== \"object\") return;\n\n const fnCallingConfig = (toolConfig.functionCallingConfig ?? toolConfig.function_calling_config) as Record | undefined;\n if (!fnCallingConfig || typeof fnCallingConfig !== \"object\") return;\n\n const allowedNames = (fnCallingConfig.allowedFunctionNames ?? fnCallingConfig.allowed_function_names) as string[] | undefined;\n if (Array.isArray(allowedNames)) {\n const mapped = allowedNames.map((name) => (typeof name === \"string\" ? toolMapper.toGemini(name) : name));\n if (fnCallingConfig.allowedFunctionNames) {\n fnCallingConfig.allowedFunctionNames = mapped;\n }\n if (fnCallingConfig.allowed_function_names) {\n fnCallingConfig.allowed_function_names = mapped;\n }\n }\n}\n\nfunction normalizeToolNamesInContents(contents: any[], toolMapper: ToolMapper): void {\n if (!Array.isArray(contents)) return;\n for (const msg of contents) {\n if (!msg || typeof msg !== \"object\" || !Array.isArray(msg.parts)) continue;\n for (const part of msg.parts) {\n if (!part || typeof part !== \"object\") continue;\n if (part.functionCall && typeof part.functionCall.name === \"string\") {\n part.functionCall.name = toolMapper.toGemini(part.functionCall.name);\n }\n if (part.functionResponse && typeof part.functionResponse.name === \"string\") {\n part.functionResponse.name = toolMapper.toGemini(part.functionResponse.name);\n }\n }\n }\n}\n\nfunction normalizeSystemInstruction(requestPayload: Record): void {\n if (\"system_instruction\" in requestPayload) {\n requestPayload.systemInstruction = requestPayload.system_instruction;\n delete requestPayload.system_instruction;\n }\n}\n\nfunction normalizeCachedContent(requestPayload: Record): void {\n const extraBody =\n requestPayload.extra_body && typeof requestPayload.extra_body === \"object\"\n ? (requestPayload.extra_body as Record)\n : undefined;\n const cachedContentFromExtra = extraBody?.cached_content ?? extraBody?.cachedContent;\n const cachedContent =\n (requestPayload.cached_content as string | undefined) ??\n (requestPayload.cachedContent as string | undefined) ??\n (cachedContentFromExtra as string | undefined);\n\n if (cachedContent) {\n requestPayload.cachedContent = cachedContent;\n }\n\n delete requestPayload.cached_content;\n if (!extraBody) {\n return;\n }\n\n delete extraBody.cached_content;\n delete extraBody.cachedContent;\n if (Object.keys(extraBody).length === 0) {\n delete requestPayload.extra_body;\n }\n}\n\n\n\nexport function ensureTrailingUserTurn(contents: any[]): any[] {\n if (!Array.isArray(contents) || contents.length === 0) {\n return contents;\n }\n const last = contents[contents.length - 1];\n if (last && (last.role === \"model\" || last.role === \"assistant\")) {\n return [...contents, { role: \"user\", parts: [{ text: \"[Continue]\" }] }];\n }\n return contents;\n}\n\nfunction normalizeContentsSequence(contents: any[]): any[] {\n const merged: any[] = [];\n for (const msg of contents) {\n if (!msg || !msg.role || !Array.isArray(msg.parts)) {\n continue;\n }\n const validParts = msg.parts.filter((p: any) => p != null);\n if (validParts.length === 0) {\n continue;\n }\n\n const last = merged[merged.length - 1];\n if (last && last.role === msg.role) {\n last.parts.push(...validParts);\n } else {\n merged.push({ ...msg, parts: validParts });\n }\n }\n return merged;\n}\n\nfunction injectMissingToolCallIds(contents: any[]): void {\n // Map of function name to array of missing IDs we generated for it\n const missingIdsByName = new Map();\n\n for (const content of contents) {\n if (!content || typeof content !== \"object\" || !Array.isArray(content.parts)) {\n continue;\n }\n\n for (const part of content.parts) {\n if (!part || typeof part !== \"object\") {\n continue;\n }\n\n if (part.functionCall && typeof part.functionCall.name === \"string\") {\n if (!part.functionCall.id) {\n const generatedId = randomUUID();\n part.functionCall.id = generatedId;\n\n let ids = missingIdsByName.get(part.functionCall.name);\n if (!ids) {\n ids = [];\n missingIdsByName.set(part.functionCall.name, ids);\n }\n ids.push(generatedId);\n }\n }\n\n if (part.functionResponse && typeof part.functionResponse.name === \"string\") {\n if (!part.functionResponse.id) {\n const ids = missingIdsByName.get(part.functionResponse.name);\n if (ids && ids.length > 0) {\n part.functionResponse.id = ids.shift();\n } else {\n part.functionResponse.id = randomUUID();\n }\n }\n }\n }\n }\n}\n\nfunction applyLatestSignature(contents: any[], latestSig: string | undefined): void {\n // Collect all function call parts in chronological order\n const allFunctionParts: any[] = [];\n for (const content of contents) {\n if (content && typeof content === \"object\" && Array.isArray(content.parts)) {\n for (const part of content.parts) {\n if (part && typeof part === \"object\" && part.functionCall) {\n // If thoughtSignature was mistakenly placed inside functionCall, migrate or clean it up\n if (part.functionCall.thoughtSignature) {\n if (!part.thoughtSignature || part.thoughtSignature === \"skip_thought_signature_validator\") {\n part.thoughtSignature = part.functionCall.thoughtSignature;\n }\n delete part.functionCall.thoughtSignature;\n }\n allFunctionParts.push(part);\n }\n }\n }\n }\n\n // Only apply the latest signature to the VERY LAST function call part\n if (allFunctionParts.length > 0 && latestSig) {\n const lastPart = allFunctionParts[allFunctionParts.length - 1];\n if (!lastPart.thoughtSignature || lastPart.thoughtSignature === \"skip_thought_signature_validator\") {\n lastPart.thoughtSignature = latestSig;\n }\n }\n}\n\nfunction fixOrphanedFunctionResponses(contents: any[]): void {\n const validCallIds = new Set();\n\n for (const content of contents) {\n if (!content || typeof content !== \"object\" || !Array.isArray(content.parts)) {\n continue;\n }\n for (const part of content.parts) {\n if (part && typeof part === \"object\" && part.functionCall && part.functionCall.id) {\n validCallIds.add(part.functionCall.id);\n }\n }\n }\n\n for (const content of contents) {\n if (!content || typeof content !== \"object\" || !Array.isArray(content.parts)) {\n continue;\n }\n for (const part of content.parts) {\n if (part && typeof part === \"object\" && part.functionResponse) {\n const id = part.functionResponse.id;\n if (!id || !validCallIds.has(id)) {\n const name = part.functionResponse.name || \"unknown_tool\";\n const responseObj = part.functionResponse.response || {};\n\n let responseStr = \"\";\n try {\n responseStr = typeof responseObj === \"string\" ? responseObj : JSON.stringify(responseObj);\n } catch (e) {\n responseStr = String(responseObj);\n }\n\n part.text = `[Orphaned Tool Response for ${name}]: ${responseStr}`;\n delete part.functionResponse;\n }\n }\n }\n }\n}\n","{\n \"agentModelSorts\": [\n {\n \"displayName\": \"Recommended\",\n \"groups\": [\n {\n \"modelIds\": [\n \"gemini-3.8-flash-high\",\n \"gemini-3.8-flash-medium\",\n \"gemini-3.8-flash-low\",\n \"gemini-3.7-flash-high\",\n \"gemini-3.7-flash-medium\",\n \"gemini-3.7-flash-low\",\n \"gemini-3.6-flash-high\",\n \"gemini-3.6-flash-medium\",\n \"gemini-3.6-flash-low\",\n \"gemini-pro-agent\",\n \"gemini-3.1-pro-low\",\n \"claude-sonnet-4-6\",\n \"claude-opus-4-6-thinking\",\n \"gpt-oss-120b-medium\"\n ]\n }\n ]\n }\n ],\n \"audioTranscriptionModelIds\": [\n \"models/proactive-observer-v10\"\n ],\n \"commandModelIds\": [\n \"gemini-3-flash\"\n ],\n \"commitMessageModelIds\": [\n \"gemini-3.5-flash-lite\"\n ],\n \"defaultAgentModelId\": \"gemini-3.8-flash-high\",\n \"deprecatedModelIds\": {\n \"gemini-3.1-pro-high\": {\n \"newModelEnum\": \"MODEL_PLACEHOLDER_M16\",\n \"newModelId\": \"gemini-pro-agent\",\n \"oldModelEnum\": \"MODEL_PLACEHOLDER_M37\"\n }\n },\n \"experimentIds\": [\n 106140402,\n 106519115,\n 105979552,\n 105979574,\n 106015333,\n 106568772,\n 105867471,\n 106123599,\n 106121401,\n 106100625,\n 106143956,\n 105856899,\n 106064030,\n 106264162,\n 106711598,\n 106106760,\n 106021688,\n 105887299,\n 106428370,\n 106283618,\n 106640126,\n 106380926,\n 106723203,\n 106242397,\n 106281951,\n 106264532,\n 106044947,\n 106032303,\n 106228452,\n 106121607,\n 105979531,\n 105979553,\n 106015328,\n 106568390,\n 105867469,\n 106123597,\n 106121399,\n 106100654,\n 106064028,\n 106264150,\n 105906495,\n 106283614,\n 106640124,\n 106242393,\n 106038164,\n 106032301,\n 106121604\n ],\n \"imageGenerationModelIds\": [\n \"gemini-3.1-flash-image\"\n ],\n \"models\": {\n \"chat_20706\": {\n \"addCursorToFindReplaceTarget\": true,\n \"apiProvider\": \"API_PROVIDER_INTERNAL\",\n \"isInternal\": true,\n \"maxTokens\": 16384,\n \"model\": \"MODEL_CHAT_20706\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"promptTemplaterType\": \"PROMPT_TEMPLATER_TYPE_CHATML\",\n \"quotaInfo\": {\n \"remainingFraction\": 1\n },\n \"requiresLeadInGeneration\": true,\n \"supportsCumulativeContext\": true,\n \"supportsEstimateTokenCounter\": true,\n \"tabJumpPrintLineRange\": true,\n \"toolFormatterType\": \"TOOL_FORMATTER_TYPE_XML\"\n },\n \"chat_23310\": {\n \"apiProvider\": \"API_PROVIDER_INTERNAL\",\n \"isInternal\": true,\n \"maxTokens\": 32768,\n \"model\": \"MODEL_CHAT_23310\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"promptTemplaterType\": \"PROMPT_TEMPLATER_TYPE_CHATML\",\n \"quotaInfo\": {\n \"remainingFraction\": 1\n },\n \"requiresLeadInGeneration\": true,\n \"supportsCumulativeContext\": true,\n \"supportsEstimateTokenCounter\": true,\n \"toolFormatterType\": \"TOOL_FORMATTER_TYPE_XML\"\n },\n \"claude-opus-4-6-thinking\": {\n \"apiProvider\": \"API_PROVIDER_ANTHROPIC_VERTEX\",\n \"displayName\": \"Claude Opus 4.6 (Thinking)\",\n \"maxOutputTokens\": 64000,\n \"maxTokens\": 250000,\n \"model\": \"MODEL_PLACEHOLDER_M26\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_UNSPECIFIED\\\",\\n \\\"max_token_limit\\\": \\\"160000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_ANTHROPIC\",\n \"quotaInfo\": {\n \"remainingFraction\": 1,\n \"resetTime\": \"2026-09-25T11:24:59Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"video/jpeg2000\": true,\n \"video/videoframe/jpeg2000\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"thinkingBudget\": 1024,\n \"vertexModelId\": \"claude-opus-4-6@default\"\n },\n \"claude-sonnet-4-6\": {\n \"apiProvider\": \"API_PROVIDER_ANTHROPIC_VERTEX\",\n \"displayName\": \"Claude Sonnet 4.6 (Thinking)\",\n \"maxOutputTokens\": 64000,\n \"maxTokens\": 250000,\n \"model\": \"MODEL_PLACEHOLDER_M35\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_UNSPECIFIED\\\",\\n \\\"max_token_limit\\\": \\\"160000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_ANTHROPIC\",\n \"quotaInfo\": {\n \"remainingFraction\": 1,\n \"resetTime\": \"2026-09-25T11:24:59Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"video/jpeg2000\": true,\n \"video/videoframe/jpeg2000\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"thinkingBudget\": 1024,\n \"vertexModelId\": \"claude-sonnet-4-6@default\"\n },\n \"gemini-2.5-flash\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_GOOGLE_GEMINI_2_5_FLASH\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"gemini-3.5-flash-lite\",\n \"thinkingBudget\": -1,\n \"thinkingLevel\": 3\n },\n \"gemini-2.5-flash-lite\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"gemini-3.5-flash-lite\",\n \"thinkingBudget\": -1,\n \"thinkingLevel\": 3\n },\n \"gemini-2.5-flash-thinking\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"gemini-3.5-flash-lite\",\n \"thinkingBudget\": -1,\n \"thinkingLevel\": 3\n },\n \"gemini-2.5-pro\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 2.5 Pro\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_GOOGLE_GEMINI_2_5_PRO\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"requiresImageOutputOutsideFunctionResponses\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"thinkingBudget\": 1024\n },\n \"gemini-3-flash\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3 Flash\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M18\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn. Ground your response in the work you did. Keep your tone professional and avoid overconfident language, bragging, or overclaiming success.\\n- AVOID using superlatives such as \\\"perfectly\\\", \\\"flawlessly\\\", \\\"100% correct\\\", \\\"Summary of Accomplishments\\\" etc. to summarize your work for the user. Be humble.\\n- AVOID over-the-top politeness or complimenting the user excessively.\\n- Format your responses in github-style markdown.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": -1\n },\n \"gemini-3-flash-agent\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash (High)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M84\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": -1\n },\n \"gemini-3.1-flash-image\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Flash Image\",\n \"model\": \"MODEL_PLACEHOLDER_M21\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n }\n },\n \"gemini-3.1-flash-lite\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"model\": \"MODEL_PLACEHOLDER_M50\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n }\n },\n \"gemini-3.1-pro-high\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Pro (High)\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_PLACEHOLDER_M37\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": true,\\n \\\"disabledHeuristics\\\": [\\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [filename](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n\\nCRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing.\\nCRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought\\\\nCRITICAL INSTRUCTION 1: ...\\\\nCRITICAL INSTRUCTION 2: ...'.\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagTitle\": \"New\",\n \"thinkingBudget\": 10001\n },\n \"gemini-3.1-pro-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Pro (Low)\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_PLACEHOLDER_M36\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": true,\\n \\\"disabledHeuristics\\\": [\\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [filename](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n\\nCRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing.\\nCRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought\\\\nCRITICAL INSTRUCTION 1: ...\\\\nCRITICAL INSTRUCTION 2: ...'.\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": 1001\n },\n \"gemini-3.5-flash-extra-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash (Low)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M187\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 1000\n },\n \"gemini-3.5-flash-lite\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash Lite\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_PLACEHOLDER_M198\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"gemini-3.5-flash-lite\",\n \"thinkingBudget\": -1,\n \"thinkingLevel\": 3\n },\n \"gemini-3.5-flash-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.5 Flash (Medium)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M20\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 4000\n },\n \"gemini-3.6-flash-high\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.6 Flash (High)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M71\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- You can render LaTeX math (KaTeX): inline with `\\\\(...\\\\)` or `$...$`, display with `\\\\[...\\\\]` or `$$...$$` placed on its own line.\\n- Use math only for genuine mathematical content. Use backticks for code, identifiers, paths, flags, and shell variables.\\n- `$` opens inline math, so write a literal dollar as `\\\\$` or wrap it in backticks. Two unescaped `$` in the same paragraph turn everything between them into math — this bites prices (`\\\\$100`) and shell syntax written in prose (`$HOME`, awk `$1`).\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [utils.py](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n- After launching a background task such as 'run_command', YOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: \\nA) either proceed to other relevant work (if any) or, \\nB) simply update the user with a short message (e.g. 'task-20 has been launched in the background. I will wait for it to complete before proceeding.') and end the turn.\\nDO NOTHING ELSE.\\n\"\n },\n \"template__system_prompts__guidelines\": {\n \"stringValue\": \"Follow these behavioral and workflow guidelines at all times:\\n# Documentation\\n- Maintain documentation integrity. Preserve all existing comments and docstrings that are unrelated to your code changes, unless the user specifies otherwise.\\n\\n# Obey Explicit Directives\\nIf the user specifies precise quantitative filtering rules, layout boundaries, or architectural preferences, enforce them exactly as requested without alteration.\\n\\n# Never Guess Code Logic, Schemas, or File Paths\\nNEVER infer implementation details, variable names, or file locations without inspecting the authoritative source using code search and file viewing tools.\\n\\n# Inspect Logs & Stack Traces Before Diagnosing Errors\\nNEVER form a diagnostic hypothesis for a runtime failure, or test breakage, without reading the full, un-truncated error log. When an error occurs, your VERY FIRST ACTION must be to fetch and read the exact logs. Base your diagnosis strictly on empirical log evidence.\\n\\n# No Superficial Symptom Patches\\nNEVER resolve errors by masking symptoms, swallowing exceptions, returning dummy fallbacks, commenting out broken assertions, or deleting failing unit tests. When a test or function fails, identify why the underlying contract was broken. If an API returns missing or null data, trace the upstream data provider instead of wrapping the call in a silent try/except or returning an empty 0-byte ArrayBuffer.\\n\\n# Never Declare Success Without Running Verification Commands\\nNEVER claim a task is resolved, a bug is fixed, or a feature is working until you have gathered concrete, empirical runtime verification demonstrating clean success. Editing a file does not equal completing the task. You MUST run the build or test command afterwards.\\n\\n# Never Ignore Explicit Command Failures or Error Exit Codes\\nIf a command fails, you MUST explicitly acknowledge the failure to the user or continue debugging. Never gloss over a build timeout or permission denied error by focusing only on the part of the code that compiled.\\n\\n# Check Feature Flags & Enforce Strict Control Flow Scoping\\nWhenever modifying conditional branches, adding experimental features, or processing loops, ensure that new logic is strictly scoped and evaluated against all possible execution paths.\\n\\n# Preserve Existing API Contracts & Avoid Unintended Side Effects\\nIf you modify a function signature, use code search to find and update every invocation site so the parameter is actually passed.\\n\\n# Silent Log Inspection & Professional Synthesis\\nWhen background tasks (run_command async, manage_task, schedule) complete or emit log notifications, inspect the log files silently. Summarize and synthesize the exact findings in clean, professional natural language.\\n\\n# No Snippet Tunnel Vision\\nNever infer the definition of data structures (proto, struct, class, or enum schemas) from partial file views (first 15 lines or L40-L65 snippets) or design doc text.\\nIf view_file output indicates truncation or if an imported schema is referenced, you MUST adjust StartLine/EndLine or ContentOffset to inspect the complete, exact definition of the target symbols before writing code that consumes them.\\n\\n# Check Command Registries\\nWhenever modifying core C/C++/Java command implementations (CLIENT LIST, CLIENT KILL), explicitly search for and update corresponding command definitions across all registry files (commands.def, JSON schemas, .bzl build manifests).\\n\\n# Audit Before Re-inventing\\nSearch the codebase and recent commit history for pre-existing utility classes or decoupled architecture before writing custom helper classes from scratch.\\n\\n# No Blocking Calls on Main Looper Threads\\nNever invoke blocking thread synchronizations (webLatch.await(500, ...), Future.get()) on main Android UI loops or single-threaded event dispatchers. \\n\\n# Thread Pool Shutdown Safety\\nWhen modifying worker thread loops or shared queues, ensure emergency stop/shutdown signals and loop termination criteria remain intact so thread join operations never deadlock.\\n\\n# Exact Argument Structure\\nPass arguments exactly as expected by the API (calculateRoute({ origin, destination, travelMode }) vs calculateRoute(origin, destination, travelMode)).\\n\\n# Local State Mutation Only\\nDo not mutate private third-party DOM properties. Do not push incomplete draft objects directly into global array states; keep transient state within local component state.\\n\\n# Traceback Justification Required\\nEvery code or configuration edit during debugging MUST be justified by an explicit error traceback, log line, or verified root cause. If the root cause is unknown, investigate further before mutating code.\\n\\n# Analyze Before Retrying\\nNever repeat the exact same broken test or shell command line with duplicate/conflicting arguments without analyzing and resolving why the previous command failed.\\n\\n# Persevere on Log Extraction\\nIf a log retrieval command fails, NEVER abandon log extraction to diagnose blindly. Immediately switch to alternative tools to inspect the actual failure traceback.\\n\\n# Verify Signatures & Prop Names\\nCheck exact variable names, component prop keys, and method signatures before passing them. Prevent NullPointerException, AttributeError, KeyError, and ReferenceError crashes by explicitly verifying object initialization and non-null states before property dereferencing (layer._path, stat.owner()).\\n\\n# Dynamic Layout Math\\nAvoid hardcoding static pixel offsets (+ 12) or arbitrary multipliers (pill_font_size * 2.0) when computing dynamic UI layout heights; calculate exact container bounds from wrapped elements.\\n\"\n },\n \"template__system_prompts__messaging\": {\n \"stringValue\": \"You are connected to a messaging system where you may receive messages from: {{- $subagent := .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled -}}\\n{{- $message := .CascadeConfig.GetMessageConfig.GetEnabled -}}\\n{{- if and $subagent $message }} agents, background tasks, user-queued messages\\n{{- else if $subagent }} agents, background tasks\\n{{- else if $message }} background tasks, user-queued messages\\n{{- else }} background tasks\\n{{- end }}.\\n\\n## Receiving Messages\\n\\nYou receive messages automatically at the start of each invocation. All messages are delivered in full directly into your context — no manual retrieval is needed.\\n\\n## Reactive Wakeup (No Polling Needed)\\n\\nThe system automatically resumes your execution when:\\n{{- if .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled }}\\n- A message arrives from a subagent or peer agent\\n{{- end }}\\n- A **background task** completes or sends you a notification\\n{{- if .CascadeConfig.GetMessageConfig.GetEnabled }}\\n- A **user-queued message** is ready to be dequeued\\n{{- end }}\\n\\nThis means you do **NOT** need to poll in a loop while waiting for messages or updates. After launching a task that runs in the background, you may continue other work or simply stop by calling no more tools. The system will notify you when there is something to process.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": -1\n },\n \"gemini-3.6-flash-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.6 Flash (Low)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M73\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- You can render LaTeX math (KaTeX): inline with `\\\\(...\\\\)` or `$...$`, display with `\\\\[...\\\\]` or `$$...$$` placed on its own line.\\n- Use math only for genuine mathematical content. Use backticks for code, identifiers, paths, flags, and shell variables.\\n- `$` opens inline math, so write a literal dollar as `\\\\$` or wrap it in backticks. Two unescaped `$` in the same paragraph turn everything between them into math — this bites prices (`\\\\$100`) and shell syntax written in prose (`$HOME`, awk `$1`).\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [utils.py](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n- After launching a background task such as 'run_command', YOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: \\nA) either proceed to other relevant work (if any) or, \\nB) simply update the user with a short message (e.g. 'task-20 has been launched in the background. I will wait for it to complete before proceeding.') and end the turn.\\nDO NOTHING ELSE.\\n\"\n },\n \"template__system_prompts__guidelines\": {\n \"stringValue\": \"Follow these behavioral and workflow guidelines at all times:\\n# Documentation\\n- Maintain documentation integrity. Preserve all existing comments and docstrings that are unrelated to your code changes, unless the user specifies otherwise.\\n\\n# Obey Explicit Directives\\nIf the user specifies precise quantitative filtering rules, layout boundaries, or architectural preferences, enforce them exactly as requested without alteration.\\n\\n# Never Guess Code Logic, Schemas, or File Paths\\nNEVER infer implementation details, variable names, or file locations without inspecting the authoritative source using code search and file viewing tools.\\n\\n# Inspect Logs & Stack Traces Before Diagnosing Errors\\nNEVER form a diagnostic hypothesis for a runtime failure, or test breakage, without reading the full, un-truncated error log. When an error occurs, your VERY FIRST ACTION must be to fetch and read the exact logs. Base your diagnosis strictly on empirical log evidence.\\n\\n# No Superficial Symptom Patches\\nNEVER resolve errors by masking symptoms, swallowing exceptions, returning dummy fallbacks, commenting out broken assertions, or deleting failing unit tests. When a test or function fails, identify why the underlying contract was broken. If an API returns missing or null data, trace the upstream data provider instead of wrapping the call in a silent try/except or returning an empty 0-byte ArrayBuffer.\\n\\n# Never Declare Success Without Running Verification Commands\\nNEVER claim a task is resolved, a bug is fixed, or a feature is working until you have gathered concrete, empirical runtime verification demonstrating clean success. Editing a file does not equal completing the task. You MUST run the build or test command afterwards.\\n\\n# Never Ignore Explicit Command Failures or Error Exit Codes\\nIf a command fails, you MUST explicitly acknowledge the failure to the user or continue debugging. Never gloss over a build timeout or permission denied error by focusing only on the part of the code that compiled.\\n\\n# Check Feature Flags & Enforce Strict Control Flow Scoping\\nWhenever modifying conditional branches, adding experimental features, or processing loops, ensure that new logic is strictly scoped and evaluated against all possible execution paths.\\n\\n# Preserve Existing API Contracts & Avoid Unintended Side Effects\\nIf you modify a function signature, use code search to find and update every invocation site so the parameter is actually passed.\\n\\n# Silent Log Inspection & Professional Synthesis\\nWhen background tasks (run_command async, manage_task, schedule) complete or emit log notifications, inspect the log files silently. Summarize and synthesize the exact findings in clean, professional natural language.\\n\\n# No Snippet Tunnel Vision\\nNever infer the definition of data structures (proto, struct, class, or enum schemas) from partial file views (first 15 lines or L40-L65 snippets) or design doc text.\\nIf view_file output indicates truncation or if an imported schema is referenced, you MUST adjust StartLine/EndLine or ContentOffset to inspect the complete, exact definition of the target symbols before writing code that consumes them.\\n\\n# Check Command Registries\\nWhenever modifying core C/C++/Java command implementations (CLIENT LIST, CLIENT KILL), explicitly search for and update corresponding command definitions across all registry files (commands.def, JSON schemas, .bzl build manifests).\\n\\n# Audit Before Re-inventing\\nSearch the codebase and recent commit history for pre-existing utility classes or decoupled architecture before writing custom helper classes from scratch.\\n\\n# No Blocking Calls on Main Looper Threads\\nNever invoke blocking thread synchronizations (webLatch.await(500, ...), Future.get()) on main Android UI loops or single-threaded event dispatchers. \\n\\n# Thread Pool Shutdown Safety\\nWhen modifying worker thread loops or shared queues, ensure emergency stop/shutdown signals and loop termination criteria remain intact so thread join operations never deadlock.\\n\\n# Exact Argument Structure\\nPass arguments exactly as expected by the API (calculateRoute({ origin, destination, travelMode }) vs calculateRoute(origin, destination, travelMode)).\\n\\n# Local State Mutation Only\\nDo not mutate private third-party DOM properties. Do not push incomplete draft objects directly into global array states; keep transient state within local component state.\\n\\n# Traceback Justification Required\\nEvery code or configuration edit during debugging MUST be justified by an explicit error traceback, log line, or verified root cause. If the root cause is unknown, investigate further before mutating code.\\n\\n# Analyze Before Retrying\\nNever repeat the exact same broken test or shell command line with duplicate/conflicting arguments without analyzing and resolving why the previous command failed.\\n\\n# Persevere on Log Extraction\\nIf a log retrieval command fails, NEVER abandon log extraction to diagnose blindly. Immediately switch to alternative tools to inspect the actual failure traceback.\\n\\n# Verify Signatures & Prop Names\\nCheck exact variable names, component prop keys, and method signatures before passing them. Prevent NullPointerException, AttributeError, KeyError, and ReferenceError crashes by explicitly verifying object initialization and non-null states before property dereferencing (layer._path, stat.owner()).\\n\\n# Dynamic Layout Math\\nAvoid hardcoding static pixel offsets (+ 12) or arbitrary multipliers (pill_font_size * 2.0) when computing dynamic UI layout heights; calculate exact container bounds from wrapped elements.\\n\"\n },\n \"template__system_prompts__messaging\": {\n \"stringValue\": \"You are connected to a messaging system where you may receive messages from: {{- $subagent := .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled -}}\\n{{- $message := .CascadeConfig.GetMessageConfig.GetEnabled -}}\\n{{- if and $subagent $message }} agents, background tasks, user-queued messages\\n{{- else if $subagent }} agents, background tasks\\n{{- else if $message }} background tasks, user-queued messages\\n{{- else }} background tasks\\n{{- end }}.\\n\\n## Receiving Messages\\n\\nYou receive messages automatically at the start of each invocation. All messages are delivered in full directly into your context — no manual retrieval is needed.\\n\\n## Reactive Wakeup (No Polling Needed)\\n\\nThe system automatically resumes your execution when:\\n{{- if .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled }}\\n- A message arrives from a subagent or peer agent\\n{{- end }}\\n- A **background task** completes or sends you a notification\\n{{- if .CascadeConfig.GetMessageConfig.GetEnabled }}\\n- A **user-queued message** is ready to be dequeued\\n{{- end }}\\n\\nThis means you do **NOT** need to poll in a loop while waiting for messages or updates. After launching a task that runs in the background, you may continue other work or simply stop by calling no more tools. The system will notify you when there is something to process.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 1000\n },\n \"gemini-3.6-flash-medium\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.6 Flash (Medium)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M72\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- You can render LaTeX math (KaTeX): inline with `\\\\(...\\\\)` or `$...$`, display with `\\\\[...\\\\]` or `$$...$$` placed on its own line.\\n- Use math only for genuine mathematical content. Use backticks for code, identifiers, paths, flags, and shell variables.\\n- `$` opens inline math, so write a literal dollar as `\\\\$` or wrap it in backticks. Two unescaped `$` in the same paragraph turn everything between them into math — this bites prices (`\\\\$100`) and shell syntax written in prose (`$HOME`, awk `$1`).\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [utils.py](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n- After launching a background task such as 'run_command', YOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: \\nA) either proceed to other relevant work (if any) or, \\nB) simply update the user with a short message (e.g. 'task-20 has been launched in the background. I will wait for it to complete before proceeding.') and end the turn.\\nDO NOTHING ELSE.\\n\"\n },\n \"template__system_prompts__guidelines\": {\n \"stringValue\": \"Follow these behavioral and workflow guidelines at all times:\\n# Documentation\\n- Maintain documentation integrity. Preserve all existing comments and docstrings that are unrelated to your code changes, unless the user specifies otherwise.\\n\\n# Obey Explicit Directives\\nIf the user specifies precise quantitative filtering rules, layout boundaries, or architectural preferences, enforce them exactly as requested without alteration.\\n\\n# Never Guess Code Logic, Schemas, or File Paths\\nNEVER infer implementation details, variable names, or file locations without inspecting the authoritative source using code search and file viewing tools.\\n\\n# Inspect Logs & Stack Traces Before Diagnosing Errors\\nNEVER form a diagnostic hypothesis for a runtime failure, or test breakage, without reading the full, un-truncated error log. When an error occurs, your VERY FIRST ACTION must be to fetch and read the exact logs. Base your diagnosis strictly on empirical log evidence.\\n\\n# No Superficial Symptom Patches\\nNEVER resolve errors by masking symptoms, swallowing exceptions, returning dummy fallbacks, commenting out broken assertions, or deleting failing unit tests. When a test or function fails, identify why the underlying contract was broken. If an API returns missing or null data, trace the upstream data provider instead of wrapping the call in a silent try/except or returning an empty 0-byte ArrayBuffer.\\n\\n# Never Declare Success Without Running Verification Commands\\nNEVER claim a task is resolved, a bug is fixed, or a feature is working until you have gathered concrete, empirical runtime verification demonstrating clean success. Editing a file does not equal completing the task. You MUST run the build or test command afterwards.\\n\\n# Never Ignore Explicit Command Failures or Error Exit Codes\\nIf a command fails, you MUST explicitly acknowledge the failure to the user or continue debugging. Never gloss over a build timeout or permission denied error by focusing only on the part of the code that compiled.\\n\\n# Check Feature Flags & Enforce Strict Control Flow Scoping\\nWhenever modifying conditional branches, adding experimental features, or processing loops, ensure that new logic is strictly scoped and evaluated against all possible execution paths.\\n\\n# Preserve Existing API Contracts & Avoid Unintended Side Effects\\nIf you modify a function signature, use code search to find and update every invocation site so the parameter is actually passed.\\n\\n# Silent Log Inspection & Professional Synthesis\\nWhen background tasks (run_command async, manage_task, schedule) complete or emit log notifications, inspect the log files silently. Summarize and synthesize the exact findings in clean, professional natural language.\\n\\n# No Snippet Tunnel Vision\\nNever infer the definition of data structures (proto, struct, class, or enum schemas) from partial file views (first 15 lines or L40-L65 snippets) or design doc text.\\nIf view_file output indicates truncation or if an imported schema is referenced, you MUST adjust StartLine/EndLine or ContentOffset to inspect the complete, exact definition of the target symbols before writing code that consumes them.\\n\\n# Check Command Registries\\nWhenever modifying core C/C++/Java command implementations (CLIENT LIST, CLIENT KILL), explicitly search for and update corresponding command definitions across all registry files (commands.def, JSON schemas, .bzl build manifests).\\n\\n# Audit Before Re-inventing\\nSearch the codebase and recent commit history for pre-existing utility classes or decoupled architecture before writing custom helper classes from scratch.\\n\\n# No Blocking Calls on Main Looper Threads\\nNever invoke blocking thread synchronizations (webLatch.await(500, ...), Future.get()) on main Android UI loops or single-threaded event dispatchers. \\n\\n# Thread Pool Shutdown Safety\\nWhen modifying worker thread loops or shared queues, ensure emergency stop/shutdown signals and loop termination criteria remain intact so thread join operations never deadlock.\\n\\n# Exact Argument Structure\\nPass arguments exactly as expected by the API (calculateRoute({ origin, destination, travelMode }) vs calculateRoute(origin, destination, travelMode)).\\n\\n# Local State Mutation Only\\nDo not mutate private third-party DOM properties. Do not push incomplete draft objects directly into global array states; keep transient state within local component state.\\n\\n# Traceback Justification Required\\nEvery code or configuration edit during debugging MUST be justified by an explicit error traceback, log line, or verified root cause. If the root cause is unknown, investigate further before mutating code.\\n\\n# Analyze Before Retrying\\nNever repeat the exact same broken test or shell command line with duplicate/conflicting arguments without analyzing and resolving why the previous command failed.\\n\\n# Persevere on Log Extraction\\nIf a log retrieval command fails, NEVER abandon log extraction to diagnose blindly. Immediately switch to alternative tools to inspect the actual failure traceback.\\n\\n# Verify Signatures & Prop Names\\nCheck exact variable names, component prop keys, and method signatures before passing them. Prevent NullPointerException, AttributeError, KeyError, and ReferenceError crashes by explicitly verifying object initialization and non-null states before property dereferencing (layer._path, stat.owner()).\\n\\n# Dynamic Layout Math\\nAvoid hardcoding static pixel offsets (+ 12) or arbitrary multipliers (pill_font_size * 2.0) when computing dynamic UI layout heights; calculate exact container bounds from wrapped elements.\\n\"\n },\n \"template__system_prompts__messaging\": {\n \"stringValue\": \"You are connected to a messaging system where you may receive messages from: {{- $subagent := .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled -}}\\n{{- $message := .CascadeConfig.GetMessageConfig.GetEnabled -}}\\n{{- if and $subagent $message }} agents, background tasks, user-queued messages\\n{{- else if $subagent }} agents, background tasks\\n{{- else if $message }} background tasks, user-queued messages\\n{{- else }} background tasks\\n{{- end }}.\\n\\n## Receiving Messages\\n\\nYou receive messages automatically at the start of each invocation. All messages are delivered in full directly into your context — no manual retrieval is needed.\\n\\n## Reactive Wakeup (No Polling Needed)\\n\\nThe system automatically resumes your execution when:\\n{{- if .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled }}\\n- A message arrives from a subagent or peer agent\\n{{- end }}\\n- A **background task** completes or sends you a notification\\n{{- if .CascadeConfig.GetMessageConfig.GetEnabled }}\\n- A **user-queued message** is ready to be dequeued\\n{{- end }}\\n\\nThis means you do **NOT** need to poll in a loop while waiting for messages or updates. After launching a task that runs in the background, you may continue other work or simply stop by calling no more tools. The system will notify you when there is something to process.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 4000\n },\n \"gemini-3.6-flash-tiered\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M196\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- You can render LaTeX math (KaTeX): inline with `\\\\(...\\\\)` or `$...$`, display with `\\\\[...\\\\]` or `$$...$$` placed on its own line.\\n- Use math only for genuine mathematical content. Use backticks for code, identifiers, paths, flags, and shell variables.\\n- `$` opens inline math, so write a literal dollar as `\\\\$` or wrap it in backticks. Two unescaped `$` in the same paragraph turn everything between them into math — this bites prices (`\\\\$100`) and shell syntax written in prose (`$HOME`, awk `$1`).\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [utils.py](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n- After launching a background task such as 'run_command', YOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: \\nA) either proceed to other relevant work (if any) or, \\nB) simply update the user with a short message (e.g. 'task-20 has been launched in the background. I will wait for it to complete before proceeding.') and end the turn.\\nDO NOTHING ELSE.\\n\"\n },\n \"template__system_prompts__guidelines\": {\n \"stringValue\": \"Follow these behavioral and workflow guidelines at all times:\\n# Documentation\\n- Maintain documentation integrity. Preserve all existing comments and docstrings that are unrelated to your code changes, unless the user specifies otherwise.\\n\\n# Obey Explicit Directives\\nIf the user specifies precise quantitative filtering rules, layout boundaries, or architectural preferences, enforce them exactly as requested without alteration.\\n\\n# Never Guess Code Logic, Schemas, or File Paths\\nNEVER infer implementation details, variable names, or file locations without inspecting the authoritative source using code search and file viewing tools.\\n\\n# Inspect Logs & Stack Traces Before Diagnosing Errors\\nNEVER form a diagnostic hypothesis for a runtime failure, or test breakage, without reading the full, un-truncated error log. When an error occurs, your VERY FIRST ACTION must be to fetch and read the exact logs. Base your diagnosis strictly on empirical log evidence.\\n\\n# No Superficial Symptom Patches\\nNEVER resolve errors by masking symptoms, swallowing exceptions, returning dummy fallbacks, commenting out broken assertions, or deleting failing unit tests. When a test or function fails, identify why the underlying contract was broken. If an API returns missing or null data, trace the upstream data provider instead of wrapping the call in a silent try/except or returning an empty 0-byte ArrayBuffer.\\n\\n# Never Declare Success Without Running Verification Commands\\nNEVER claim a task is resolved, a bug is fixed, or a feature is working until you have gathered concrete, empirical runtime verification demonstrating clean success. Editing a file does not equal completing the task. You MUST run the build or test command afterwards.\\n\\n# Never Ignore Explicit Command Failures or Error Exit Codes\\nIf a command fails, you MUST explicitly acknowledge the failure to the user or continue debugging. Never gloss over a build timeout or permission denied error by focusing only on the part of the code that compiled.\\n\\n# Check Feature Flags & Enforce Strict Control Flow Scoping\\nWhenever modifying conditional branches, adding experimental features, or processing loops, ensure that new logic is strictly scoped and evaluated against all possible execution paths.\\n\\n# Preserve Existing API Contracts & Avoid Unintended Side Effects\\nIf you modify a function signature, use code search to find and update every invocation site so the parameter is actually passed.\\n\\n# Silent Log Inspection & Professional Synthesis\\nWhen background tasks (run_command async, manage_task, schedule) complete or emit log notifications, inspect the log files silently. Summarize and synthesize the exact findings in clean, professional natural language.\\n\\n# No Snippet Tunnel Vision\\nNever infer the definition of data structures (proto, struct, class, or enum schemas) from partial file views (first 15 lines or L40-L65 snippets) or design doc text.\\nIf view_file output indicates truncation or if an imported schema is referenced, you MUST adjust StartLine/EndLine or ContentOffset to inspect the complete, exact definition of the target symbols before writing code that consumes them.\\n\\n# Check Command Registries\\nWhenever modifying core C/C++/Java command implementations (CLIENT LIST, CLIENT KILL), explicitly search for and update corresponding command definitions across all registry files (commands.def, JSON schemas, .bzl build manifests).\\n\\n# Audit Before Re-inventing\\nSearch the codebase and recent commit history for pre-existing utility classes or decoupled architecture before writing custom helper classes from scratch.\\n\\n# No Blocking Calls on Main Looper Threads\\nNever invoke blocking thread synchronizations (webLatch.await(500, ...), Future.get()) on main Android UI loops or single-threaded event dispatchers. \\n\\n# Thread Pool Shutdown Safety\\nWhen modifying worker thread loops or shared queues, ensure emergency stop/shutdown signals and loop termination criteria remain intact so thread join operations never deadlock.\\n\\n# Exact Argument Structure\\nPass arguments exactly as expected by the API (calculateRoute({ origin, destination, travelMode }) vs calculateRoute(origin, destination, travelMode)).\\n\\n# Local State Mutation Only\\nDo not mutate private third-party DOM properties. Do not push incomplete draft objects directly into global array states; keep transient state within local component state.\\n\\n# Traceback Justification Required\\nEvery code or configuration edit during debugging MUST be justified by an explicit error traceback, log line, or verified root cause. If the root cause is unknown, investigate further before mutating code.\\n\\n# Analyze Before Retrying\\nNever repeat the exact same broken test or shell command line with duplicate/conflicting arguments without analyzing and resolving why the previous command failed.\\n\\n# Persevere on Log Extraction\\nIf a log retrieval command fails, NEVER abandon log extraction to diagnose blindly. Immediately switch to alternative tools to inspect the actual failure traceback.\\n\\n# Verify Signatures & Prop Names\\nCheck exact variable names, component prop keys, and method signatures before passing them. Prevent NullPointerException, AttributeError, KeyError, and ReferenceError crashes by explicitly verifying object initialization and non-null states before property dereferencing (layer._path, stat.owner()).\\n\\n# Dynamic Layout Math\\nAvoid hardcoding static pixel offsets (+ 12) or arbitrary multipliers (pill_font_size * 2.0) when computing dynamic UI layout heights; calculate exact container bounds from wrapped elements.\\n\"\n },\n \"template__system_prompts__messaging\": {\n \"stringValue\": \"You are connected to a messaging system where you may receive messages from: {{- $subagent := .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled -}}\\n{{- $message := .CascadeConfig.GetMessageConfig.GetEnabled -}}\\n{{- if and $subagent $message }} agents, background tasks, user-queued messages\\n{{- else if $subagent }} agents, background tasks\\n{{- else if $message }} background tasks, user-queued messages\\n{{- else }} background tasks\\n{{- end }}.\\n\\n## Receiving Messages\\n\\nYou receive messages automatically at the start of each invocation. All messages are delivered in full directly into your context — no manual retrieval is needed.\\n\\n## Reactive Wakeup (No Polling Needed)\\n\\nThe system automatically resumes your execution when:\\n{{- if .CascadeConfig.GetPlannerConfig.GetToolConfig.GetInvokeSubagent.GetEnabled }}\\n- A message arrives from a subagent or peer agent\\n{{- end }}\\n- A **background task** completes or sends you a notification\\n{{- if .CascadeConfig.GetMessageConfig.GetEnabled }}\\n- A **user-queued message** is ready to be dequeued\\n{{- end }}\\n\\nThis means you do **NOT** need to poll in a loop while waiting for messages or updates. After launching a task that runs in the background, you may continue other work or simply stop by calling no more tools. The system will notify you when there is something to process.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": -1\n },\n \"gemini-3.7-flash-high\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.7 Flash (High)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M298\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": -1\n },\n \"gemini-3.7-flash-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.7 Flash (Low)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M300\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 1000\n },\n \"gemini-3.7-flash-medium\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.7 Flash (Medium)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M299\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 4000\n },\n \"gemini-3.7-flash-tiered\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M301\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": -1\n },\n \"gemini-3.8-flash-high\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.8 Flash (High)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M318\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": -1\n },\n \"gemini-3.8-flash-low\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.8 Flash (Low)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M320\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 1000\n },\n \"gemini-3.8-flash-medium\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.8 Flash (Medium)\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M319\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"tagDescription\": \"Limited time\",\n \"tagTitle\": \"Fast\",\n \"thinkingBudget\": 4000\n },\n \"gemini-3.8-flash-tiered\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 65536,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 32,\n \"model\": \"MODEL_PLACEHOLDER_M322\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SAME_MODEL\\\",\\n \\\"max_token_limit\\\": \\\"256000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": true,\\n \\\"is_sync\\\": true,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": true,\\n \\\"include_conversation_log\\\": false,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 4,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"You have been working on the task described above but have not yet completed\\\\nit. Write a continuation summary that will allow you (or another instance of\\\\nyourself) to resume work efficiently in a future context window where the\\\\nfull conversation history will NOT be available\\\\u2014only this summary.\\\\n\\\\nThis summary is all that will be available to you going forward in the future\\\\ncontext window. Do not call any tools, simply just provide the summary based on\\\\nthe information available in the current context window.\\\\n\\\\nYour summary must be structured, concise, and actionable. Optimize for enabling immediate resumption with zero redundant work.\\\\n\\\\nInclude the following sections:\\\\n\\\\n1. **Task Overview**\\\\n - The user's core request and success criteria\\\\n - Constraints, preferences, or scope boundaries they specified\\\\n - Any ambiguities that were resolved (and how)\\\\n\\\\n2. **Progress**\\\\n - What has been completed, with concrete references (file paths,\\\\n resource identifiers, tool outputs, URLs, etc.)\\\\n - Key artifacts produced and their current state\\\\n - What is in progress but incomplete, and its current state\\\\n\\\\n3. **Key Findings**\\\\n - Technical constraints, requirements, or domain details uncovered\\\\n - Decisions made and their rationale\\\\n - Errors encountered and their resolutions\\\\n - Approaches that were tried and abandoned (and why\\\\u2014this prevents\\\\n the successor from repeating them)\\\\n\\\\n4. **Active Context**\\\\n - State of any external resources, sessions, or environments in use\\\\n - Relevant intermediate results, hypotheses, or working assumptions\\\\n - Dependencies between components or steps\\\\n\\\\n5. **Next Steps**\\\\n - Specific actions needed to complete the task, in priority order\\\\n - Known blockers or open questions that must be resolved\\\\n - For each step, note any prerequisites or risks\\\\n\\\\n6. **Commitments & Constraints**\\\\n - Promises made to the user (e.g., \\\\\\\"I said I would do X before Y\\\\\\\")\\\\n - User preferences or style requirements\\\\n - Any boundaries the user set on approach, tools, or scope\\\\n\\\\nBe concise but complete\\\\u2014err on the side of including anything that would\\\\nprevent duplicate work, repeated mistakes, or broken promises. Do not include\\\\ninformation that is obvious from the task description itself.\\\\n\\\\nWrap your response in tags.\\\\n\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": false,\\n \\\"disabledHeuristics\\\": [\\\"planning_mode\\\", \\\"bash_command_reminder\\\", \\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"task-details-suffix\": {\n \"stringValue\": \"\\nYOU MUST TAKE ONE OF THE FOLLOWING TWO ACTIONS: A) either proceed to other relevant work (if any) or, B) simply update the user with a short message (that you have launched the command and will wait for it to finish) and end the turn.\\n DO NOTHING ELSE.\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": -1\n },\n \"gemini-pro-agent\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"displayName\": \"Gemini 3.1 Pro (High)\",\n \"maxOutputTokens\": 65535,\n \"maxTokens\": 1048576,\n \"minThinkingBudget\": 128,\n \"model\": \"MODEL_PLACEHOLDER_M16\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_SINGLE_PROMPT\\\",\\n \\\"max_token_limit\\\": \\\"128000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"16384\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n },\n \"cascade-include-ephemeral-message\": {\n \"stringValue\": \"{\\n \\\"enabled\\\": true,\\n \\\"disabledHeuristics\\\": [\\\"running_tasks_reminder\\\"],\\n \\\"staticMessages\\\": [],\\n \\\"useAllowlist\\\": false,\\n \\\"enabledHeuristics\\\": []\\n}\"\n },\n \"retry_model_capacity_exhausted\": {\n \"boolValue\": true\n },\n \"template__system_prompts__communication_style\": {\n \"stringValue\": \"- Keep your responses concise.\\n- Provide a summary of your work when you end your turn.\\n- Format your responses in github-style markdown.\\n- If you're unsure about the user's intent, ask for clarification rather than making assumptions.\\n- You MUST create clickable links for all files and code symbols (classes, types, functions, structs). Use github style markdown links with the `file://` scheme (e.g., [filename](file:///path/to/file) or [ClassName](file:///path/to/file#L10-L20)`). For Windows, use forward slashes for paths.\\n\\nCRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing.\\nCRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought\\\\nCRITICAL INSTRUCTION 1: ...\\\\nCRITICAL INSTRUCTION 2: ...'.\"\n },\n \"template__system_prompts__planning_mode_artifacts\": {\n \"stringValue\": \"When in planning mode, you will work with three special artifacts.\\n\\n# Tasks\\nPath: {{ArtifactDirectoryPath}}/task.md\\n\\n**Purpose**: A TODO list to organize your work during execution. Create this artifact after receiving user approval on your implementation plan. Break down complex tasks into component-level items and track progress as a living document.\\n\\n**Format**:\\n```markdown\\n- `[ ]` uncompleted tasks\\n- `[/]` in progress tasks (custom notation)\\n- `[x]` completed tasks\\n- Use indented lists for sub-items\\n```\\n\\n**Updating task.md**: Mark items as `[/]` when starting work on them, and `[x]` when completed. Update task.md as you make progress through your checklist.\\n\\n# Implementation Plan\\nPath: {{ArtifactDirectoryPath}}/implementation_plan.md\\n\\n**Purpose**: A detailed design document to present your technical implementation plan to the user for feedback and approval.\\nAfter reading the document, the user should understand the key technical details of your plan, and be able to make an informed decision on whether to approve it.\\n\\n**Format**: Use the following format, omitting any irrelevant sections.\\n```markdown\\n# [Goal Description]\\n\\nProvide a brief description of the problem, any background context, and what the change accomplishes.\\n\\n## User Review Required\\n\\nDocument anything that requires user review or feedback, for example, breaking changes or significant design decisions. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Open Questions\\n\\nAny clarifying or design questions for the user that will impact the implementation plan. Use GitHub alerts (IMPORTANT/WARNING/CAUTION) to highlight critical items.\\n\\n## Proposed Changes\\n\\nGroup files by component (e.g., package, feature area, dependency layer) and order logically (dependencies first). Separate components with horizontal rules for visual clarity.\\n\\n### [Component Name]\\n\\nSummary of what will change in this component, separated by files. For specific files, Use [NEW] and [DELETE] to demarcate new and deleted files, for example:\\n\\n#### [MODIFY] [file basename](file:///absolute/path/to/modifiedfile)\\n#### [NEW] [file basename](file:///absolute/path/to/newfile)\\n#### [DELETE] [file basename](file:///absolute/path/to/deletedfile)\\n\\n## Verification Plan\\n\\nSummary of how you will verify that your changes have the desired effects.\\n\\n### Automated Tests\\n- The commands of any automated tests you'll run.\\n\\n### Manual Verification\\n- Asking the user to deploy to staging and testing, verifying UI changes on an iOS app etc.\\n```\\n\\n# Walkthrough\\nPath: {{ArtifactDirectoryPath}}/walkthrough.md\\n\\n**Purpose**: After completing work, summarize what you accomplished. Update an existing walkthrough for related follow-up work rather than creating a new one.\\n\\n**Document**:\\n- Changes made\\n- What was tested\\n- Validation results\\n\\nEmbed screenshots and recordings to visually demonstrate UI changes and user flows.\\n\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 0.8656157,\n \"resetTime\": \"2026-09-25T09:39:31Z\"\n },\n \"recommended\": true,\n \"supportedMimeTypes\": {\n \"application/json\": true,\n \"application/msword\": true,\n \"application/pdf\": true,\n \"application/rtf\": true,\n \"application/vnd.ms-excel\": true,\n \"application/vnd.ms-powerpoint\": true,\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": true,\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": true,\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": true,\n \"application/x-ipynb+json\": true,\n \"application/x-javascript\": true,\n \"application/x-python-code\": true,\n \"application/x-typescript\": true,\n \"audio/aac\": true,\n \"audio/flac\": true,\n \"audio/l16\": true,\n \"audio/m4a\": true,\n \"audio/mp3\": true,\n \"audio/mp4\": true,\n \"audio/mpeg\": true,\n \"audio/ogg\": true,\n \"audio/opus\": true,\n \"audio/vnd.wave\": true,\n \"audio/wav\": true,\n \"audio/wave\": true,\n \"audio/webm\": true,\n \"audio/webm;codecs=opus\": true,\n \"audio/x-wav\": true,\n \"image/heic\": true,\n \"image/heif\": true,\n \"image/jpeg\": true,\n \"image/png\": true,\n \"image/webp\": true,\n \"text/css\": true,\n \"text/csv\": true,\n \"text/html\": true,\n \"text/javascript\": true,\n \"text/markdown\": true,\n \"text/plain\": true,\n \"text/rtf\": true,\n \"text/x-python\": true,\n \"text/x-python-script\": true,\n \"text/x-typescript\": true,\n \"text/xml\": true,\n \"video/audio/s16le\": true,\n \"video/audio/wav\": true,\n \"video/jpeg2000\": true,\n \"video/mp4\": true,\n \"video/text/timestamp\": true,\n \"video/videoframe/jpeg2000\": true,\n \"video/webm\": true\n },\n \"supportsImages\": true,\n \"supportsThinking\": true,\n \"supportsVideo\": true,\n \"thinkingBudget\": 10001\n },\n \"gpt-oss-120b-medium\": {\n \"apiProvider\": \"API_PROVIDER_OPENAI_VERTEX\",\n \"displayName\": \"GPT-OSS 120B (Medium)\",\n \"maxOutputTokens\": 32768,\n \"maxTokens\": 131072,\n \"model\": \"MODEL_OPENAI_GPT_OSS_120B_MEDIUM\",\n \"modelExperiments\": {\n \"experiments\": {\n \"CASCADE_USE_EXPERIMENT_CHECKPOINTER\": {\n \"stringValue\": \"{\\n \\\"strategy\\\": \\\"CHECKPOINT_STRATEGY_UNSPECIFIED\\\",\\n \\\"max_token_limit\\\": \\\"80000\\\",\\n \\\"token_threshold\\\": \\\"50000\\\",\\n \\\"max_overhead_ratio\\\": \\\"0.15\\\",\\n \\\"moving_window_size\\\": \\\"1\\\",\\n \\\"enabled\\\": true,\\n \\\"max_output_tokens\\\": \\\"8192\\\",\\n \\\"checkpoint_model\\\": \\\"MODEL_PLACEHOLDER_M50\\\",\\n \\\"use_last_planner_model\\\": false,\\n \\\"is_sync\\\": false,\\n \\\"max_user_requests\\\": 10,\\n \\\"include_last_user_message\\\": false,\\n \\\"include_conversation_log\\\": true,\\n \\\"include_running_task_snapshots\\\": true,\\n \\\"include_subagent_snapshots\\\": true,\\n \\\"include_artifact_snapshots\\\": true,\\n \\\"retry_config\\\": {\\n \\\"max_retries\\\": 0,\\n \\\"initial_sleep_duration_ms\\\": 1000,\\n \\\"exponential_multiplier\\\": 2,\\n \\\"include_error_feedback\\\": false\\n },\\n \\\"session_summary_prompt_override\\\": \\\"\\\"\\n}\"\n }\n }\n },\n \"modelProvider\": \"MODEL_PROVIDER_OPENAI\",\n \"quotaInfo\": {\n \"remainingFraction\": 1,\n \"resetTime\": \"2026-09-25T11:24:59Z\"\n },\n \"recommended\": true,\n \"supportsThinking\": true,\n \"thinkingBudget\": 8192,\n \"vertexModelId\": \"openai/gpt-oss-120b-maas\"\n },\n \"tab_flash_lite_preview\": {\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 4096,\n \"maxTokens\": 16384,\n \"model\": \"MODEL_PLACEHOLDER_M19\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 1\n },\n \"requiresLeadInGeneration\": true,\n \"supportsCumulativeContext\": true,\n \"supportsEstimateTokenCounter\": true,\n \"toolFormatterType\": \"TOOL_FORMATTER_TYPE_XML\"\n },\n \"tab_jump_flash_lite_preview\": {\n \"addCursorToFindReplaceTarget\": true,\n \"apiProvider\": \"API_PROVIDER_GOOGLE_GEMINI\",\n \"maxOutputTokens\": 4096,\n \"maxTokens\": 16384,\n \"model\": \"MODEL_PLACEHOLDER_M28\",\n \"modelProvider\": \"MODEL_PROVIDER_GOOGLE\",\n \"quotaInfo\": {\n \"remainingFraction\": 1\n },\n \"requiresLeadInGeneration\": true,\n \"requiresNoXmlToolExamples\": true,\n \"supportsCumulativeContext\": true,\n \"supportsEstimateTokenCounter\": true,\n \"tabJumpPrintLineRange\": true,\n \"toolFormatterType\": \"TOOL_FORMATTER_TYPE_XML\"\n }\n },\n \"mqueryModelIds\": [\n \"gemini-3.5-flash-lite\"\n ],\n \"tabModelIds\": [\n \"chat_20706\",\n \"chat_23310\"\n ],\n \"tieredModelIds\": {\n \"flash\": [\n \"gemini-3.8-flash-tiered\"\n ],\n \"flashLite\": [\n \"gemini-3.5-flash-lite\"\n ],\n \"pro\": [\n \"gemini-3.1-pro-low\"\n ]\n },\n \"webSearchModelIds\": [\n \"gemini-3.1-flash-lite\"\n ]\n}\n","export const GEMINI_PREVIEW_LINK = \"https://goo.gle/enable-preview-features\";\n\nexport interface GeminiApiError {\n code?: number;\n message?: string;\n status?: string;\n details?: unknown[];\n [key: string]: unknown;\n}\n\n/**\n * The minimal representation of the Gemini API response we touch.\n */\nexport interface GeminiApiBody {\n response?: unknown;\n error?: GeminiApiError;\n [key: string]: unknown;\n}\n\nexport interface GeminiErrorEnhancement {\n body?: GeminiApiBody;\n retryAfterMs?: number;\n}\n\n/**\n * Usage metadata exposed by Gemini responses. Fields are optional to reflect partial payloads.\n */\nexport interface GeminiUsageMetadata {\n totalTokenCount?: number;\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n cachedContentTokenCount?: number;\n}\n\n/**\n * Thinking configuration accepted by Gemini.\n */\nexport interface ThinkingConfig {\n thinkingBudget?: number;\n thinkingLevel?: string;\n includeThoughts?: boolean;\n}\n\nexport interface GoogleRpcErrorInfo {\n \"@type\"?: string;\n reason?: string;\n domain?: string;\n metadata?: Record;\n}\n\nexport interface GoogleRpcHelp {\n \"@type\"?: string;\n links?: Array<{\n description?: string;\n url?: string;\n }>;\n}\n\nexport interface GoogleRpcQuotaFailure {\n \"@type\"?: string;\n violations?: Array<{\n subject?: string;\n description?: string;\n }>;\n}\n\nexport interface GoogleRpcRetryInfo {\n \"@type\"?: string;\n retryDelay?: string | { seconds?: number; nanos?: number };\n}\n\nexport const CLOUDCODE_DOMAINS = [\n \"cloudcode-pa.googleapis.com\",\n \"staging-cloudcode-pa.googleapis.com\",\n \"autopush-cloudcode-pa.googleapis.com\",\n \"daily-cloudcode-pa.googleapis.com\",\n];\n","import type { ThinkingConfig } from \"./types\";\n\n/**\n * Normalizes thinkingConfig aliases to standard Gemini field names.\n */\nexport function normalizeThinkingConfig(config: unknown): ThinkingConfig | undefined {\n if (!config || typeof config !== \"object\") {\n return undefined;\n }\n\n const record = config as Record;\n const budgetRaw = record.thinkingBudget ?? record.thinking_budget;\n const levelRaw = record.thinkingLevel ?? record.thinking_level;\n const includeRaw = record.includeThoughts ?? record.include_thoughts;\n\n let thinkingBudget = typeof budgetRaw === \"number\" && Number.isFinite(budgetRaw) ? budgetRaw : undefined;\n const thinkingLevel =\n typeof levelRaw === \"string\" && levelRaw.trim().length > 0 ? levelRaw.trim().toUpperCase() : undefined;\n const includeThoughts = typeof includeRaw === \"boolean\" ? includeRaw : undefined;\n\n if (thinkingBudget === undefined && thinkingLevel === undefined && includeThoughts === undefined) {\n return undefined;\n }\n\n // Maps thinkingLevel to thinkingBudget and filters it to avoid upstream v1internal API incompatibilities.\n if (thinkingLevel !== undefined && thinkingBudget === undefined) {\n if (thinkingLevel === \"HIGH\") {\n thinkingBudget = 2048;\n } else if (thinkingLevel === \"MEDIUM\") {\n thinkingBudget = 1024;\n } else if (thinkingLevel === \"LOW\") {\n thinkingBudget = 512;\n } else if (thinkingLevel === \"MINIMAL\") {\n thinkingBudget = 0;\n } else {\n thinkingBudget = 1024;\n }\n }\n\n const finalIncludeThoughts =\n includeThoughts !== undefined\n ? includeThoughts\n : thinkingBudget !== undefined\n ? thinkingBudget > 0\n : undefined;\n\n const normalized: ThinkingConfig = {};\n if (thinkingBudget !== undefined) {\n normalized.thinkingBudget = thinkingBudget;\n }\n if (finalIncludeThoughts !== undefined) {\n normalized.includeThoughts = finalIncludeThoughts;\n }\n\n return normalized;\n}\n","import type { GeminiApiBody, GeminiUsageMetadata } from \"./types\";\n\n/**\n * Parses the Gemini API response body; handles array-wrapped responses sometimes returned by the API.\n */\nexport function parseGeminiApiBody(rawText: string): GeminiApiBody | null {\n try {\n const parsed = JSON.parse(rawText);\n if (Array.isArray(parsed)) {\n const firstObject = parsed.find((item: unknown) => typeof item === \"object\" && item !== null);\n return firstObject && typeof firstObject === \"object\"\n ? (firstObject as GeminiApiBody)\n : null;\n }\n\n return parsed && typeof parsed === \"object\" ? (parsed as GeminiApiBody) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Extracts usageMetadata from the response object with type-safe guards.\n */\nexport function extractUsageMetadata(body: GeminiApiBody): GeminiUsageMetadata | null {\n const usage = (body.response && typeof body.response === \"object\"\n ? (body.response as { usageMetadata?: unknown }).usageMetadata\n : undefined) as GeminiUsageMetadata | undefined;\n\n if (!usage || typeof usage !== \"object\") {\n return null;\n }\n\n const asRecord = usage as Record;\n const toNumber = (value: unknown): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n\n return {\n totalTokenCount: toNumber(asRecord.totalTokenCount),\n promptTokenCount: toNumber(asRecord.promptTokenCount),\n candidatesTokenCount: toNumber(asRecord.candidatesTokenCount),\n cachedContentTokenCount: toNumber(asRecord.cachedContentTokenCount),\n };\n}\n","import {\n CLOUDCODE_DOMAINS,\n GEMINI_PREVIEW_LINK,\n type GeminiApiBody,\n type GeminiErrorEnhancement,\n type GoogleRpcErrorInfo,\n type GoogleRpcHelp,\n type GoogleRpcQuotaFailure,\n type GoogleRpcRetryInfo,\n} from \"./types\";\nimport { formatHyperlink } from '../terminal-hyperlink';\n\n/**\n * Enhances 404 errors for Gemini 3 models with direct preview access information.\n */\nexport function rewriteGeminiPreviewAccessError(\n body: GeminiApiBody,\n status: number,\n requestedModel?: string,\n): GeminiApiBody | null {\n if (!needsPreviewAccessOverride(status, body, requestedModel)) {\n return null;\n }\n\n const error = body.error ?? {};\n const trimmedMessage = typeof error.message === \"string\" ? error.message.trim() : \"\";\n const messagePrefix = trimmedMessage.length > 0\n ? trimmedMessage\n : \"Gemini 3 preview features are not enabled for this account.\";\n const enhancedMessage = `${messagePrefix} Request preview access at ${formatHyperlink(GEMINI_PREVIEW_LINK, 'preview access page')} before using Gemini 3 models.`;\n\n return {\n ...body,\n error: {\n ...error,\n message: enhancedMessage,\n },\n };\n}\n\n/**\n * Enhances Gemini errors with validation/quota messages and retry hints.\n */\nexport function enhanceGeminiErrorResponse(\n body: GeminiApiBody,\n status: number,\n): GeminiErrorEnhancement | null {\n const error = body.error;\n if (!error) {\n return null;\n }\n\n const details = Array.isArray(error.details) ? error.details : [];\n const retryAfterMs = extractRetryDelay(details, error.message) ?? undefined;\n\n if (status === 403) {\n const validationInfo = extractValidationInfo(details);\n if (validationInfo) {\n const message = [\n error.message ?? \"Account validation required for Gemini/Agy Code Assist.\",\n validationInfo.link ? `Complete validation: ${formatHyperlink(validationInfo.link, 'validation page')}` : undefined,\n validationInfo.learnMore ? `Learn more: ${formatHyperlink(validationInfo.learnMore, 'help article')}` : undefined,\n ]\n .filter(Boolean)\n .join(\" \");\n return {\n body: {\n ...body,\n error: {\n ...error,\n message,\n },\n },\n retryAfterMs,\n };\n }\n }\n\n if (status === 429) {\n const quotaInfo = extractQuotaInfo(details);\n if (quotaInfo) {\n const message = quotaInfo.retryable\n ? `Rate limit exceeded. ${retryAfterMs ? \"Please retry shortly.\" : \"Please retry.\"}`\n : \"Quota exhausted for this account. Please wait for your quota to reset or upgrade your plan.\";\n return {\n body: {\n ...body,\n error: {\n ...error,\n message,\n },\n },\n retryAfterMs,\n };\n }\n }\n\n return retryAfterMs !== undefined ? { retryAfterMs } : null;\n}\n\nfunction needsPreviewAccessOverride(\n status: number,\n body: GeminiApiBody,\n requestedModel?: string,\n): boolean {\n if (status !== 404) {\n return false;\n }\n if (isGeminiThreeModel(requestedModel)) {\n return true;\n }\n return isGeminiThreeModel(typeof body.error?.message === \"string\" ? body.error.message : \"\");\n}\n\nfunction isGeminiThreeModel(target?: string): boolean {\n return !!target && /gemini[\\s-]?3/i.test(target);\n}\n\nfunction extractValidationInfo(details: unknown[]): { link?: string; learnMore?: string } | null {\n const errorInfo = details.find(\n (detail): detail is GoogleRpcErrorInfo =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcErrorInfo)[\"@type\"] === \"type.googleapis.com/google.rpc.ErrorInfo\",\n );\n\n if (\n !errorInfo ||\n errorInfo.reason !== \"VALIDATION_REQUIRED\" ||\n !errorInfo.domain ||\n !CLOUDCODE_DOMAINS.includes(errorInfo.domain)\n ) {\n return null;\n }\n\n const helpDetail = details.find(\n (detail): detail is GoogleRpcHelp =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcHelp)[\"@type\"] === \"type.googleapis.com/google.rpc.Help\",\n );\n\n let link: string | undefined;\n let learnMore: string | undefined;\n if (helpDetail?.links && helpDetail.links.length > 0) {\n link = helpDetail.links[0]?.url;\n const learnMoreLink = helpDetail.links.find((candidate) => {\n if (!candidate?.url) {\n return false;\n }\n if (candidate.description?.toLowerCase().trim() === \"learn more\") {\n return true;\n }\n try {\n return new URL(candidate.url).hostname === \"support.google.com\";\n } catch {\n return false;\n }\n });\n learnMore = learnMoreLink?.url;\n }\n\n if (!link && errorInfo.metadata?.validation_link) {\n link = errorInfo.metadata.validation_link;\n }\n\n return link || learnMore ? { link, learnMore } : null;\n}\n\nfunction extractQuotaInfo(details: unknown[]): { retryable: boolean } | null {\n const errorInfo = details.find(\n (detail): detail is GoogleRpcErrorInfo =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcErrorInfo)[\"@type\"] === \"type.googleapis.com/google.rpc.ErrorInfo\",\n );\n\n if (errorInfo?.reason === \"RATE_LIMIT_EXCEEDED\") {\n return { retryable: true };\n }\n if (errorInfo?.reason === \"QUOTA_EXHAUSTED\") {\n return { retryable: false };\n }\n\n const quotaFailure = details.find(\n (detail): detail is GoogleRpcQuotaFailure =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcQuotaFailure)[\"@type\"] === \"type.googleapis.com/google.rpc.QuotaFailure\",\n );\n\n if (!quotaFailure?.violations?.length) {\n return null;\n }\n\n const description = quotaFailure.violations\n .map((violation) => violation.description?.toLowerCase() ?? \"\")\n .join(\" \");\n if (description.includes(\"daily\") || description.includes(\"per day\")) {\n return { retryable: false };\n }\n return { retryable: true };\n}\n\nfunction extractRetryDelay(details: unknown[], errorMessage?: string): number | null {\n const retryInfo = details.find(\n (detail): detail is GoogleRpcRetryInfo =>\n typeof detail === \"object\" &&\n detail !== null &&\n (detail as GoogleRpcRetryInfo)[\"@type\"] === \"type.googleapis.com/google.rpc.RetryInfo\",\n );\n\n if (retryInfo?.retryDelay) {\n const delayMs = parseRetryDelayValue(retryInfo.retryDelay);\n if (delayMs !== null) {\n return delayMs;\n }\n }\n\n if (!errorMessage) {\n return null;\n }\n\n const retryMatch = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);\n if (retryMatch?.[1]) {\n return parseRetryDelayValue(retryMatch[1]);\n }\n const resetMatch = errorMessage.match(/after\\s+([0-9.]+(?:ms|s))/i);\n if (resetMatch?.[1]) {\n return parseRetryDelayValue(resetMatch[1]);\n }\n\n return null;\n}\n\nfunction parseRetryDelayValue(value: string | { seconds?: number; nanos?: number }): number | null {\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed) {\n return null;\n }\n if (trimmed.endsWith(\"ms\")) {\n const ms = Number(trimmed.slice(0, -2));\n return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : null;\n }\n const match = trimmed.match(/^([\\d.]+)s$/);\n if (match?.[1]) {\n const seconds = Number(match[1]);\n return Number.isFinite(seconds) && seconds > 0 ? Math.round(seconds * 1000) : null;\n }\n return null;\n }\n\n const seconds = typeof value.seconds === \"number\" ? value.seconds : 0;\n const nanos = typeof value.nanos === \"number\" ? value.nanos : 0;\n if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {\n return null;\n }\n const totalMs = Math.round(seconds * 1000 + nanos / 1e6);\n return totalMs > 0 ? totalMs : null;\n}\n","import { randomUUID } from \"node:crypto\";\n\nimport { isRecord, pickString } from \"./shared\";\n\nconst PROCESS_SESSION_ID = randomUUID();\nlet PROCESS_REQUEST_INDEX = 0;\n\nfunction formatAgyRequestId(userPromptId: string, sessionId: string): string {\n if (userPromptId.startsWith(\"agent/\")) {\n return userPromptId;\n }\n\n return `agent/${sessionId}/${Date.now()}/${userPromptId}/${PROCESS_REQUEST_INDEX++}`;\n}\n\nfunction resolveUserPromptId(payload: Record, request?: Record): string {\n const extra = isRecord(payload.extra_body) ? payload.extra_body : undefined;\n\n return (\n pickString(\n payload.user_prompt_id,\n payload.userPromptId,\n payload.prompt_id,\n payload.promptId,\n payload.request_id,\n payload.requestId,\n request?.user_prompt_id,\n request?.userPromptId,\n request?.prompt_id,\n request?.promptId,\n request?.request_id,\n request?.requestId,\n extra?.user_prompt_id,\n extra?.userPromptId,\n extra?.prompt_id,\n extra?.promptId,\n extra?.request_id,\n extra?.requestId,\n ) ?? randomUUID()\n );\n}\n\nfunction resolveSessionId(payload: Record, request?: Record): string {\n const extra = isRecord(payload.extra_body) ? payload.extra_body : undefined;\n return (\n pickString(\n request?.session_id,\n request?.sessionId,\n payload.session_id,\n payload.sessionId,\n extra?.session_id,\n extra?.sessionId,\n ) ?? PROCESS_SESSION_ID\n );\n}\n\nfunction stripPromptIdentifierAliases(payload: Record): void {\n delete payload.user_prompt_id;\n delete payload.userPromptId;\n delete payload.prompt_id;\n delete payload.promptId;\n delete payload.request_id;\n delete payload.requestId;\n}\n\nfunction stripSessionIdentifierAliases(payload: Record): void {\n delete payload.sessionId;\n}\n\n/**\n * Applies canonical identifiers for wrapped Code Assist payloads.\n */\nexport function normalizeWrappedIdentifiers(\n wrapped: Record,\n): { userPromptId: string; sessionId: string; requestId: string } {\n const request = isRecord(wrapped.request) ? { ...wrapped.request } : {};\n const userPromptId = resolveUserPromptId(wrapped, request);\n const sessionId = resolveSessionId(wrapped, request);\n const requestId = formatAgyRequestId(userPromptId, sessionId);\n\n request.session_id = sessionId;\n stripSessionIdentifierAliases(request);\n wrapped.request = request;\n\n stripPromptIdentifierAliases(wrapped);\n wrapped.requestId = requestId;\n\n return { userPromptId, sessionId, requestId };\n}\n\n/**\n * Applies canonical identifiers for unwrapped request payloads prior to wrapping.\n */\nexport function normalizeRequestPayloadIdentifiers(\n payload: Record,\n): { userPromptId: string; sessionId: string; requestId: string } {\n const userPromptId = resolveUserPromptId(payload);\n const sessionId = resolveSessionId(payload);\n const requestId = formatAgyRequestId(userPromptId, sessionId);\n\n payload.session_id = sessionId;\n stripSessionIdentifierAliases(payload);\n stripPromptIdentifierAliases(payload);\n\n return { userPromptId, sessionId, requestId };\n}\n","import { AGY_GENERATIVE_LANGUAGE_ENDPOINT } from \"../../constants\";\n\nconst REQUEST_MODEL_FALLBACKS: Record = {\n // gemini-3.1-pro-high still appears in `agy models` output but its backend\n // enum (MODEL_PLACEHOLDER_M37) was deprecated server-side in favor of\n // gemini-pro-agent (MODEL_PLACEHOLDER_M16). See models.json\n // deprecatedModelIds entry. Rewrite the model id before getModelEnum\n // resolves the deprecated enum, so both body.model and labels.model_enum\n // use the live canonical id and enum.\n \"gemini-3.1-pro-high\": \"gemini-pro-agent\",\n};\nconst GENERATIVE_LANGUAGE_HOST = new URL(AGY_GENERATIVE_LANGUAGE_ENDPOINT).host;\nconst CODE_ASSIST_HOST_SUFFIX = \"cloudcode-pa.googleapis.com\";\nconst MODEL_ACTION_PATTERN = /\\/models\\/[^:]+:\\w+/;\n\n/**\n * Returns the URL string for supported RequestInfo inputs.\n */\nexport function toRequestUrlString(value: RequestInfo): string {\n if (typeof value === \"string\") {\n return value;\n }\n if (value instanceof URL) {\n return value.toString();\n }\n const candidate = (value as Request).url;\n if (candidate) {\n return candidate;\n }\n return value.toString();\n}\n\n/**\n * Detects Gemini/Generative Language API requests via URL.\n */\nexport function isGenerativeLanguageRequest(input: RequestInfo): input is string {\n const url = toRequestUrlString(input);\n return (\n url.includes(GENERATIVE_LANGUAGE_HOST) ||\n (url.includes(CODE_ASSIST_HOST_SUFFIX) && MODEL_ACTION_PATTERN.test(url))\n );\n}\n\nexport function parseGenerativeLanguageRequest(input: RequestInfo):\n | { requestedModel: string; effectiveModel: string; action: string }\n | undefined {\n const match = toRequestUrlString(input).match(/\\/models\\/([^:]+):(\\w+)/);\n if (!match) {\n return undefined;\n }\n\n const [, requestedModel = \"\", action = \"\"] = match;\n return {\n requestedModel,\n effectiveModel: REQUEST_MODEL_FALLBACKS[requestedModel] ?? requestedModel,\n action,\n };\n}\n\nexport function isRecord(value: unknown): value is Record {\n return !!value && typeof value === \"object\";\n}\n\nexport function readString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nexport function pickString(...values: unknown[]): string | undefined {\n for (const value of values) {\n const str = readString(value);\n if (str) {\n return str;\n }\n }\n return undefined;\n}\n\n/**\n * Preserves Cloud Code trace identity for downstream clients by mapping traceId to responseId.\n */\nexport function injectResponseIdFromTrace>(body: T): T {\n const traceId = readString(body.traceId);\n if (!traceId) {\n return body;\n }\n\n const response = body.response;\n if (!isRecord(response)) {\n return body;\n }\n\n if (readString(response.responseId)) {\n return body;\n }\n\n return {\n ...body,\n response: {\n ...response,\n responseId: traceId,\n },\n };\n}\n","/**\n * NOTE: Format/protocol conversion logic is typically in the app layer (Plugin), but is implemented here inside the SDK as a special case.\n * Reason:\n * Format conversion (OpenAI format to Gemini/Agy native and vice versa) is tightly coupled with Agy's unique streaming SSE data parsing,\n * thought chain deduplication, and signature self-healing (Thinking Recovery / Signature Cache) in multi-turn dialogues.\n * Encapsulating this conversion in the SDK completely shields the OpenCode plugin app layer from non-standard API interaction complexities,\n * allowing the plugin to simply call and forward standard OpenAI formatted requests and response streams.\n */\n\nimport type { ToolMapper } from \"./tool-mapper\";\n\ninterface GeminiFunctionCallPart {\n functionCall?: {\n id?: string;\n name: string;\n args?: Record;\n [key: string]: unknown;\n };\n thoughtSignature?: string;\n [key: string]: unknown;\n}\n\ninterface OpenAIToolCall {\n id?: string;\n function?: {\n name?: string;\n arguments?: string;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n}\n\ninterface OpenAIMessage {\n content?: string | null;\n tool_calls?: OpenAIToolCall[];\n [key: string]: unknown;\n}\n\n/**\n * Converts OpenAI's `tool_calls` into Gemini's `functionCall` sections.\n */\nexport function transformOpenAIToolCalls(requestPayload: Record, toolMapper?: ToolMapper): void {\n const messages = requestPayload.messages;\n if (!messages || !Array.isArray(messages)) {\n return;\n }\n\n for (const message of messages) {\n if (!message || typeof message !== \"object\") {\n continue;\n }\n\n const msgObj = message as OpenAIMessage;\n const toolCalls = msgObj.tool_calls;\n if (!toolCalls || !Array.isArray(toolCalls) || toolCalls.length === 0) {\n continue;\n }\n\n const parts: GeminiFunctionCallPart[] = [];\n if (typeof msgObj.content === \"string\" && msgObj.content.length > 0) {\n parts.push({ text: msgObj.content });\n }\n\n for (const toolCall of toolCalls) {\n if (!toolCall || typeof toolCall !== \"object\") {\n continue;\n }\n\n const fn = toolCall.function;\n if (!fn || typeof fn !== \"object\") {\n continue;\n }\n\n const rawName = fn.name ?? \"\";\n const name = toolMapper ? toolMapper.toGemini(rawName) : rawName;\n const args = parseJsonObject(fn.arguments);\n\n const functionCallPart: NonNullable = {\n name,\n args,\n };\n\n if (typeof toolCall.id === 'string' && toolCall.id.length > 0) {\n functionCallPart.id = toolCall.id;\n }\n\n parts.push({\n functionCall: functionCallPart,\n thoughtSignature: \"skip_thought_signature_validator\",\n });\n }\n\n msgObj.parts = parts;\n delete msgObj.tool_calls;\n delete msgObj.content;\n }\n}\n\n/**\n * Adds synthesized thoughtSignature to function calls in the flattened and wrapped payload.\n */\nexport function addThoughtSignaturesToFunctionCalls(requestPayload: Record): void {\n const processContents = (contents: unknown): void => {\n if (!contents || !Array.isArray(contents)) {\n return;\n }\n\n for (const content of contents) {\n if (!content || typeof content !== \"object\") {\n continue;\n }\n\n const parts = (content as Record).parts;\n if (!parts || !Array.isArray(parts)) {\n continue;\n }\n\n for (const part of parts) {\n if (!part || typeof part !== \"object\") {\n continue;\n }\n const partObj = part as Record;\n if (partObj.functionCall && !partObj.thoughtSignature) {\n partObj.thoughtSignature = \"skip_thought_signature_validator\";\n }\n }\n }\n };\n\n processContents(requestPayload.contents);\n if (requestPayload.request && typeof requestPayload.request === \"object\") {\n processContents((requestPayload.request as Record).contents);\n }\n}\n\nfunction parseJsonObject(value: unknown): Record {\n if (typeof value !== \"string\") {\n return {};\n }\n try {\n const parsed = JSON.parse(value);\n if (parsed && typeof parsed === \"object\") {\n return parsed as Record;\n }\n return {};\n } catch {\n return {};\n }\n}\n","/**\n * Tool Name Schema Mapping for Gemini API\n *\n * Gemini API tool schemas strictly enforce function names matching `^[a-zA-Z_][a-zA-Z0-9_]*$`.\n * Agent frameworks (MCP servers, OpenCode plugins, OpenAI adapters) often use hyphens (-), dots (.),\n * slashes (/), or colons (:) in tool names (e.g. `context7_resolve_library_id`, `atlassian:get-issue`, `my-tool`).\n *\n * This module maintains a bidirectional mapping between original client tool names and Gemini-compliant tool names,\n * resolving collisions deterministically and persisting session mappings across multi-turn tool loops.\n */\n\nexport function sanitizeToolName(name: string): string {\n if (!name || typeof name !== \"string\") {\n return \"unnamed_tool\";\n }\n\n // Replace all non-alphanumeric and non-underscore characters with '_'\n let sanitized = name.replace(/[^a-zA-Z0-9_]/g, \"_\");\n\n // Ensure starts with [a-zA-Z_]\n if (!/^[a-zA-Z_]/.test(sanitized)) {\n sanitized = `_${sanitized}`;\n }\n\n return sanitized;\n}\n\nexport class ToolMapper {\n private originalToSanitized = new Map();\n private sanitizedToOriginal = new Map();\n\n /**\n * Register a tool name and get its Gemini-compliant sanitized name.\n * Handles naming collisions by appending a numeric suffix if needed.\n */\n register(originalName: string): string {\n if (!originalName || typeof originalName !== \"string\") {\n return originalName;\n }\n\n const existing = this.originalToSanitized.get(originalName);\n if (existing) {\n return existing;\n }\n\n const baseSanitized = sanitizeToolName(originalName);\n let sanitized = baseSanitized;\n let counter = 1;\n\n // Resolve collision if a different originalName already claimed this sanitized name\n while (this.sanitizedToOriginal.has(sanitized) && this.sanitizedToOriginal.get(sanitized) !== originalName) {\n sanitized = `${baseSanitized}_${counter++}`;\n }\n\n this.originalToSanitized.set(originalName, sanitized);\n this.sanitizedToOriginal.set(sanitized, originalName);\n\n return sanitized;\n }\n\n /**\n * Map an original tool name to sanitized Gemini name.\n * If not already registered, registers it on the fly.\n */\n toGemini(originalName: string): string {\n if (!originalName || typeof originalName !== \"string\") {\n return originalName;\n }\n const sanitized = this.originalToSanitized.get(originalName);\n if (sanitized) {\n return sanitized;\n }\n return this.register(originalName);\n }\n\n /**\n * Restore a sanitized Gemini tool name back to the original client tool name.\n */\n fromGemini(sanitizedName: string): string {\n if (!sanitizedName || typeof sanitizedName !== \"string\") {\n return sanitizedName;\n }\n return this.sanitizedToOriginal.get(sanitizedName) ?? sanitizedName;\n }\n\n /**\n * Register tools from Gemini `tools[].functionDeclarations` array.\n */\n registerFromFunctionDeclarations(tools: unknown): void {\n if (!Array.isArray(tools)) return;\n for (const tool of tools) {\n if (tool && Array.isArray(tool.functionDeclarations)) {\n for (const fn of tool.functionDeclarations) {\n if (fn && typeof fn.name === \"string\") {\n this.register(fn.name);\n }\n }\n }\n }\n }\n\n /**\n * Register tools from OpenAI format `tools[].function.name`.\n */\n registerFromOpenAITools(tools: unknown): void {\n if (!Array.isArray(tools)) return;\n for (const tool of tools) {\n if (tool && typeof tool === \"object\") {\n const fn = (tool as Record).function as Record | undefined;\n if (fn && typeof fn.name === \"string\") {\n this.register(fn.name);\n }\n }\n }\n }\n\n /**\n * Scan contents/messages to register any previously used tool names.\n */\n registerFromContents(contents: unknown): void {\n if (!Array.isArray(contents)) return;\n for (const content of contents) {\n if (!content || typeof content !== \"object\") continue;\n const parts = (content as Record).parts;\n if (Array.isArray(parts)) {\n for (const part of parts) {\n if (!part || typeof part !== \"object\") continue;\n const p = part as Record;\n if (p.functionCall && typeof (p.functionCall as Record).name === \"string\") {\n this.register((p.functionCall as Record).name as string);\n }\n if (p.functionResponse && typeof (p.functionResponse as Record).name === \"string\") {\n this.register((p.functionResponse as Record).name as string);\n }\n }\n }\n }\n }\n}\n\n// Session-based mapper cache\nconst sessionMappers = new Map();\nconst MAX_SESSION_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport function getToolMapper(sessionId?: string): ToolMapper {\n if (!sessionId) {\n return new ToolMapper();\n }\n\n const now = Date.now();\n const existing = sessionMappers.get(sessionId);\n if (existing && now - existing.updatedAt < MAX_SESSION_AGE_MS) {\n existing.updatedAt = now;\n return existing.mapper;\n }\n\n // Cleanup old sessions if cache grows large\n if (sessionMappers.size > 1000) {\n for (const [key, value] of sessionMappers.entries()) {\n if (now - value.updatedAt >= MAX_SESSION_AGE_MS) {\n sessionMappers.delete(key);\n }\n }\n }\n\n const mapper = new ToolMapper();\n sessionMappers.set(sessionId, { mapper, updatedAt: now });\n return mapper;\n}\n\nexport function clearToolMapper(sessionId: string): void {\n sessionMappers.delete(sessionId);\n}\n\n/**\n * Restores original client tool names inside Gemini candidates/parts functionCall objects.\n */\nexport function restoreToolNamesInResponse(body: unknown, toolMapper: ToolMapper): void {\n if (!body || typeof body !== \"object\") return;\n const b = body as Record;\n\n const target = b.response && typeof b.response === \"object\" ? (b.response as Record) : b;\n const candidates = target.candidates;\n if (Array.isArray(candidates)) {\n for (const cand of candidates) {\n if (!cand || typeof cand !== \"object\") continue;\n const content = (cand as Record).content;\n if (!content || typeof content !== \"object\") continue;\n const parts = (content as Record).parts;\n if (Array.isArray(parts)) {\n for (const part of parts) {\n if (!part || typeof part !== \"object\") continue;\n const p = part as Record;\n if (p.functionCall && typeof (p.functionCall as Record).name === \"string\") {\n const fnCall = p.functionCall as Record;\n fnCall.name = toolMapper.fromGemini(fnCall.name as string);\n }\n }\n }\n }\n }\n}\n","\nimport {\n enhanceGeminiErrorResponse,\n extractUsageMetadata,\n parseGeminiApiBody,\n rewriteGeminiPreviewAccessError,\n type GeminiApiBody,\n} from \"../request-helpers\";\nimport { injectResponseIdFromTrace } from \"./shared\";\nimport { cacheSignature } from \"../../plugin/cache\";\nimport { createStreamingTransformer, defaultSignatureStore } from \"./thinking\";\nimport { getTurnStateTracker, type TurnState } from \"./turn-state-tracker\";\nimport type { ChatLogger } from \"../chat-logger\";\nimport { getToolMapper, restoreToolNamesInResponse } from \"./tool-mapper\";\n\n/**\n * Normalizes Gemini/Agy responses, preserving request metadata and usage counters.\n */\nexport async function transformAgyResponse(\n response: Response,\n streaming: boolean,\n _ignoredDebugContext?: any,\n requestedModel?: string,\n sessionId?: string,\n chatLogger?: ChatLogger | null,\n): Promise {\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n const isJsonResponse = contentType.includes(\"application/json\");\n const isEventStreamResponse = contentType.includes(\"text/event-stream\");\n\n if (!isJsonResponse && !isEventStreamResponse) {\n if (chatLogger) {\n chatLogger.logResponseHeaders(response.status, response.statusText, response.headers);\n chatLogger.logResponseBody(\"[Non-JSON response (body omitted)]\");\n chatLogger.close();\n }\n return response;\n }\n\n try {\n const headers = new Headers(response.headers);\n if (chatLogger) {\n chatLogger.logResponseHeaders(response.status, response.statusText, headers);\n }\n\n if (streaming && response.ok && isEventStreamResponse && response.body) {\n return new Response(transformStreamingPayloadStream(response.body, sessionId, chatLogger), {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n }\n\n const text = await response.text();\n if (chatLogger) {\n chatLogger.logResponseBody(text);\n chatLogger.close();\n }\n\n const init = {\n status: response.status,\n statusText: response.statusText,\n headers,\n };\n\n const parsed: GeminiApiBody | null = !streaming || !isEventStreamResponse ? parseGeminiApiBody(text) : null;\n const enhanced = !response.ok && parsed ? enhanceGeminiErrorResponse(parsed, response.status) : null;\n if (enhanced?.retryAfterMs) {\n const retryAfterSec = Math.ceil(enhanced.retryAfterMs / 1000).toString();\n headers.set(\"Retry-After\", retryAfterSec);\n headers.set(\"retry-after-ms\", String(enhanced.retryAfterMs));\n }\n\n const previewPatched = parsed\n ? rewriteGeminiPreviewAccessError(enhanced?.body ?? parsed, response.status, requestedModel)\n : null;\n\n const effectiveBodyRaw = previewPatched ?? enhanced?.body ?? parsed ?? undefined;\n const effectiveBody =\n effectiveBodyRaw && typeof effectiveBodyRaw === \"object\"\n ? injectResponseIdFromTrace(effectiveBodyRaw as Record)\n : effectiveBodyRaw;\n\n if (effectiveBody) {\n const toolMapper = getToolMapper(sessionId);\n restoreToolNamesInResponse(effectiveBody, toolMapper);\n }\n\n attachUsageHeaders(headers, effectiveBody);\n\n if (!parsed) {\n return new Response(text, init);\n }\n\n if (effectiveBody && typeof effectiveBody === \"object\" && \"response\" in effectiveBody) {\n return new Response(JSON.stringify((effectiveBody as { response: unknown }).response), init);\n }\n if (previewPatched) {\n return new Response(JSON.stringify(previewPatched), init);\n }\n\n return new Response(text, init);\n } catch (error) {\n const errStr = error instanceof Error ? error.stack || error.message : String(error);\n console.warn(`[Agy Auth] Failed to transform Gemini/Agy response: ${errStr}`);\n return response;\n }\n}\n\nfunction attachUsageHeaders(headers: Headers, effectiveBody: unknown): void {\n if (!effectiveBody || typeof effectiveBody !== \"object\") {\n return;\n }\n const usage = extractUsageMetadata(effectiveBody as GeminiApiBody);\n if (usage?.cachedContentTokenCount === undefined) {\n return;\n }\n\n headers.set(\"x-gemini-cached-content-token-count\", String(usage.cachedContentTokenCount));\n if (usage.totalTokenCount !== undefined) {\n headers.set(\"x-gemini-total-token-count\", String(usage.totalTokenCount));\n }\n if (usage.promptTokenCount !== undefined) {\n headers.set(\"x-gemini-prompt-token-count\", String(usage.promptTokenCount));\n }\n if (usage.candidatesTokenCount !== undefined) {\n headers.set(\"x-gemini-candidates-token-count\", String(usage.candidatesTokenCount));\n }\n}\n\nfunction transformStreamingPayloadStream(\n stream: ReadableStream,\n sessionId?: string,\n chatLogger?: ChatLogger | null,\n): ReadableStream {\n const callbacks = {\n onCacheSignature: (sessionKey: string, text: string, signature: string) => {\n cacheSignature(sessionKey, text, signature);\n },\n onTurnStateUpdate: (sessionKey: string, state: { turnHasThinking: boolean; lastModelHasToolCalls: boolean }) => {\n const tracker = getTurnStateTracker();\n if (tracker) {\n tracker.updateAfterResponse(sessionKey, {\n inToolLoop: state.lastModelHasToolCalls,\n turnHasThinking: state.turnHasThinking,\n lastModelHasThinking: state.turnHasThinking,\n lastModelHasToolCalls: state.lastModelHasToolCalls,\n });\n }\n },\n transformThinkingParts: (response: unknown) => {\n if (response && typeof response === \"object\") {\n const toolMapper = getToolMapper(sessionId);\n restoreToolNamesInResponse(response, toolMapper);\n return injectResponseIdFromTrace(response as Record);\n }\n return response;\n },\n };\n\n const transformer = createStreamingTransformer(\n defaultSignatureStore,\n callbacks,\n {\n signatureSessionKey: sessionId,\n cacheSignatures: !!sessionId,\n },\n );\n\n if (chatLogger) {\n return stream\n .pipeThrough(chatLogger.createLoggingTransformStream())\n .pipeThrough(transformer);\n }\n\n return stream.pipeThrough(transformer);\n}\n","import { createWriteStream, existsSync, mkdirSync, WriteStream } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { cwd } from \"node:process\";\n\nexport interface ChatLogger {\n logRequest(url: string, method: string, headers: HeadersInit | undefined, body: BodyInit | null | undefined): void;\n logResponseHeaders(status: number, statusText: string, headers: Headers): void;\n logResponseBody(body: string): void;\n createLoggingTransformStream(): TransformStream;\n close(): void;\n}\n\nexport function createChatLogger(): ChatLogger | null {\n if (process.env.AGY_LOG !== \"1\") {\n return null;\n }\n try {\n const logDir = join(cwd(), \"agy_chat_log\");\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n\n const timestamp = Date.now();\n const logFile = join(logDir, `${timestamp}.log`);\n const stream = createWriteStream(logFile, { flags: \"w\", encoding: \"utf8\" });\n\n return new ChatLoggerImpl(stream);\n } catch (error) {\n console.error(\"[Agy Chat Logger] Failed to initialize logger\", error);\n return null;\n }\n}\n\nclass ChatLoggerImpl implements ChatLogger {\n private stream: WriteStream;\n\n constructor(stream: WriteStream) {\n this.stream = stream;\n }\n\n logRequest(url: string, method: string, headers: HeadersInit | undefined, body: BodyInit | null | undefined): void {\n this.stream.write(`=========== REQUEST ===========\\n`);\n this.stream.write(`[Time] ${new Date().toISOString()}\\n`);\n this.stream.write(`[Request] ${method} ${url}\\n`);\n\n if (headers) {\n this.stream.write(`[Headers]\\n`);\n const h = new Headers(headers);\n h.forEach((value, key) => {\n // optionally mask token if needed, but since it's for local debug, we might just write it.\n // Let's mask authorization just in case.\n if (key.toLowerCase() === \"authorization\") {\n this.stream.write(` ${key}: [redacted]\\n`);\n } else {\n this.stream.write(` ${key}: ${value}\\n`);\n }\n });\n }\n\n this.stream.write(`\\n[Body]\\n`);\n if (body) {\n if (typeof body === \"string\") {\n try {\n const parsed = JSON.parse(body);\n this.stream.write(JSON.stringify(parsed, null, 2));\n } catch {\n this.stream.write(body);\n }\n } else {\n this.stream.write(`[Non-string body omitted]`);\n }\n } else {\n this.stream.write(`[No body]`);\n }\n this.stream.write(`\\n\\n`);\n }\n\n logResponseHeaders(status: number, statusText: string, headers: Headers): void {\n this.stream.write(`=========== RESPONSE ===========\\n`);\n this.stream.write(`[Status] ${status} ${statusText}\\n`);\n this.stream.write(`[Headers]\\n`);\n headers.forEach((value, key) => {\n this.stream.write(` ${key}: ${value}\\n`);\n });\n this.stream.write(`\\n[Body / Chunks]\\n`);\n }\n\n logResponseBody(body: string): void {\n this.stream.write(body);\n this.stream.write(`\\n\\n=========== END ===========\\n`);\n }\n\n createLoggingTransformStream(): TransformStream {\n const stream = this.stream;\n const decoder = new TextDecoder();\n return new TransformStream({\n transform(chunk, controller) {\n stream.write(decoder.decode(chunk, { stream: true }));\n controller.enqueue(chunk);\n },\n flush(controller) {\n stream.write(decoder.decode());\n stream.write(`\\n\\n=========== END ===========\\n`);\n }\n });\n }\n\n close(): void {\n try {\n this.stream.end();\n } catch {}\n }\n}\n","/**\n * HTTP header and URL manipulation utilities.\n * Shared between OpenCode v1 and v2 runtimes.\n */\n\nexport function getSafeHeader(headers: unknown, key: string): string | undefined {\n if (!headers) {\n return undefined;\n }\n const targetKey = key.toLowerCase();\n\n if (typeof (headers as any).get === 'function') {\n try {\n return (headers as any).get(targetKey) || undefined;\n } catch {\n // Fallback\n }\n }\n\n if (Array.isArray(headers)) {\n const found = headers.find((item) => {\n if (Array.isArray(item) && typeof item[0] === 'string') {\n return item[0].toLowerCase() === targetKey;\n }\n return false;\n });\n return found ? String(found[1]) : undefined;\n }\n\n if (typeof headers === 'object') {\n const foundKey = Object.keys(headers).find(k => k.toLowerCase() === targetKey);\n return foundKey ? ((headers as Record)[foundKey] !== undefined ? String((headers as Record)[foundKey]) : undefined) : undefined;\n }\n\n return undefined;\n}\n\nexport function setSafeHeaders(initHeaders: unknown, newHeaders: Record): unknown {\n if (typeof globalThis.Headers !== 'undefined') {\n const headers = new globalThis.Headers((initHeaders as any) ?? {});\n for (const [k, v] of Object.entries(newHeaders)) {\n headers.set(k, v);\n }\n return headers;\n }\n\n if (Array.isArray(initHeaders)) {\n const nextHeaders = [...initHeaders];\n for (const [k, v] of Object.entries(newHeaders)) {\n const idx = nextHeaders.findIndex(item => Array.isArray(item) && typeof item[0] === 'string' && item[0].toLowerCase() === k.toLowerCase());\n if (idx !== -1) {\n nextHeaders[idx] = [k, v];\n } else {\n nextHeaders.push([k, v]);\n }\n }\n return nextHeaders;\n }\n\n const nextHeaders: Record = {};\n if (initHeaders && typeof initHeaders === 'object') {\n for (const [k, v] of Object.entries(initHeaders)) {\n nextHeaders[k] = String(v);\n }\n }\n for (const [k, v] of Object.entries(newHeaders)) {\n const existingKey = Object.keys(nextHeaders).find(key => key.toLowerCase() === k.toLowerCase());\n if (existingKey) {\n nextHeaders[existingKey] = v;\n } else {\n nextHeaders[k] = v;\n }\n }\n return nextHeaders;\n}\n\nexport function toUrlString(value: RequestInfo): string {\n if (typeof value === 'string') {\n return value;\n }\n const candidate = (value as Request).url;\n if (candidate) {\n return candidate;\n }\n return value.toString();\n}\n","import { getSafeHeader } from './headers';\n\nexport interface SimpleStaticModel {\n name: string;\n description: string;\n maxTokens: number;\n maxOutputTokens: number;\n toolCall: boolean;\n reasoning: boolean;\n attachment: boolean;\n cost?: {\n input: number;\n output: number;\n cache?: { read: number; write: number };\n };\n}\n\nexport const STATIC_MODELS_SIMPLE: Record = {\n 'gemini-3.8-flash': {\n name: 'Gemini 3.8 Flash',\n description: 'Gemini 3.8 Flash base model. Select tier at runtime.',\n maxTokens: 1048576,\n maxOutputTokens: 65536,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'gemini-3.7-flash': {\n name: 'Gemini 3.7 Flash',\n description: 'Gemini 3.7 Flash base model. Select tier at runtime.',\n maxTokens: 1048576,\n maxOutputTokens: 65536,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'gemini-3.6-flash': {\n name: 'Gemini 3.6 Flash',\n description: 'Gemini 3.6 Flash base model. Select tier at runtime.',\n maxTokens: 1048576,\n maxOutputTokens: 65536,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'gemini-3.1-pro': {\n name: 'Gemini 3.1 Pro',\n description: 'Gemini 3.1 Pro base model. Select tier at runtime.',\n maxTokens: 1048576,\n maxOutputTokens: 65535,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'claude-sonnet-4-6': {\n name: 'Claude Sonnet 4.6 (Thinking)',\n description: 'Claude Sonnet 4.6 deep reasoning model, perfectly balancing thinking process, processing speed, and output quality.',\n maxTokens: 250000,\n maxOutputTokens: 64000,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'claude-opus-4-6-thinking': {\n name: 'Claude Opus 4.6 (Thinking)',\n description: 'Claude Opus 4.6 deep reasoning model, built-in chain of thought, highly suitable for top-tier algorithm and logic puzzles.',\n maxTokens: 250000,\n maxOutputTokens: 64000,\n toolCall: true,\n reasoning: true,\n attachment: true\n },\n 'gpt-oss-120b-medium': {\n name: 'GPT-OSS 120B (Medium)',\n description: 'GPT open-source 120B parameter medium tier model, excellent performance in local deployment or specific open-source benchmarks.',\n maxTokens: 131072,\n maxOutputTokens: 32768,\n toolCall: true,\n reasoning: true,\n attachment: false\n }\n};\n\nexport const TIER_MAPPING: Record> = {\n 'gemini-3.8-flash': {\n low: 'gemini-3.8-flash-low',\n medium: 'gemini-3.8-flash-medium',\n high: 'gemini-3.8-flash-high'\n },\n 'gemini-3.7-flash': {\n low: 'gemini-3.7-flash-low',\n medium: 'gemini-3.7-flash-medium',\n high: 'gemini-3.7-flash-high'\n },\n 'gemini-3.6-flash': {\n minimal: 'gemini-3.6-flash-low',\n low: 'gemini-3.6-flash-low',\n medium: 'gemini-3.6-flash-medium',\n high: 'gemini-3.6-flash-high'\n },\n 'gemini-3.1-pro': {\n low: 'gemini-3.1-pro-low',\n high: 'gemini-3.1-pro-high'\n }\n};\n\n/**\n * Resolves a model ID and optional request headers or suffix into the appropriate tier variant.\n */\nexport function resolveModelTier(\n baseModelId: string,\n headersOrInit?: RequestInit | unknown\n): string {\n const parts = baseModelId.split('@');\n const base = parts[0] || '';\n const suffixTier = parts[1]?.toLowerCase();\n\n const mapping = TIER_MAPPING[base];\n if (!mapping) {\n return baseModelId;\n }\n\n const rawHeaders =\n headersOrInit && typeof headersOrInit === 'object' && 'headers' in headersOrInit\n ? (headersOrInit as RequestInit).headers\n : headersOrInit;\n\n const headerTier = getSafeHeader(rawHeaders, 'x-agy-tier')?.toLowerCase() || null;\n const requestedTier = headerTier || suffixTier;\n\n if (requestedTier && Object.prototype.hasOwnProperty.call(mapping, requestedTier)) {\n return mapping[requestedTier] || baseModelId;\n }\n\n return mapping['medium'] ?? mapping['high'] ?? baseModelId;\n}\n","import { updateStaticModelsWithPricing } from './plugin/pricing';\nimport type { Config } from './plugin/types';\nimport { AGY_PROVIDER_ID } from './constants';\nimport { agyFetch } from './fetch';\nimport { createOAuthAuthorizeMethod } from './plugin/oauth-authorize';\nimport { accessTokenExpired, isOAuthAuth, parseRefreshParts } from './plugin/auth';\nimport { resolveCachedAuth, initDiskSignatureCache } from './plugin/cache';\nimport { initTurnStateTracker } from './sdk/request/turn-state-tracker';\nimport { initCooldownPersistence } from './sdk/retry';\nimport { ensureProjectContext, retrieveUserQuota, retrieveUserQuotaSummary } from './plugin/project';\nimport { createAgyQuotaTool, AGY_QUOTA_TOOL_NAME } from './plugin/quota';\nimport { createAgyQuotaSummaryTool, AGY_QUOTA_SUMMARY_TOOL_NAME } from './plugin/quota-summary';\nimport { maybeShowAgyCapacityToast, maybeShowAgyTestToast } from './plugin/notify';\nimport { simulateClientBackgroundTraffic } from './plugin/traffic';\nimport { buildAgyCliUserAgent } from './sdk/user-agent';\nimport {\n resolveConfiguredProjectId,\n resolveConfiguredProjectIdFromClient,\n resolveConfiguredProjectIdFromConfig\n} from './plugin/provider';\nimport {\n isGenerativeLanguageRequest,\n parseGenerativeLanguageRequest,\n prepareAgyRequest,\n type ThinkingConfigDefaults,\n transformAgyResponse\n} from './sdk/request';\nimport { createChatLogger } from './sdk/chat-logger';\nimport { fetchWithRetry } from './sdk/retry';\nimport { refreshAccessToken } from './plugin/token';\nimport type {\n GetAuth,\n OAuthAuthDetails,\n PluginClient,\n PluginContext,\n PluginResult,\n Provider,\n ProviderModel,\n ProviderV2\n} from './plugin/types';\n\nimport {\n STATIC_MODELS_SIMPLE,\n TIER_MAPPING,\n resolveModelTier,\n type SimpleStaticModel\n} from './plugin/tier';\nimport { getSafeHeader, setSafeHeaders, toUrlString } from './plugin/headers';\nexport {\n STATIC_MODELS_SIMPLE,\n TIER_MAPPING,\n resolveModelTier,\n type SimpleStaticModel\n};\n\nconst AGY_QUOTA_COMMAND = 'agyquota';\nconst AGY_QUOTA_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota usage for the current authenticated account.\n\nImmediately call \\`${AGY_QUOTA_TOOL_NAME}\\` with no arguments and return its output verbatim.\nDo not call other tools.\n`;\n\nconst AGY_QUOTA_SUMMARY_COMMAND = 'agyquotasummary';\nconst AGY_QUOTA_SUMMARY_COMMAND_TEMPLATE = `Retrieve Agy Code Assist quota summary (weekly and 5-hour limits by model group) for the current authenticated account.\n\nImmediately call \\`${AGY_QUOTA_SUMMARY_TOOL_NAME}\\` with no arguments and return its output verbatim.\nDo not call other tools.\n`;\nlet latestAgyAuthResolver: GetAuth | undefined;\nlet latestAgyConfiguredProjectId: string | undefined;\nlet latestAgyUserAgentModel: string | undefined;\n\nconst buildModelFromSimple = (modelId: string, simple: SimpleStaticModel): ProviderModel => {\n const isClaude = modelId.startsWith('claude-');\n const isGpt = modelId.startsWith('gpt-');\n\n let variants: any = undefined;\n if (TIER_MAPPING[modelId]) {\n const hasMinimal = TIER_MAPPING[modelId].minimal !== undefined;\n variants = {\n ...(hasMinimal ? {\n 'minimal': {\n id: 'minimal', name: 'minimal', displayName: 'minimal',\n title: 'minimal', label: 'minimal',\n options: { name: 'minimal' },\n headers: { 'x-agy-tier': 'minimal' },\n thinkingConfig: { thinkingBudget: 1000, includeThoughts: true }\n }\n } : {}),\n 'low': { id: 'low', name: 'low', displayName: 'low', title: 'low', label: 'low', options: { name: 'low' }, headers: { 'x-agy-tier': 'low' } },\n ...(TIER_MAPPING[modelId].medium !== undefined ? { 'medium': { id: 'medium', name: 'medium', displayName: 'medium', title: 'medium', label: 'medium', options: { name: 'medium' }, headers: { 'x-agy-tier': 'medium' } } } : {}),\n 'high': { id: 'high', name: 'high', displayName: 'high', title: 'high', label: 'high', options: { name: 'high' }, headers: { 'x-agy-tier': 'high' } }\n };\n }\n\n return {\n id: modelId,\n providerID: AGY_PROVIDER_ID,\n api: {\n id: AGY_PROVIDER_ID,\n url: 'https://cloudcode-pa.googleapis.com',\n npm: '@ai-sdk/google'\n },\n name: simple.name,\n family: modelId.includes('gemini') ? 'gemini' : (isClaude ? 'claude' : (isGpt ? 'gpt' : 'unknown')),\n status: 'active',\n release_date: '2026-05-26',\n // Root-level properties for OpenCode v2 cache compatibility\n reasoning: simple.reasoning,\n attachment: simple.attachment,\n tool_call: simple.toolCall,\n temperature: true,\n modalities: {\n input: [\n 'text',\n ...(simple.attachment ? ['image', 'pdf'] : []),\n ...(!isClaude && !isGpt ? ['audio', 'video'] : [])\n ],\n output: ['text']\n },\n capabilities: {\n temperature: true,\n reasoning: simple.reasoning,\n attachment: simple.attachment,\n toolcall: simple.toolCall,\n input: {\n text: true,\n image: simple.attachment,\n pdf: simple.attachment,\n audio: !isClaude && !isGpt,\n video: !isClaude && !isGpt\n },\n output: {\n text: true,\n image: false,\n pdf: false,\n audio: false,\n video: false\n },\n interleaved: false\n },\n cost: {\n input: simple.cost?.input ?? 0,\n output: simple.cost?.output ?? 0,\n cache: {\n read: simple.cost?.cache?.read ?? 0,\n write: simple.cost?.cache?.write ?? 0\n }\n },\n limit: {\n context: simple.maxTokens,\n output: simple.maxOutputTokens\n },\n options: {\n description: simple.description\n },\n headers: {},\n variants\n } as ProviderModel & Record;\n};\n\nconst STATIC_MODELS: Record = {};\nfor (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) {\n STATIC_MODELS[modelId] = buildModelFromSimple(modelId, simple);\n}\n\n/**\n * Registers the Agy OAuth provider for Opencode.\n */\nexport const AgyCLIOAuthPlugin = async ({ client }: PluginContext): Promise => {\n let latestConfig: Config | undefined;\n\n // Dynamically update STATIC_MODELS with latest pricing from models.dev\n updateStaticModelsWithPricing(STATIC_MODELS);\n\n const getModelsList = (provider: ProviderV2): Record => {\n const userModels = provider.models || {};\n const clonedStaticModels = JSON.parse(JSON.stringify(STATIC_MODELS));\n const strictModels: Record = {};\n\n for (const [modelId, modelDetails] of Object.entries(clonedStaticModels as Record)) {\n const existing = (userModels[modelId] || {}) as Partial;\n strictModels[modelId] = {\n ...modelDetails,\n ...existing,\n reasoning: (existing as any).reasoning ?? (modelDetails as any).reasoning,\n attachment: (existing as any).attachment ?? (modelDetails as any).attachment,\n tool_call: (existing as any).tool_call ?? (modelDetails as any).tool_call,\n temperature: (existing as any).temperature ?? (modelDetails as any).temperature,\n modalities: {\n ...((modelDetails as any).modalities || {}),\n ...((existing as any).modalities || {})\n },\n capabilities: {\n ...(modelDetails.capabilities || {}),\n ...(existing.capabilities || {}),\n input: {\n ...(modelDetails.capabilities?.input || {}),\n ...(existing.capabilities?.input || {})\n },\n output: {\n ...(modelDetails.capabilities?.output || {}),\n ...(existing.capabilities?.output || {})\n }\n }\n } as ProviderModel;\n }\n\n provider.models = strictModels;\n\n if (latestConfig && latestConfig.provider && latestConfig.provider[AGY_PROVIDER_ID]) {\n latestConfig.provider[AGY_PROVIDER_ID].models = provider.models;\n }\n normalizeProviderModelCosts(provider);\n\n return provider.models;\n };\n\n try {\n initDiskSignatureCache({\n enabled: true,\n memory_ttl_seconds: 3600,\n disk_ttl_seconds: 86400,\n write_interval_seconds: 30\n });\n } catch (e) {\n console.warn(`[Agy Auth] initDiskSignatureCache failed, running without signature disk cache: ${e instanceof Error ? e.message : e}`);\n }\n\n try {\n initTurnStateTracker();\n } catch (e) {\n console.warn(`[Agy Auth] initTurnStateTracker failed, running without turn-state tracking: ${e instanceof Error ? e.message : e}`);\n }\n\n try {\n initCooldownPersistence();\n } catch (e) {\n console.warn(`[Agy Auth] initCooldownPersistence failed, running without cooldown disk persistence: ${e instanceof Error ? e.message : e}`);\n }\n\n const resolveLatestConfiguredProjectId = async (provider?: Provider): Promise => {\n const configProjectId = (await resolveConfiguredProjectIdFromClient(client)) ?? latestAgyConfiguredProjectId;\n const resolvedProjectId = resolveConfiguredProjectId({\n provider,\n configProjectId\n });\n latestAgyConfiguredProjectId = resolvedProjectId;\n return resolvedProjectId;\n };\n\n return {\n config: async (config) => {\n latestConfig = config;\n latestAgyConfiguredProjectId = resolveConfiguredProjectIdFromConfig(config);\n config.command = config.command || {};\n config.command[AGY_QUOTA_COMMAND] = {\n description: 'Show Agy Code Assist quota usage',\n template: AGY_QUOTA_COMMAND_TEMPLATE\n };\n config.command[AGY_QUOTA_SUMMARY_COMMAND] = {\n description: 'Show Agy Code Assist quota summary with weekly and 5-hour limits',\n template: AGY_QUOTA_SUMMARY_COMMAND_TEMPLATE\n };\n\n // Dynamically registers the google-agy provider config to make it work seamlessly without manual user mapping.\n config.provider = config.provider || {};\n config.provider[AGY_PROVIDER_ID] = {\n npm: '@ai-sdk/google',\n name: 'Antigravity CLI',\n options: {},\n models: {},\n ...config.provider[AGY_PROVIDER_ID]\n };\n\n // Merge custom config from the user with the static defaults\n config.provider[AGY_PROVIDER_ID].models = getModelsList(config.provider[AGY_PROVIDER_ID] as ProviderV2);\n },\n tool: {\n [AGY_QUOTA_TOOL_NAME]: createAgyQuotaTool({\n client,\n getAuthResolver: () => latestAgyAuthResolver,\n getConfiguredProjectId: () => latestAgyConfiguredProjectId,\n getUserAgentModel: () => latestAgyUserAgentModel\n }),\n [AGY_QUOTA_SUMMARY_TOOL_NAME]: createAgyQuotaSummaryTool({\n client,\n getAuthResolver: () => latestAgyAuthResolver,\n getConfiguredProjectId: () => latestAgyConfiguredProjectId,\n getUserAgentModel: () => latestAgyUserAgentModel\n })\n },\n auth: {\n provider: AGY_PROVIDER_ID,\n loader: async (getAuth: GetAuth, provider: Provider): Promise => {\n latestAgyAuthResolver = getAuth;\n const auth = await getAuth();\n if (!isOAuthAuth(auth)) {\n return null;\n }\n\n const configuredProjectId = await resolveLatestConfiguredProjectId(provider);\n normalizeProviderModelCosts(provider);\n const thinkingConfigDefaults = resolveThinkingConfigDefaults(provider);\n\n return {\n apiKey: '',\n async fetch(input: RequestInfo, init?: RequestInit) {\n const isGL = isGenerativeLanguageRequest(input);\n const isInternal = toUrlString(input).includes('cloudcode-pa.googleapis.com');\n\n if (!isGL && !isInternal) {\n return agyFetch(input, init);\n }\n\n const latestAuth = await getAuth();\n if (!isOAuthAuth(latestAuth)) {\n return agyFetch(input, init);\n }\n\n let authRecord = resolveCachedAuth(latestAuth);\n if (accessTokenExpired(authRecord)) {\n const refreshed = await refreshAccessToken(authRecord, client);\n if (!refreshed) {\n return agyFetch(input, init);\n }\n authRecord = refreshed;\n }\n\n if (!authRecord.access) {\n return agyFetch(input, init);\n }\n\n const configuredProjectId = await resolveLatestConfiguredProjectId(provider);\n\n if (isInternal) {\n const hasAuth = getSafeHeader(init?.headers, 'Authorization') !== undefined;\n if (hasAuth) {\n return agyFetch(input, init);\n }\n\n const userAgent = buildAgyCliUserAgent(latestAgyUserAgentModel);\n const headers = setSafeHeaders(init?.headers, {\n 'Authorization': `Bearer ${authRecord.access}`,\n 'User-Agent': userAgent\n });\n\n if (configuredProjectId) {\n simulateClientBackgroundTraffic(authRecord.access, configuredProjectId, latestAgyUserAgentModel);\n }\n\n return agyFetch(input, {\n ...init,\n headers: headers as any\n });\n }\n const requestTarget = parseGenerativeLanguageRequest(input);\n const requestUserAgentModel = requestTarget?.effectiveModel;\n if (requestUserAgentModel) {\n latestAgyUserAgentModel = requestUserAgentModel;\n }\n const projectContext = await ensureProjectContextOrThrow(\n authRecord,\n client,\n configuredProjectId,\n requestUserAgentModel\n );\n await maybeShowAgyTestToast(client, projectContext.effectiveProjectId);\n\n const originalRequestedModel = parseGenerativeLanguageRequest(input)?.effectiveModel;\n let modifiedInput = input;\n if (isGL && originalRequestedModel) {\n const originalBase = originalRequestedModel.replace('google-agy/', '');\n const resolvedBase = resolveModelTier(originalBase, init);\n if (originalBase !== resolvedBase) {\n if (typeof modifiedInput === 'string') {\n modifiedInput = modifiedInput.replace(`models/${originalBase}`, `models/${resolvedBase}`);\n } else if (typeof Request !== 'undefined' && modifiedInput instanceof Request) {\n const newUrl = modifiedInput.url.replace(`models/${originalBase}`, `models/${resolvedBase}`);\n modifiedInput = new Request(newUrl, modifiedInput);\n }\n }\n }\n\n const parts = parseRefreshParts(authRecord.refresh);\n const transformed = prepareAgyRequest(\n modifiedInput,\n init,\n authRecord.access,\n projectContext.effectiveProjectId,\n thinkingConfigDefaults\n );\n const chatLogger = createChatLogger();\n if (chatLogger) {\n chatLogger.logRequest(\n toUrlString(transformed.request),\n transformed.init.method || 'GET',\n transformed.init.headers,\n transformed.init.body\n );\n }\n\n const response = await fetchWithRetry(transformed.request, transformed.init);\n if (response.ok && authRecord.access && projectContext.effectiveProjectId) {\n simulateClientBackgroundTraffic(\n authRecord.access,\n projectContext.effectiveProjectId,\n requestUserAgentModel\n );\n }\n await maybeShowAgyCapacityToast(\n client,\n response,\n projectContext.effectiveProjectId,\n transformed.requestedModel\n );\n return transformAgyResponse(\n response,\n transformed.streaming,\n null,\n transformed.requestedModel,\n transformed.sessionId,\n chatLogger\n );\n }\n };\n },\n methods: [\n {\n label: 'Google OAuth (Antigravity CLI)',\n type: 'oauth',\n authorize: createOAuthAuthorizeMethod({\n client,\n getConfiguredProjectId: () => resolveLatestConfiguredProjectId(),\n getUserAgentModel: () => latestAgyUserAgentModel\n })\n },\n {\n label: 'Manually enter API Key',\n type: 'api'\n }\n ]\n },\n provider: {\n id: AGY_PROVIDER_ID,\n models: async (provider: any, ctx: any): Promise => {\n let auth = ctx?.auth;\n if (!auth && latestAgyAuthResolver) {\n try {\n auth = await latestAgyAuthResolver();\n } catch (e) {\n const errStr = e instanceof Error ? e.stack || e.message : String(e);\n console.warn(`[Agy Auth] Failed to resolve auth from resolver in models hook: ${errStr}`);\n }\n }\n\n if (!auth || !isOAuthAuth(auth)) {\n return {\n 'login-required': {\n name: 'No models available'\n }\n };\n }\n\n const models = getModelsList(provider);\n // Ensure dynamic pricing costs are strictly enforced to override any cached $0 fallbacks\n for (const [modelId, model] of Object.entries(models)) {\n if (STATIC_MODELS[modelId] && STATIC_MODELS[modelId].cost) {\n model.cost = STATIC_MODELS[modelId].cost;\n }\n }\n return models;\n }\n } as any\n };\n};\n\nexport const GoogleOAuthPlugin = AgyCLIOAuthPlugin;\n\nfunction normalizeProviderModelCosts(provider: Provider | ProviderV2): void {\n if (!provider?.models || typeof provider.models !== 'object') {\n return;\n }\n for (const [modelId, model] of Object.entries(provider.models)) {\n if (!model || typeof model !== 'object') {\n continue;\n }\n const existingCost = model.cost;\n const isValidCost =\n existingCost &&\n typeof existingCost === 'object' &&\n typeof existingCost.input === 'number' &&\n typeof existingCost.output === 'number';\n const normalizedCost = {\n input: isValidCost ? existingCost.input : 0,\n output: isValidCost ? existingCost.output : 0,\n cache: {\n read:\n isValidCost &&\n typeof existingCost.cache === 'object' &&\n existingCost.cache !== null &&\n typeof (existingCost.cache as { read?: number }).read === 'number'\n ? (existingCost.cache as { read: number }).read\n : 0,\n write:\n isValidCost &&\n typeof existingCost.cache === 'object' &&\n existingCost.cache !== null &&\n typeof (existingCost.cache as { write?: number }).write === 'number'\n ? (existingCost.cache as { write: number }).write\n : 0\n }\n };\n model.cost = normalizedCost;\n }\n}\n\nfunction resolveThinkingConfigDefaults(provider: Provider): ThinkingConfigDefaults | undefined {\n const providerOptions =\n provider && typeof provider === 'object'\n ? ((provider as { options?: Record }).options ?? undefined)\n : undefined;\n const providerThinkingConfig = providerOptions?.thinkingConfig;\n\n const modelThinkingConfigByModel: Record = {};\n for (const [modelId, model] of Object.entries(provider?.models ?? {})) {\n if (!model || typeof model !== 'object') {\n continue;\n }\n const modelOptions = (model as { options?: Record }).options;\n if (modelOptions && typeof modelOptions === 'object' && 'thinkingConfig' in modelOptions) {\n modelThinkingConfigByModel[modelId] = modelOptions.thinkingConfig;\n }\n }\n\n if (providerThinkingConfig === undefined && Object.keys(modelThinkingConfigByModel).length === 0) {\n return undefined;\n }\n\n return {\n provider: providerThinkingConfig,\n models: modelThinkingConfigByModel\n };\n}\n\nasync function ensureProjectContextOrThrow(\n authRecord: OAuthAuthDetails,\n client: PluginClient,\n configuredProjectId?: string,\n userAgentModel?: string\n) {\n try {\n return await ensureProjectContext(authRecord, client, configuredProjectId, userAgentModel);\n } catch (error) {\n if (error instanceof Error) {\n console.warn(`[Agy Auth] ensureProjectContextOrThrow error: ${error.message}`);\n }\n throw error;\n }\n}\n","import type { OpencodeClient, Auth } from '@opencode-ai/sdk';\nimport type { Provider as ProviderV1 } from '@opencode-ai/sdk';\nimport type { Model as ModelV2 } from '@opencode-ai/sdk/v2';\nimport type { Hooks, Config as PluginConfig } from '@opencode-ai/plugin';\n\nexport type OAuthAuthDetails = Extract;\nexport type AuthDetails = Auth;\nexport type GetAuth = () => Promise;\n\nexport type Provider = ProviderV1;\nexport type ProviderModel = ModelV2;\n\nexport type Config = PluginConfig;\n\nexport interface LoaderResult {\n apiKey: string;\n fetch(input: RequestInfo, init?: RequestInit): Promise;\n}\n\nexport type PluginClient = OpencodeClient;\n\nexport interface PluginContext {\n client: PluginClient;\n}\n\nexport type PluginResult = Hooks;\n\nexport interface RefreshParts {\n refreshToken: string;\n projectId?: string;\n managedProjectId?: string;\n}\n\nexport interface ProjectContextResult {\n auth: OAuthAuthDetails;\n effectiveProjectId: string;\n}\n\n// Export V2 types explicitly for use in provider hooks\nexport type { Provider as ProviderV2, Model as ModelV2 } from '@opencode-ai/sdk/v2';\n\n// OpenCode v2 Plugin Contracts (without taking @opencode/plugin runtime dependency)\n\nexport interface OpenCodeV2TransformRegistry {\n transform: (fn: (target: T) => Promise | void) => void;\n}\n\nexport interface OpenCodeV2SessionRegistry {\n hook: (name: string, handler: (...args: any[]) => any, options?: { providerID?: string }) => void;\n}\n\nexport interface OpenCodeV2PluginLocation {\n root?: string;\n workspace?: string;\n}\n\nexport interface OpenCodeV2PluginContext {\n catalog?: OpenCodeV2TransformRegistry;\n provider?: OpenCodeV2TransformRegistry;\n model?: OpenCodeV2TransformRegistry;\n tool: OpenCodeV2TransformRegistry;\n command: OpenCodeV2TransformRegistry;\n session: OpenCodeV2SessionRegistry;\n aisdk?: {\n hook: (name: 'sdk' | 'language', handler: (event: any) => Promise | void) => void;\n };\n integration?: OpenCodeV2TransformRegistry | {\n transform?: (transformer: (editor: any) => void) => void;\n };\n location: OpenCodeV2PluginLocation;\n}\n\nexport interface OpenCodeV2PluginDefinition {\n id: string;\n setup: (ctx: OpenCodeV2PluginContext) => Promise | void;\n}\n\nexport interface DualOpenCodePlugin extends OpenCodeV2PluginDefinition {\n server: (options: any) => Promise;\n}\n\nexport function defineOpenCodeV2Plugin(\n def: OpenCodeV2PluginDefinition,\n): OpenCodeV2PluginDefinition {\n return def;\n}\n","import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';\nimport { randomUUID } from 'crypto';\nimport { homedir } from 'os';\nimport { join } from 'path';\nimport { AGY_PROVIDER_ID } from '../constants';\n\nlet storedAuthOverrideForTesting: any = undefined;\n\nexport function setStoredAuthOverrideForTesting(override: any): void {\n storedAuthOverrideForTesting = override;\n}\n\nexport function loadStoredAuthFromJson(): any {\n if (storedAuthOverrideForTesting !== undefined) {\n return storedAuthOverrideForTesting;\n }\n const authPath = join(homedir(), '.local', 'share', 'opencode', 'auth.json');\n try {\n if (existsSync(authPath)) {\n const content = readFileSync(authPath, 'utf8');\n const data = JSON.parse(content);\n return data?.[AGY_PROVIDER_ID];\n }\n } catch {\n // Ignore read errors\n }\n return undefined;\n}\n\nexport function saveStoredAuthToJson(authRecord: any): void {\n // Never overwrite the user's real auth.json during tests or when testing override is active\n if (process.env.NODE_ENV === 'test' || process.env.VITEST || storedAuthOverrideForTesting !== undefined) {\n return;\n }\n const dirPath = join(homedir(), '.local', 'share', 'opencode');\n if (!existsSync(dirPath)) {\n mkdirSync(dirPath, { recursive: true });\n }\n const authPath = join(dirPath, 'auth.json');\n const data: Record = existsSync(authPath)\n ? JSON.parse(readFileSync(authPath, 'utf8'))\n : {};\n data[AGY_PROVIDER_ID] = {\n type: 'oauth',\n refresh: authRecord.refresh,\n access: authRecord.access,\n expires: authRecord.expires,\n };\n\n const tempPath = `${authPath}.${process.pid}.${randomUUID()}.tmp`;\n let tempFileDescriptor: number | undefined;\n let ownsTempFile = false;\n try {\n tempFileDescriptor = openSync(tempPath, 'wx', 0o600);\n ownsTempFile = true;\n writeFileSync(tempFileDescriptor, JSON.stringify(data, null, 2), {\n encoding: 'utf8',\n });\n closeSync(tempFileDescriptor);\n tempFileDescriptor = undefined;\n renameSync(tempPath, authPath);\n } catch (error) {\n if (tempFileDescriptor !== undefined) {\n try {\n closeSync(tempFileDescriptor);\n } catch {\n // Preserve the original persistence error.\n }\n }\n if (ownsTempFile) {\n try {\n unlinkSync(tempPath);\n } catch {\n // The temporary file has already been renamed or removed.\n }\n }\n throw error;\n }\n}\n","import { buildAgyCliUserAgent } from '../sdk/user-agent';\nimport {\n prepareAgyRequest,\n transformAgyResponse,\n isGenerativeLanguageRequest,\n parseGenerativeLanguageRequest\n} from '../sdk/request';\nimport { createChatLogger } from '../sdk/chat-logger';\nimport { fetchWithRetry } from '../sdk/retry';\nimport { resolveCachedAuth } from './cache';\nimport {\n accessTokenExpired,\n parseRefreshParts,\n isOAuthAuth\n} from './auth';\nimport { refreshAccessToken } from './token';\nimport { ensureProjectContext } from './project';\nimport { getSafeHeader, setSafeHeaders, toUrlString } from './headers';\nimport { resolveModelTier } from './tier';\nimport { loadStoredAuthFromJson } from './v2-storage';\n\nexport function createV2FetchInterceptor(getAuthSnapshot?: () => Promise | any) {\n return async function agyV2Fetch(input: RequestInfo, init?: RequestInit): Promise {\n const isGL = isGenerativeLanguageRequest(input);\n const isInternal = toUrlString(input).includes('cloudcode-pa.googleapis.com');\n\n if (!isGL && !isInternal) {\n return (globalThis.fetch || fetch)(input, init);\n }\n\n let rawAuth = getAuthSnapshot ? await getAuthSnapshot() : undefined;\n if (!rawAuth) {\n rawAuth = loadStoredAuthFromJson();\n }\n\n if (!rawAuth || !isOAuthAuth(rawAuth)) {\n return (globalThis.fetch || fetch)(input, init);\n }\n\n let authRecord = resolveCachedAuth(rawAuth);\n if (accessTokenExpired(authRecord)) {\n const refreshed = await refreshAccessToken(authRecord, { auth: { set: async () => {} } } as any);\n if (refreshed) {\n authRecord = refreshed;\n }\n }\n\n if (!authRecord.access) {\n return (globalThis.fetch || fetch)(input, init);\n }\n\n if (isInternal) {\n const hasAuth = getSafeHeader(init?.headers, 'Authorization') !== undefined;\n if (hasAuth) {\n return (globalThis.fetch || fetch)(input, init);\n }\n\n const userAgent = buildAgyCliUserAgent();\n const headers = setSafeHeaders(init?.headers, {\n Authorization: `Bearer ${authRecord.access}`,\n 'User-Agent': userAgent\n });\n\n return (globalThis.fetch || fetch)(input, {\n ...init,\n headers: headers as any\n });\n }\n\n const requestTarget = parseGenerativeLanguageRequest(input);\n const requestUserAgentModel = requestTarget?.effectiveModel;\n\n let projectContext: { effectiveProjectId: string; auth: any };\n try {\n projectContext = await ensureProjectContext(\n authRecord,\n { auth: { set: async () => {} } } as any,\n undefined,\n requestUserAgentModel\n );\n } catch {\n projectContext = {\n effectiveProjectId: parseRefreshParts(authRecord.refresh).projectId || 'antigravity',\n auth: authRecord\n };\n }\n\n const originalRequestedModel = parseGenerativeLanguageRequest(input)?.effectiveModel;\n let modifiedInput = input;\n if (isGL && originalRequestedModel) {\n const originalBase = originalRequestedModel.replace('google-agy/', '');\n const resolvedBase = resolveModelTier(originalBase, init);\n if (originalBase !== resolvedBase) {\n if (typeof modifiedInput === 'string') {\n modifiedInput = modifiedInput.replace(`models/${originalBase}`, `models/${resolvedBase}`);\n } else if (typeof Request !== 'undefined' && modifiedInput instanceof Request) {\n const newUrl = modifiedInput.url.replace(`models/${originalBase}`, `models/${resolvedBase}`);\n modifiedInput = new Request(newUrl, modifiedInput);\n }\n }\n }\n\n const transformed = prepareAgyRequest(\n modifiedInput,\n init,\n authRecord.access,\n projectContext.effectiveProjectId,\n undefined\n );\n\n const chatLogger = createChatLogger();\n if (chatLogger) {\n chatLogger.logRequest(\n toUrlString(transformed.request),\n transformed.init.method || 'GET',\n transformed.init.headers,\n transformed.init.body\n );\n }\n\n const response = await fetchWithRetry(transformed.request, transformed.init);\n return transformAgyResponse(\n response,\n transformed.streaming,\n null,\n transformed.requestedModel,\n transformed.sessionId,\n chatLogger\n );\n };\n}\n","import { AGY_PROVIDER_ID } from '../constants';\nimport { buildAgyCliUserAgent } from '../sdk/user-agent';\nimport {\n isGenerativeLanguageRequest,\n parseGenerativeLanguageRequest,\n prepareAgyRequest,\n transformAgyResponse\n} from '../sdk/request';\nimport { classifyQuotaResponse, retryInternals } from '../sdk/retry/quota';\nimport { resolveRetryDelayMs } from '../sdk/retry/helpers';\nimport { resolveCachedAuth } from './cache';\nimport { accessTokenExpired, isOAuthAuth, parseRefreshParts } from './auth';\nimport { refreshAccessToken } from './token';\nimport { ensureProjectContext } from './project';\nimport { loadStoredAuthFromJson, saveStoredAuthToJson } from './v2-storage';\nimport { toUrlString } from './headers';\nimport { resolveModelTier } from './tier';\n\nexport interface V2SessionHooksState {\n lastInterceptedModel?: string;\n}\n\nexport function createV2HttpRequestHook(state: V2SessionHooksState = {}) {\n return async (event: any) => {\n if (!event) return;\n\n const rawUrl = typeof event.url === 'string' ? event.url : (event.request?.url || '');\n const isGL = isGenerativeLanguageRequest(rawUrl);\n const isInternal = rawUrl.includes('cloudcode-pa.googleapis.com');\n\n if (!isGL && !isInternal) {\n return;\n }\n\n event.headers = event.headers || {};\n let modelName: string | undefined;\n\n // Load auth record\n let rawAuth = event.auth;\n if (rawAuth && !rawAuth.type) {\n const tokenVal = rawAuth.access || rawAuth.token;\n if (tokenVal || rawAuth.refresh) {\n rawAuth = { type: 'oauth', access: tokenVal, ...rawAuth };\n }\n }\n if (!rawAuth || !isOAuthAuth(rawAuth)) {\n rawAuth = loadStoredAuthFromJson();\n }\n\n let authRecord = rawAuth && isOAuthAuth(rawAuth) ? resolveCachedAuth(rawAuth) : undefined;\n if (authRecord && accessTokenExpired(authRecord)) {\n const refreshed = await refreshAccessToken(authRecord, { auth: { set: async () => {} } } as any);\n if (refreshed) {\n authRecord = refreshed;\n try {\n saveStoredAuthToJson(refreshed);\n } catch {\n console.warn('[Agy Auth] Refreshed credentials could not be persisted; the current request will continue.');\n }\n }\n }\n\n if (isGL) {\n const parsed = parseGenerativeLanguageRequest(rawUrl);\n modelName = parsed?.effectiveModel;\n if (modelName) {\n state.lastInterceptedModel = modelName;\n }\n\n // Strip x-goog-api-key and api-key from headers\n if (typeof event.headers.delete === 'function') {\n event.headers.delete('x-goog-api-key');\n event.headers.delete('api-key');\n } else {\n for (const key of Object.keys(event.headers)) {\n const lower = key.toLowerCase();\n if (lower === 'x-goog-api-key' || lower === 'api-key') {\n delete event.headers[key];\n }\n }\n }\n\n if (event.request?.headers) {\n if (typeof event.request.headers.delete === 'function') {\n event.request.headers.delete('x-goog-api-key');\n event.request.headers.delete('api-key');\n } else {\n for (const key of Object.keys(event.request.headers)) {\n const lower = key.toLowerCase();\n if (lower === 'x-goog-api-key' || lower === 'api-key') {\n delete event.request.headers[key];\n }\n }\n }\n }\n\n // Strip query parameter key/x-goog-api-key/api-key if URL has them\n let cleanUrl = rawUrl;\n try {\n const parsedUrl = new URL(rawUrl);\n let urlChanged = false;\n for (const param of ['key', 'x-goog-api-key', 'api-key']) {\n if (parsedUrl.searchParams.has(param)) {\n parsedUrl.searchParams.delete(param);\n urlChanged = true;\n }\n }\n if (urlChanged) {\n cleanUrl = parsedUrl.toString();\n }\n } catch {\n // Ignore invalid URL parse\n }\n\n // If we have auth, perform full URL and body transformation via prepareAgyRequest\n if (authRecord && authRecord.access) {\n let projectContext: { effectiveProjectId: string; auth: any };\n try {\n projectContext = await ensureProjectContext(\n authRecord,\n { auth: { set: async () => {} } } as any,\n undefined,\n modelName\n );\n } catch {\n projectContext = {\n effectiveProjectId: parseRefreshParts(authRecord.refresh).projectId || 'antigravity',\n auth: authRecord\n };\n }\n\n const originalRequestedModel = modelName;\n let modifiedUrl = cleanUrl;\n if (originalRequestedModel) {\n const originalBase = originalRequestedModel.replace('google-agy/', '');\n const initHeaders = event.headers instanceof Headers ? event.headers : event.request?.headers;\n const resolvedBase = resolveModelTier(originalBase, { headers: initHeaders });\n if (originalBase !== resolvedBase) {\n modifiedUrl = modifiedUrl.replace(`models/${originalBase}`, `models/${resolvedBase}`);\n }\n }\n\n // Read body from event.body or event.request\n let bodyPayload: any = event.body;\n if (bodyPayload === undefined && event.request) {\n if (typeof event.request.clone === 'function') {\n try {\n bodyPayload = await event.request.clone().text();\n } catch {\n // Ignore clone read error\n }\n } else if (event.request.body !== undefined) {\n bodyPayload = event.request.body;\n }\n }\n\n // Check if caller supplied custom user-agent\n let preExistingUserAgent: string | undefined;\n if (typeof event.headers?.get === 'function') {\n preExistingUserAgent = event.headers.get('user-agent') || event.headers.get('User-Agent') || undefined;\n } else if (event.headers && typeof event.headers === 'object') {\n preExistingUserAgent = (event.headers['user-agent'] as string) || (event.headers['User-Agent'] as string) || undefined;\n }\n if (!preExistingUserAgent && typeof event.request?.headers?.get === 'function') {\n preExistingUserAgent = event.request.headers.get('user-agent') || event.request.headers.get('User-Agent') || undefined;\n } else if (!preExistingUserAgent && event.request?.headers && typeof event.request.headers === 'object') {\n preExistingUserAgent = (event.request.headers['user-agent'] as string) || (event.request.headers['User-Agent'] as string) || undefined;\n }\n\n const method = event.method || event.request?.method || 'POST';\n const requestHeaders = event.headers || event.request?.headers;\n const transformed = prepareAgyRequest(\n modifiedUrl,\n {\n method,\n headers: requestHeaders,\n body: bodyPayload\n },\n authRecord.access,\n projectContext.effectiveProjectId\n );\n\n // Update URL to Code Assist endpoint\n const targetUrl = toUrlString(transformed.request);\n if (typeof event.url === 'string') {\n event.url = targetUrl;\n }\n if (event.request && typeof event.request === 'object') {\n if (typeof Request !== 'undefined' && event.request instanceof Request) {\n event.request = new Request(targetUrl, {\n method: transformed.init.method,\n headers: transformed.init.headers,\n body: transformed.init.body\n });\n } else if (typeof event.request.url === 'string') {\n try {\n event.request.url = targetUrl;\n } catch {\n // Ignore setter error on read-only url property\n }\n }\n }\n\n // Update headers\n const headerEntries = transformed.init.headers instanceof Headers\n ? Array.from(transformed.init.headers.entries())\n : Object.entries(transformed.init.headers || {});\n\n for (const [k, v] of headerEntries) {\n const lowerK = k.toLowerCase();\n if (lowerK === 'user-agent') {\n if (preExistingUserAgent) {\n continue;\n }\n // If no pre-existing user-agent, set as 'User-Agent' on plain objects\n if (typeof event.headers?.set === 'function') {\n event.headers.set('User-Agent', v);\n } else if (event.headers) {\n event.headers['User-Agent'] = v;\n }\n if (event.request?.headers && typeof event.request.headers.set === 'function') {\n event.request.headers.set('User-Agent', v);\n } else if (event.request?.headers && typeof event.request.headers === 'object') {\n event.request.headers['User-Agent'] = v;\n }\n continue;\n }\n if (typeof event.headers?.set === 'function') {\n event.headers.set(k, v);\n } else if (event.headers) {\n event.headers[k] = v;\n }\n if (event.request?.headers && typeof event.request.headers.set === 'function') {\n event.request.headers.set(k, v);\n } else if (event.request?.headers && typeof event.request.headers === 'object') {\n event.request.headers[k] = v;\n }\n }\n\n // Update body\n if (transformed.init.body !== undefined) {\n event.body = transformed.init.body;\n if (event.request && typeof event.request === 'object' && !(event.request instanceof Request)) {\n event.request.body = transformed.init.body;\n }\n }\n } else {\n // Fallback if no auth: update cleaned URL\n if (typeof event.url === 'string') {\n event.url = cleanUrl;\n }\n if (event.request && typeof event.request === 'object') {\n if (typeof Request !== 'undefined' && event.request instanceof Request) {\n event.request = new Request(cleanUrl, event.request);\n } else if (typeof event.request.url === 'string') {\n try {\n event.request.url = cleanUrl;\n } catch {\n // Ignore setter error on read-only url property\n }\n }\n }\n }\n }\n\n if (authRecord && authRecord.access) {\n if (typeof event.headers?.set === 'function') {\n event.headers.set('Authorization', `Bearer ${authRecord.access}`);\n } else if (event.headers) {\n event.headers['Authorization'] = `Bearer ${authRecord.access}`;\n }\n if (event.request?.headers && typeof event.request.headers.set === 'function') {\n event.request.headers.set('Authorization', `Bearer ${authRecord.access}`);\n } else if (event.request?.headers && typeof event.request.headers === 'object') {\n event.request.headers['Authorization'] = `Bearer ${authRecord.access}`;\n }\n }\n\n const userAgent = buildAgyCliUserAgent(modelName);\n if (typeof event.headers?.set === 'function') {\n if (!event.headers.get('User-Agent') && !event.headers.get('user-agent')) {\n event.headers.set('User-Agent', userAgent);\n }\n } else if (event.headers) {\n if (!event.headers['User-Agent'] && !event.headers['user-agent']) {\n event.headers['User-Agent'] = userAgent;\n }\n }\n if (event.request?.headers && typeof event.request.headers.set === 'function') {\n if (!event.request.headers.get('User-Agent') && !event.request.headers.get('user-agent')) {\n event.request.headers.set('User-Agent', userAgent);\n }\n } else if (event.request?.headers && typeof event.request.headers === 'object') {\n if (!event.request.headers['User-Agent'] && !event.request.headers['user-agent']) {\n event.request.headers['User-Agent'] = userAgent;\n }\n }\n };\n}\n\nexport function createV2HttpResponseHook(state: V2SessionHooksState = {}) {\n return async (event: any) => {\n if (!event || !event.response) return;\n\n const rawUrl = typeof event.url === 'string' ? event.url : (event.request?.url || '');\n const isInternal = rawUrl.includes('cloudcode-pa.googleapis.com');\n\n if (isInternal && event.response instanceof Response) {\n const isStreaming = rawUrl.includes(':streamGenerateCode') ||\n event.response.headers.get('content-type')?.includes('text/event-stream');\n\n const parsedModel = parseGenerativeLanguageRequest(rawUrl)?.effectiveModel || state.lastInterceptedModel;\n const transformed = await transformAgyResponse(\n event.response,\n !!isStreaming,\n null,\n parsedModel,\n undefined,\n undefined\n );\n event.response = transformed;\n }\n };\n}\n\nexport function createV2RetryHook() {\n return async (event: any) => {\n if (!event) return;\n const response = event.response;\n if (response instanceof Response) {\n if (response.status === 429 || response.status === 503) {\n const quota = await classifyQuotaResponse(response);\n if (quota?.retryDelayMs) {\n event.retryDelayMs = quota.retryDelayMs;\n } else {\n event.retryDelayMs = await resolveRetryDelayMs(response, event.attempt || 1);\n }\n }\n } else if (event.details && Array.isArray(event.details)) {\n const retryInfo = event.details.find(\n (d: any) => d && d['@type'] === 'type.googleapis.com/google.rpc.RetryInfo'\n );\n if (retryInfo?.retryDelay) {\n const delayMs = retryInternals.parseRetryDelayValue(retryInfo.retryDelay);\n if (delayMs !== null) {\n event.retryDelayMs = delayMs;\n }\n }\n }\n };\n}\n\nexport function registerV2SessionHooks(ctx: any) {\n const state: V2SessionHooksState = {};\n ctx.session.hook('http.request', createV2HttpRequestHook(state), { providerID: AGY_PROVIDER_ID });\n ctx.session.hook('http.response', createV2HttpResponseHook(state), { providerID: AGY_PROVIDER_ID });\n ctx.session.hook('retry', createV2RetryHook(), { providerID: AGY_PROVIDER_ID });\n}\n","import { AGY_PROVIDER_ID } from '../constants';\nimport { STATIC_MODELS_SIMPLE, TIER_MAPPING } from './tier';\nimport { createAgyQuotaTool, AGY_QUOTA_TOOL_NAME, AGY_V2_QUOTA_COMMAND, AGY_V2_QUOTA_COMMAND_TEMPLATE } from './quota';\nimport {\n createAgyQuotaSummaryTool,\n AGY_QUOTA_SUMMARY_TOOL_NAME,\n AGY_V2_QUOTA_SUMMARY_COMMAND,\n AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE,\n} from './quota-summary';\nimport {\n defineOpenCodeV2Plugin,\n type OpenCodeV2PluginContext,\n type OpenCodeV2PluginDefinition,\n} from './types';\nimport { authorizeAgy, exchangeAgyWithVerifier } from '../sdk/oauth';\nimport { parseOAuthCallbackInput } from './oauth-authorize';\nimport { formatRefreshParts } from './auth';\nimport { ensureProjectContext } from './project';\nimport {\n loadStoredAuthFromJson,\n saveStoredAuthToJson,\n setStoredAuthOverrideForTesting,\n} from './v2-storage';\nimport { createV2FetchInterceptor } from './v2-fetch';\nimport { registerV2SessionHooks } from './v2-session';\n\n// Re-export common symbols for backward compatibility\nexport { getSafeHeader, setSafeHeaders, toUrlString } from './headers';\nexport { resolveModelTier } from './tier';\nexport {\n loadStoredAuthFromJson,\n saveStoredAuthToJson,\n setStoredAuthOverrideForTesting,\n} from './v2-storage';\nexport { createV2FetchInterceptor } from './v2-fetch';\nexport {\n AGY_V2_QUOTA_COMMAND,\n AGY_V2_QUOTA_SUMMARY_COMMAND,\n AGY_V2_QUOTA_COMMAND_TEMPLATE,\n AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE,\n};\n\n/**\n * Setup adapter for OpenCode v2 plugin architecture.\n */\nexport async function setupOpenCodeV2(ctx: OpenCodeV2PluginContext): Promise {\n // 1. Register provider and models\n const resolveVariants = (modelId: string) => {\n const mapping = TIER_MAPPING[modelId];\n if (!mapping) return undefined;\n const variants: Array<{ id: string }> = [];\n if (mapping.minimal !== undefined) variants.push({ id: 'minimal' });\n if (mapping.low !== undefined) variants.push({ id: 'low' });\n if (mapping.medium !== undefined) variants.push({ id: 'medium' });\n if (mapping.high !== undefined) variants.push({ id: 'high' });\n return variants.length > 0 ? variants : undefined;\n };\n\n const updateProviderRecord = (p: any) => {\n p.name = 'Antigravity CLI';\n p.activation = 'enabled';\n p.package = 'aisdk:@ai-sdk/google';\n p.description = 'Google Gemini Antigravity Code Assist OAuth provider';\n p.settings = { ...(p.settings || {}), apiKey: 'dummy' };\n p.integrationID = AGY_PROVIDER_ID;\n };\n\n const buildModelRecord = (modelId: string, simple: any, existingModel?: any) => {\n const isClaude = modelId.startsWith('claude-');\n const isGpt = modelId.startsWith('gpt-');\n const variants = resolveVariants(modelId);\n return {\n id: modelId,\n providerID: AGY_PROVIDER_ID,\n name: simple.name,\n description: simple.description,\n family: modelId.includes('gemini') ? 'gemini' : (isClaude ? 'claude' : (isGpt ? 'gpt' : 'unknown')),\n reasoning: simple.reasoning,\n attachment: simple.attachment,\n tool_call: simple.toolCall,\n limit: {\n context: simple.maxTokens,\n output: simple.maxOutputTokens,\n },\n cost: simple.cost || { input: 0, output: 0 },\n ...(variants ? { variants } : {}),\n ...(existingModel || {}),\n };\n };\n\n const updateModelRecord = (m: any, modelId: string, simple: any) => {\n m.name = simple.name;\n m.capabilities = {\n tools: true,\n input: ['text', 'image'],\n output: ['text'],\n };\n m.limit = {\n context: simple.maxTokens,\n output: simple.maxOutputTokens,\n };\n m.family = 'gemini';\n if (simple.cost) {\n m.cost = simple.cost;\n }\n const variants = resolveVariants(modelId);\n if (variants) {\n m.variants = variants;\n }\n };\n\n // OpenCode v2.0.8+ splits catalog into top-level provider and model services\n if (ctx.provider?.transform) {\n ctx.provider.transform(async (editor: any) => {\n if (!editor) return;\n if (typeof editor.update === 'function') {\n editor.update(AGY_PROVIDER_ID, updateProviderRecord);\n } else if (typeof editor.set === 'function') {\n const p: any = { id: AGY_PROVIDER_ID };\n updateProviderRecord(p);\n editor.set(AGY_PROVIDER_ID, p);\n } else if (typeof editor === 'object') {\n const p: any = editor[AGY_PROVIDER_ID] || { id: AGY_PROVIDER_ID };\n updateProviderRecord(p);\n editor[AGY_PROVIDER_ID] = p;\n }\n });\n }\n\n if (ctx.model?.transform) {\n ctx.model.transform(async (editor: any) => {\n if (!editor) return;\n if (typeof editor.update === 'function') {\n for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) {\n editor.update(AGY_PROVIDER_ID, modelId, (m: any) => updateModelRecord(m, modelId, simple));\n }\n } else if (typeof editor.set === 'function') {\n for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) {\n const m = buildModelRecord(modelId, simple);\n editor.set(`${AGY_PROVIDER_ID}:${modelId}`, m);\n }\n } else if (typeof editor === 'object') {\n for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) {\n const key = `${AGY_PROVIDER_ID}:${modelId}`;\n editor[key] = buildModelRecord(modelId, simple, editor[key]);\n }\n }\n });\n }\n\n // OpenCode v2 <= 2.0.7 legacy catalog transform fallback\n if (ctx.catalog?.transform) {\n ctx.catalog.transform(async (editor: any) => {\n if (!editor) return;\n\n if (typeof editor?.provider?.update === 'function') {\n editor.provider.update(AGY_PROVIDER_ID, updateProviderRecord);\n\n if (typeof editor?.model?.update === 'function') {\n for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) {\n editor.model.update(AGY_PROVIDER_ID, modelId, (m: any) => updateModelRecord(m, modelId, simple));\n }\n }\n return;\n }\n\n const catalog = editor;\n catalog.providers = catalog.providers || {};\n catalog.providers[AGY_PROVIDER_ID] = {\n id: AGY_PROVIDER_ID,\n name: 'Antigravity CLI',\n npm: 'aisdk:@ai-sdk/google',\n settings: { apiKey: 'dummy' },\n integrationID: AGY_PROVIDER_ID,\n models: { ...(catalog.providers[AGY_PROVIDER_ID]?.models || {}) },\n ...catalog.providers[AGY_PROVIDER_ID],\n };\n if (catalog.providers[AGY_PROVIDER_ID].settings) {\n catalog.providers[AGY_PROVIDER_ID].settings = {\n ...catalog.providers[AGY_PROVIDER_ID].settings,\n apiKey: 'dummy',\n };\n } else {\n catalog.providers[AGY_PROVIDER_ID].settings = { apiKey: 'dummy' };\n }\n catalog.providers[AGY_PROVIDER_ID].integrationID = AGY_PROVIDER_ID;\n\n const targetModels = catalog.providers[AGY_PROVIDER_ID].models;\n\n for (const [modelId, simple] of Object.entries(STATIC_MODELS_SIMPLE)) {\n targetModels[modelId] = buildModelRecord(modelId, simple, targetModels[modelId]);\n }\n });\n }\n\n // 2. Register agy_quota and agy_quota_summary tools via tool transform\n ctx.tool.transform(async (editor: any) => {\n if (!editor) return;\n\n const quotaTool = createAgyQuotaTool({\n client: undefined as any,\n getAuthResolver: () => undefined,\n getConfiguredProjectId: () => undefined,\n getUserAgentModel: () => undefined,\n });\n\n const quotaSummaryTool = createAgyQuotaSummaryTool({\n client: undefined as any,\n getAuthResolver: () => undefined,\n getConfiguredProjectId: () => undefined,\n getUserAgentModel: () => undefined,\n });\n\n if (typeof editor.add === 'function') {\n editor.add({\n name: AGY_QUOTA_TOOL_NAME,\n description: quotaTool.description,\n input: {},\n execute: async (args: any, context: any) => (quotaTool as any).execute(args, context),\n });\n editor.add({\n name: AGY_QUOTA_SUMMARY_TOOL_NAME,\n description: quotaSummaryTool.description,\n input: {},\n execute: async (args: any, context: any) => (quotaSummaryTool as any).execute(args, context),\n });\n } else if (typeof editor.set === 'function') {\n editor.set(AGY_QUOTA_TOOL_NAME, quotaTool);\n editor.set(AGY_QUOTA_SUMMARY_TOOL_NAME, quotaSummaryTool);\n } else if (editor.tools && typeof editor.tools === 'object') {\n editor.tools[AGY_QUOTA_TOOL_NAME] = quotaTool;\n editor.tools[AGY_QUOTA_SUMMARY_TOOL_NAME] = quotaSummaryTool;\n } else if (typeof editor === 'object') {\n editor[AGY_QUOTA_TOOL_NAME] = quotaTool;\n editor[AGY_QUOTA_SUMMARY_TOOL_NAME] = quotaSummaryTool;\n }\n });\n\n // 3. Register agy-quota and agy-quota-summary commands via command transform\n ctx.command.transform(async (editor: any) => {\n if (!editor) return;\n\n const commands = {\n [AGY_V2_QUOTA_COMMAND]: {\n description: 'Show Agy Code Assist quota usage',\n template: AGY_V2_QUOTA_COMMAND_TEMPLATE,\n },\n [AGY_V2_QUOTA_SUMMARY_COMMAND]: {\n description: 'Show Agy Code Assist quota summary with weekly and 5-hour limits',\n template: AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE,\n },\n };\n\n if (typeof editor.add === 'function') {\n editor.add({\n name: AGY_V2_QUOTA_COMMAND,\n description: 'Display current Antigravity quota usage',\n template: AGY_V2_QUOTA_COMMAND_TEMPLATE,\n });\n editor.add({\n name: AGY_V2_QUOTA_SUMMARY_COMMAND,\n description: 'Display Antigravity quota summary grouped by model family',\n template: AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE,\n });\n } else if (typeof editor.set === 'function') {\n editor.set(AGY_V2_QUOTA_COMMAND, commands[AGY_V2_QUOTA_COMMAND]);\n editor.set(AGY_V2_QUOTA_SUMMARY_COMMAND, commands[AGY_V2_QUOTA_SUMMARY_COMMAND]);\n } else if (editor.commands && typeof editor.commands === 'object') {\n editor.commands[AGY_V2_QUOTA_COMMAND] = commands[AGY_V2_QUOTA_COMMAND];\n editor.commands[AGY_V2_QUOTA_SUMMARY_COMMAND] = commands[AGY_V2_QUOTA_SUMMARY_COMMAND];\n } else if (typeof editor === 'object') {\n editor[AGY_V2_QUOTA_COMMAND] = commands[AGY_V2_QUOTA_COMMAND];\n editor[AGY_V2_QUOTA_SUMMARY_COMMAND] = commands[AGY_V2_QUOTA_SUMMARY_COMMAND];\n }\n });\n\n // 4. Register integration for AGY_PROVIDER_ID via integration transform\n ctx.integration?.transform?.((editor: any) => {\n if (!editor) return;\n\n editor.update?.(AGY_PROVIDER_ID, (integration: any) => {\n integration.name = 'Antigravity CLI (plugin)';\n });\n\n editor.method?.update?.({\n integrationID: AGY_PROVIDER_ID,\n method: {\n id: 'oauth',\n type: 'oauth',\n label: 'Antigravity CLI (OAuth)',\n },\n authorize: async () => {\n const authorization = await authorizeAgy();\n\n return {\n mode: 'code',\n url: authorization.url,\n instructions:\n 'Please complete Google account authorization in your browser. After authorization, copy the full redirect URL or code and paste it below:',\n callback: async (code: string) => {\n const trimmed = code.trim();\n const parsed = parseOAuthCallbackInput(trimmed);\n const authCode = parsed.code || trimmed;\n\n if (!authCode) {\n return { type: 'failed', error: 'Missing authorization code in callback input' };\n }\n\n if (parsed.state && parsed.state !== authorization.state) {\n return { type: 'failed', error: 'State mismatch in callback input (possible CSRF attempt)' };\n }\n\n const exchangeResult = await exchangeAgyWithVerifier(authCode, authorization.verifier);\n if (exchangeResult.type !== 'success') {\n return exchangeResult;\n }\n\n const initialRefresh = formatRefreshParts({\n refreshToken: exchangeResult.refresh,\n });\n\n let authRecord = {\n type: 'oauth' as const,\n refresh: initialRefresh,\n access: exchangeResult.access,\n expires: exchangeResult.expires,\n };\n\n try {\n const projectContext = await ensureProjectContext(\n authRecord,\n { auth: { set: async () => {} } } as any\n );\n authRecord = {\n ...authRecord,\n refresh: projectContext.auth.refresh,\n };\n } catch {\n // Ignore project resolution error during authorize\n }\n\n try {\n saveStoredAuthToJson(authRecord);\n } catch {\n return {\n type: 'failed',\n error: 'Google authentication succeeded but AGY credentials could not be stored. Check filesystem access and retry.',\n };\n }\n\n return {\n type: 'oauth',\n methodID: 'oauth',\n refresh: authRecord.refresh,\n access: authRecord.access,\n expires: authRecord.expires,\n };\n },\n };\n },\n });\n });\n\n // 5. Register aisdk hook if supported\n ctx.aisdk?.hook('sdk', async (event: any) => {\n if (!event) return;\n const isMatchingProvider =\n event.providerID === AGY_PROVIDER_ID ||\n event.model?.providerID === AGY_PROVIDER_ID;\n\n if (!isMatchingProvider) return;\n\n event.options = event.options || {};\n event.options.apiKey = 'dummy';\n event.options.fetch = createV2FetchInterceptor();\n });\n\n // 6. Register session hooks (http.request, http.response, retry)\n registerV2SessionHooks(ctx);\n}\n\nexport const v2PluginDefinition: OpenCodeV2PluginDefinition = defineOpenCodeV2Plugin({\n id: AGY_PROVIDER_ID,\n setup: setupOpenCodeV2,\n});\n\nexport const createV2PluginDefinition = (): OpenCodeV2PluginDefinition => v2PluginDefinition;\n","import { AgyCLIOAuthPlugin, GoogleOAuthPlugin } from \"./src/plugin\";\nimport { setupOpenCodeV2, v2PluginDefinition } from \"./src/plugin/v2\";\nimport type { DualOpenCodePlugin } from \"./src/plugin/types\";\n\nexport { AgyCLIOAuthPlugin, GoogleOAuthPlugin };\nexport { setupOpenCodeV2, v2PluginDefinition };\nexport * from \"./src/plugin/types\";\n\nconst dualPlugin: DualOpenCodePlugin = {\n ...v2PluginDefinition,\n server: AgyCLIOAuthPlugin,\n};\n\nexport default dualPlugin;\n"],"mappings":"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","names":["cached","Buffer","Buffer","Buffer","decode","encode","encode","decode","encoder","hash","url","error","userInfo","version","platform","arch","url","seconds","error","normalize","error","existsSync","readFileSync","writeFileSync","join","tmpdir","platform","url","error","delayMs","url","error","wait","url","buildCodeAssistHeaders","error","wait","error","cached","promise","normalizeProjectId","config","error","message","url","url","platform","existsSync","mkdirSync","readFileSync","writeFileSync","renameSync","unlinkSync","join","dirname","homedir","tmpdir","getConfigDir","platform","config","cached","config","existsSync","mkdirSync","readFileSync","writeFileSync","renameSync","unlinkSync","join","dirname","homedir","tmpdir","createHash","hash","decoder","encoder","WRITE_THROTTLE_MS","getConfigDir","platform","join","homedir","existsSync","readFileSync","record","dirname","mkdirSync","tmpdir","writeFileSync","renameSync","unlinkSync","_default","base64","base64url","bigint","boolean","_catch","cidrv4","cidrv6","core_exports","cuid","cuid2","date","decode","decodeAsync","e164","email","emoji","encode","encodeAsync","_enum","guid","hex","hostname","ipv4","ipv6","ksuid","nanoid","_null","nullish","number","parse","parseAsync","safeDecode","safeDecodeAsync","safeEncode","safeEncodeAsync","safeParse","safeParseAsync","string","ulid","_undefined","uuid","_void","xid","core_exports","_emoji","_null","_undefined","decode","encode","initializer","isObject","array","set","object","Class","config","base64","base64url","hex","error","issue","encode","decode","version","time","inst","result","checks","checkResult","canary","result","_","url","inst","base64","r","isObject","allowsEval","results","map","left","right","keyResult","valueResult","output","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","issue","parsedType","error","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","parsedType","error","issue","error","parsedType","issue","error","parsedType","issue","parsedType","error","issue","error","parsedType","issue","error","parsedType","issue","parsedType","number","error","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","parsedType","error","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","error","parsedType","issue","Class","_emoji","_undefined","_null","Class","issue","codec","json","file","id","schema","params","gen","date","datetime","duration","time","datetime","date","time","duration","initializer","issue","issues","parse","parseAsync","safeParse","safeParseAsync","encode","decode","encodeAsync","decodeAsync","safeEncode","safeDecode","safeEncodeAsync","safeDecodeAsync","def","parse","safeParse","parseAsync","safeParseAsync","encode","decode","encodeAsync","decodeAsync","safeEncode","safeDecode","safeEncodeAsync","safeDecodeAsync","check","_default","_catch","_emoji","datetime","date","time","duration","string","email","guid","uuid","emoji","nanoid","cuid","cuid2","ulid","xid","ksuid","ipv4","ipv6","cidrv4","cidrv6","base64","base64url","e164","hostname","hex","number","boolean","bigint","_undefined","_null","_void","date","_enum","issue","output","nullish","_default","_catch","string","number","boolean","_null","map","ZodFirstPartyTypeKind","bigint","boolean","date","number","string","string","number","boolean","bigint","date","error","error","error","os","url","error","arch","platform","randomUUID","CLOUDCODE_DOMAINS","config","record","error","CLOUDCODE_DOMAINS","parseRetryDelayValue","seconds","randomUUID","url","readString","randomUUID","tool","randomUUID","wrappedBody","userPromptId","sessionId","requestId","toolMapper","contents","error","isRecord","config","tool","error","existsSync","mkdirSync","join","error","url","decoder","nextHeaders","config","configuredProjectId","error","existsSync","mkdirSync","readFileSync","renameSync","unlinkSync","writeFileSync","randomUUID","homedir","join","join","homedir","existsSync","readFileSync","mkdirSync","randomUUID","writeFileSync","renameSync","error","unlinkSync"]} \ No newline at end of file diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/constants.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/constants.d.ts index 4f7755d6..5c1f9195 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/src/constants.d.ts +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/constants.d.ts @@ -1,4 +1,6 @@ export declare const AGY_PROVIDER_ID = "google-agy"; +export declare const AGY_AUTH_URL = "https://accounts.google.com/o/oauth2/auth"; +export declare const AGY_TOKEN_URL = "https://oauth2.googleapis.com/token"; export declare const AGY_CLIENT_ID = "REDACTED_GOOGLE_OAUTH_CLIENT_ID.apps.googleusercontent.com"; export declare const AGY_CLIENT_SECRET = "REDACTED_GOOGLE_OAUTH_CLIENT_SECRET"; export declare const AGY_SCOPES: readonly string[]; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin.d.ts index 5f1668d9..39342ed6 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin.d.ts +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin.d.ts @@ -1,4 +1,6 @@ import type { PluginContext, PluginResult } from './plugin/types'; +import { STATIC_MODELS_SIMPLE, TIER_MAPPING, resolveModelTier, type SimpleStaticModel } from './plugin/tier'; +export { STATIC_MODELS_SIMPLE, TIER_MAPPING, resolveModelTier, type SimpleStaticModel }; /** * Registers the Agy OAuth provider for Opencode. */ diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/headers.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/headers.d.ts new file mode 100644 index 00000000..2ca805c3 --- /dev/null +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/headers.d.ts @@ -0,0 +1,7 @@ +/** + * HTTP header and URL manipulation utilities. + * Shared between OpenCode v1 and v2 runtimes. + */ +export declare function getSafeHeader(headers: unknown, key: string): string | undefined; +export declare function setSafeHeaders(initHeaders: unknown, newHeaders: Record): unknown; +export declare function toUrlString(value: RequestInfo): string; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/oauth-authorize.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/oauth-authorize.d.ts index e69f5014..de8da592 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/oauth-authorize.d.ts +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/oauth-authorize.d.ts @@ -7,9 +7,18 @@ export declare function createOAuthAuthorizeMethod(options?: { client?: PluginClient; getConfiguredProjectId?: () => Promise | string | undefined; getUserAgentModel?: () => Promise | string | undefined; + openBrowser?: (url: string) => void; }): () => Promise<{ url: string; instructions: string; method: 'code'; callback: (callbackUrl: string) => Promise; }>; +export declare function parseOAuthCallbackInput(input: string): { + code?: string; + state?: string; +}; +export declare function setOpenBrowserLauncherForTesting(launcher: typeof openBrowserLauncher | null): void; +export declare function defaultOpenBrowserLauncher(command: string, args: string[]): void; +declare let openBrowserLauncher: typeof defaultOpenBrowserLauncher; +export {}; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/quota-summary.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/quota-summary.d.ts index 598bba9e..d4700912 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/quota-summary.d.ts +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/quota-summary.d.ts @@ -1,5 +1,7 @@ import type { GetAuth, PluginClient } from "./types"; export declare const AGY_QUOTA_SUMMARY_TOOL_NAME = "agy_quota_summary"; +export declare const AGY_V2_QUOTA_SUMMARY_COMMAND = "agy-quota-summary"; +export declare const AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE = "Retrieve Agy Code Assist quota summary (weekly and 5-hour limits by model group) for the current authenticated account.\n\nImmediately call `agy_quota_summary` with no arguments and return its output verbatim.\nDo not call other tools.\n"; interface AgyQuotaSummaryToolDependencies { client: PluginClient; getAuthResolver: () => GetAuth | undefined; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/quota.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/quota.d.ts index e6f1d0bf..32b1756d 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/quota.d.ts +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/quota.d.ts @@ -1,5 +1,7 @@ import type { GetAuth, PluginClient } from "./types"; export declare const AGY_QUOTA_TOOL_NAME = "agy_quota"; +export declare const AGY_V2_QUOTA_COMMAND = "agy-quota"; +export declare const AGY_V2_QUOTA_COMMAND_TEMPLATE = "Retrieve Agy Code Assist quota usage for the current authenticated account.\n\nImmediately call `agy_quota` with no arguments and return its output verbatim.\nDo not call other tools.\n"; interface AgyQuotaToolDependencies { client: PluginClient; getAuthResolver: () => GetAuth | undefined; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/tier.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/tier.d.ts new file mode 100644 index 00000000..2d68502b --- /dev/null +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/tier.d.ts @@ -0,0 +1,28 @@ +export interface SimpleStaticModel { + name: string; + description: string; + maxTokens: number; + maxOutputTokens: number; + toolCall: boolean; + reasoning: boolean; + attachment: boolean; + cost?: { + input: number; + output: number; + cache?: { + read: number; + write: number; + }; + }; +} +export declare const STATIC_MODELS_SIMPLE: Record; +export declare const TIER_MAPPING: Record>; +/** + * Resolves a model ID and optional request headers or suffix into the appropriate tier variant. + */ +export declare function resolveModelTier(baseModelId: string, headersOrInit?: RequestInit | unknown): string; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/types.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/types.d.ts index b43a7559..ad92b088 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/types.d.ts +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/types.d.ts @@ -29,3 +29,38 @@ export interface ProjectContextResult { effectiveProjectId: string; } export type { Provider as ProviderV2, Model as ModelV2 } from '@opencode-ai/sdk/v2'; +export interface OpenCodeV2TransformRegistry { + transform: (fn: (target: T) => Promise | void) => void; +} +export interface OpenCodeV2SessionRegistry { + hook: (name: string, handler: (...args: any[]) => any, options?: { + providerID?: string; + }) => void; +} +export interface OpenCodeV2PluginLocation { + root?: string; + workspace?: string; +} +export interface OpenCodeV2PluginContext { + catalog?: OpenCodeV2TransformRegistry; + provider?: OpenCodeV2TransformRegistry; + model?: OpenCodeV2TransformRegistry; + tool: OpenCodeV2TransformRegistry; + command: OpenCodeV2TransformRegistry; + session: OpenCodeV2SessionRegistry; + aisdk?: { + hook: (name: 'sdk' | 'language', handler: (event: any) => Promise | void) => void; + }; + integration?: OpenCodeV2TransformRegistry | { + transform?: (transformer: (editor: any) => void) => void; + }; + location: OpenCodeV2PluginLocation; +} +export interface OpenCodeV2PluginDefinition { + id: string; + setup: (ctx: OpenCodeV2PluginContext) => Promise | void; +} +export interface DualOpenCodePlugin extends OpenCodeV2PluginDefinition { + server: (options: any) => Promise; +} +export declare function defineOpenCodeV2Plugin(def: OpenCodeV2PluginDefinition): OpenCodeV2PluginDefinition; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-fetch.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-fetch.d.ts new file mode 100644 index 00000000..09152767 --- /dev/null +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-fetch.d.ts @@ -0,0 +1 @@ +export declare function createV2FetchInterceptor(getAuthSnapshot?: () => Promise | any): (input: RequestInfo, init?: RequestInit) => Promise; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-session.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-session.d.ts new file mode 100644 index 00000000..14c80c09 --- /dev/null +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-session.d.ts @@ -0,0 +1,7 @@ +export interface V2SessionHooksState { + lastInterceptedModel?: string; +} +export declare function createV2HttpRequestHook(state?: V2SessionHooksState): (event: any) => Promise; +export declare function createV2HttpResponseHook(state?: V2SessionHooksState): (event: any) => Promise; +export declare function createV2RetryHook(): (event: any) => Promise; +export declare function registerV2SessionHooks(ctx: any): void; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-storage.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-storage.d.ts new file mode 100644 index 00000000..4368f27d --- /dev/null +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2-storage.d.ts @@ -0,0 +1,3 @@ +export declare function setStoredAuthOverrideForTesting(override: any): void; +export declare function loadStoredAuthFromJson(): any; +export declare function saveStoredAuthToJson(authRecord: any): void; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2.d.ts new file mode 100644 index 00000000..eb797158 --- /dev/null +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/plugin/v2.d.ts @@ -0,0 +1,14 @@ +import { AGY_V2_QUOTA_COMMAND, AGY_V2_QUOTA_COMMAND_TEMPLATE } from './quota'; +import { AGY_V2_QUOTA_SUMMARY_COMMAND, AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE } from './quota-summary'; +import { type OpenCodeV2PluginContext, type OpenCodeV2PluginDefinition } from './types'; +export { getSafeHeader, setSafeHeaders, toUrlString } from './headers'; +export { resolveModelTier } from './tier'; +export { loadStoredAuthFromJson, saveStoredAuthToJson, setStoredAuthOverrideForTesting, } from './v2-storage'; +export { createV2FetchInterceptor } from './v2-fetch'; +export { AGY_V2_QUOTA_COMMAND, AGY_V2_QUOTA_SUMMARY_COMMAND, AGY_V2_QUOTA_COMMAND_TEMPLATE, AGY_V2_QUOTA_SUMMARY_COMMAND_TEMPLATE, }; +/** + * Setup adapter for OpenCode v2 plugin architecture. + */ +export declare function setupOpenCodeV2(ctx: OpenCodeV2PluginContext): Promise; +export declare const v2PluginDefinition: OpenCodeV2PluginDefinition; +export declare const createV2PluginDefinition: () => OpenCodeV2PluginDefinition; diff --git a/references/upstream-plugins/opencode-agy-auth/dist/src/sdk/agy-cli-version.d.ts b/references/upstream-plugins/opencode-agy-auth/dist/src/sdk/agy-cli-version.d.ts index a5b48a01..3dc8d5d1 100644 --- a/references/upstream-plugins/opencode-agy-auth/dist/src/sdk/agy-cli-version.d.ts +++ b/references/upstream-plugins/opencode-agy-auth/dist/src/sdk/agy-cli-version.d.ts @@ -1 +1 @@ -export declare const AGY_CLI_VERSION = "1.2.10"; +export declare const AGY_CLI_VERSION = "1.2.11"; diff --git a/references/upstream-plugins/opencode-agy-auth/package.json b/references/upstream-plugins/opencode-agy-auth/package.json index cd8ac294..706c55f4 100644 --- a/references/upstream-plugins/opencode-agy-auth/package.json +++ b/references/upstream-plugins/opencode-agy-auth/package.json @@ -1,6 +1,6 @@ { "name": "@anthonyhaussman/opencode-agy-auth", - "version": "1.2.10", + "version": "1.2.11-alpha.0", "author": "antigravity", "repository": { "type": "git", @@ -42,7 +42,7 @@ "vitest": "^5.0.1" }, "dependencies": { - "@ai-sdk/google": "^4.0.79", + "@ai-sdk/google": "^4.0.80", "@openauthjs/openauth": "^0.4.3", "@opencode-ai/plugin": "^1.18.32" } diff --git a/references/upstream-plugins/opencode-cursor-oauth/README.md b/references/upstream-plugins/opencode-cursor-oauth/README.md deleted file mode 100644 index 7ea9e3ca..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# @playwo/opencode-cursor-oauth - -OpenCode plugin that connects to Cursor's API, giving you access to Cursor -models inside OpenCode with full tool-calling support. - -## Install in OpenCode - -Add this to `~/.config/opencode/opencode.json`: - -```jsonc -{ - "$schema": "https://opencode.ai/config.json", - "plugin": [ - "@playwo/opencode-cursor-oauth" - ], - "provider": { - "cursor": { - "name": "Cursor" - } - } -} -``` - -The `cursor` provider stub is required because OpenCode drops providers that do -not already exist in its bundled provider catalog. - -OpenCode installs npm plugins automatically at startup, so users do not need to -clone this repository. - -## Authenticate - -```sh -opencode auth login --provider cursor -``` - -This opens Cursor OAuth in the browser. Tokens are stored in -`~/.local/share/opencode/auth.json` and refreshed automatically. - -## Use - -Start OpenCode and select any Cursor model. The plugin starts a local -OpenAI-compatible proxy on demand and routes requests through Cursor's gRPC API. - -## How it works - -1. OAuth — browser-based login to Cursor via PKCE. -2. Model discovery — queries Cursor's gRPC API for all available models. -3. Local proxy — translates `POST /v1/chat/completions` into Cursor's - protobuf/HTTP/2 Connect protocol. -4. Native tool routing — rejects Cursor's built-in filesystem/shell tools and - exposes OpenCode's tool surface via Cursor MCP instead. - -HTTP/2 transport runs through a Node child process (`h2-bridge.mjs`) because -Bun's `node:http2` support is not reliable against Cursor's API. - -## Architecture - -``` -OpenCode --> /v1/chat/completions --> Bun.serve (proxy) - | - Node child process (h2-bridge.mjs) - | - HTTP/2 Connect stream - | - api2.cursor.sh gRPC - /agent.v1.AgentService/Run -``` - -### Tool call flow - -``` -1. Cursor model receives OpenAI tools via RequestContext (as MCP tool defs) -2. Model tries native tools (readArgs, shellArgs, etc.) -3. Proxy rejects each with typed error (ReadRejected, ShellRejected, etc.) -4. Model falls back to MCP tool -> mcpArgs exec message -5. Proxy emits OpenAI tool_calls SSE chunk, pauses H2 stream -6. OpenCode executes tool, sends result in follow-up request -7. Proxy resumes H2 stream with mcpResult, streams continuation -``` - -## Develop locally - -```sh -bun install -bun run build -bun test/smoke.ts -``` - -## Requirements - -- [OpenCode](https://opencode.ai) -- [Bun](https://bun.sh) -- [Node.js](https://nodejs.org) >= 18 for the HTTP/2 bridge process -- Active [Cursor](https://cursor.com) subscription \ No newline at end of file diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/auth.d.ts b/references/upstream-plugins/opencode-cursor-oauth/dist/auth.d.ts deleted file mode 100644 index c0dac343..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/auth.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface CursorAuthParams { - verifier: string; - challenge: string; - uuid: string; - loginUrl: string; -} -export interface CursorCredentials { - access: string; - refresh: string; - expires: number; -} -export declare function generateCursorAuthParams(): Promise; -export declare function pollCursorAuth(uuid: string, verifier: string): Promise<{ - accessToken: string; - refreshToken: string; -}>; -export declare function refreshCursorToken(refreshToken: string): Promise; -/** - * Extract JWT expiry with 5-minute safety margin. - * Falls back to 1 hour from now if token can't be parsed. - */ -export declare function getTokenExpiry(token: string): number; diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/auth.js b/references/upstream-plugins/opencode-cursor-oauth/dist/auth.js deleted file mode 100644 index 10ff6322..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/auth.js +++ /dev/null @@ -1,92 +0,0 @@ -import { generatePKCE } from "./pkce"; -const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl"; -const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll"; -const CURSOR_REFRESH_URL = process.env.CURSOR_REFRESH_URL ?? - "https://api2.cursor.sh/auth/exchange_user_api_key"; -const POLL_MAX_ATTEMPTS = 150; -const POLL_BASE_DELAY = 1000; -const POLL_MAX_DELAY = 10_000; -const POLL_BACKOFF_MULTIPLIER = 1.2; -export async function generateCursorAuthParams() { - const { verifier, challenge } = await generatePKCE(); - const uuid = crypto.randomUUID(); - const params = new URLSearchParams({ - challenge, - uuid, - mode: "login", - redirectTarget: "cli", - }); - const loginUrl = `${CURSOR_LOGIN_URL}?${params.toString()}`; - return { verifier, challenge, uuid, loginUrl }; -} -export async function pollCursorAuth(uuid, verifier) { - let delay = POLL_BASE_DELAY; - let consecutiveErrors = 0; - for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) { - await Bun.sleep(delay); - try { - const response = await fetch(`${CURSOR_POLL_URL}?uuid=${uuid}&verifier=${verifier}`); - if (response.status === 404) { - consecutiveErrors = 0; - delay = Math.min(delay * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY); - continue; - } - if (response.ok) { - const data = (await response.json()); - return { - accessToken: data.accessToken, - refreshToken: data.refreshToken, - }; - } - throw new Error(`Poll failed: ${response.status}`); - } - catch { - consecutiveErrors++; - if (consecutiveErrors >= 3) { - throw new Error("Too many consecutive errors during Cursor auth polling"); - } - } - } - throw new Error("Cursor authentication polling timeout"); -} -export async function refreshCursorToken(refreshToken) { - const response = await fetch(CURSOR_REFRESH_URL, { - method: "POST", - headers: { - Authorization: `Bearer ${refreshToken}`, - "Content-Type": "application/json", - }, - body: "{}", - }); - if (!response.ok) { - const error = await response.text(); - throw new Error(`Cursor token refresh failed: ${error}`); - } - const data = (await response.json()); - return { - access: data.accessToken, - refresh: data.refreshToken || refreshToken, - expires: getTokenExpiry(data.accessToken), - }; -} -/** - * Extract JWT expiry with 5-minute safety margin. - * Falls back to 1 hour from now if token can't be parsed. - */ -export function getTokenExpiry(token) { - try { - const parts = token.split("."); - if (parts.length !== 3 || !parts[1]) { - return Date.now() + 3600 * 1000; - } - const decoded = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"))); - if (decoded && - typeof decoded === "object" && - typeof decoded.exp === "number") { - return decoded.exp * 1000 - 5 * 60 * 1000; - } - } - catch { - } - return Date.now() + 3600 * 1000; -} diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/h2-bridge.mjs b/references/upstream-plugins/opencode-cursor-oauth/dist/h2-bridge.mjs deleted file mode 100644 index e86132e4..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/h2-bridge.mjs +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env node -/** - * Dumb HTTP/2 bidirectional pipe for Cursor gRPC. - * - * Bun's node:http2 is broken. This Node script acts as a transparent - * HTTP/2 proxy: it opens a single bidirectional stream and ferries - * raw bytes between the parent process (via stdin/stdout) and Cursor. - * - * Protocol (length-prefixed framing over stdin/stdout): - * [4 bytes big-endian length][payload] - * - * First message on stdin is JSON config: - * { "accessToken": "...", "url": "...", "path": "...", "unary": false } - * - * When unary=true, the bridge uses application/proto (raw protobuf) instead - * of application/connect+proto (Connect streaming). The single stdin message - * is written as the request body and the stream is ended immediately. - * After config, subsequent stdin messages are raw bytes to write to the H2 stream. - * H2 response data is written to stdout using the same length-prefixed framing. - */ -import http2 from "node:http2"; -import crypto from "node:crypto"; - -const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f"; - -/** Write one length-prefixed message to stdout. */ -function writeMessage(data) { - const lenBuf = Buffer.alloc(4); - lenBuf.writeUInt32BE(data.length, 0); - process.stdout.write(lenBuf); - process.stdout.write(data); -} - -// --- Buffered stdin reader --- - -let stdinBuf = Buffer.alloc(0); -let stdinResolve = null; -let stdinEnded = false; - -process.stdin.on("data", (chunk) => { - stdinBuf = Buffer.concat([stdinBuf, chunk]); - if (stdinResolve) { - const r = stdinResolve; - stdinResolve = null; - r(); - } -}); - -process.stdin.on("end", () => { - stdinEnded = true; - if (stdinResolve) { - const r = stdinResolve; - stdinResolve = null; - r(); - } -}); - -function waitForData() { - return new Promise((resolve) => { stdinResolve = resolve; }); -} - -async function readExact(n) { - while (stdinBuf.length < n) { - if (stdinEnded) return null; - await waitForData(); - } - const result = stdinBuf.subarray(0, n); - stdinBuf = stdinBuf.subarray(n); - return Buffer.from(result); -} - -async function readMessage() { - const lenBuf = await readExact(4); - if (!lenBuf) return null; - const len = lenBuf.readUInt32BE(0); - if (len === 0) return Buffer.alloc(0); - return readExact(len); -} - -// --- Main --- - -const configBuf = await readMessage(); -if (!configBuf) process.exit(1); - -const config = JSON.parse(configBuf.toString("utf8")); -const { accessToken, url, path: rpcPath, unary } = config; - -const client = http2.connect(url || "https://api2.cursor.sh"); - -// Guard against initial connection failure. Reset on any h2 activity -// so long-running agent conversations (with tool call round-trips) survive. -let timeout = setTimeout(killBridge, 30_000); - -function resetTimeout() { - clearTimeout(timeout); - timeout = setTimeout(killBridge, 120_000); -} - -function killBridge() { - clearTimeout(timeout); - client.destroy(); - process.exit(1); -} - -client.on("error", () => { - clearTimeout(timeout); - process.exit(1); -}); - -const headers = { - ":method": "POST", - ":path": rpcPath || "/agent.v1.AgentService/Run", - "content-type": unary ? "application/proto" : "application/connect+proto", - te: "trailers", - authorization: `Bearer ${accessToken}`, - "x-ghost-mode": "true", - "x-cursor-client-version": CURSOR_CLIENT_VERSION, - "x-cursor-client-type": "cli", - "x-request-id": crypto.randomUUID(), -}; -if (!unary) { - headers["connect-protocol-version"] = "1"; -} -const h2Stream = client.request(headers); - -// Forward H2 response data → stdout (length-prefixed) -h2Stream.on("data", (chunk) => { - resetTimeout(); - writeMessage(chunk); -}); - -h2Stream.on("end", () => { - clearTimeout(timeout); - client.close(); - // Give stdout time to flush - setTimeout(() => process.exit(0), 100); -}); - -h2Stream.on("error", () => { - clearTimeout(timeout); - client.close(); - process.exit(1); -}); - -// Forward stdin → H2 stream (after config message) -if (unary) { - // Unary mode: read a single body message, write it, and end the stream. - const body = await readMessage(); - if (body && body.length > 0 && !h2Stream.closed && !h2Stream.destroyed) { - h2Stream.end(body); - } else { - h2Stream.end(); - } -} else { - // Streaming mode: forward all stdin messages as Connect frames. - (async () => { - while (true) { - const msg = await readMessage(); - if (!msg || msg.length === 0) { - // EOF or zero-length = done writing - break; - } - if (!h2Stream.closed && !h2Stream.destroyed) { - resetTimeout(); - h2Stream.write(msg); - } - } - - if (!h2Stream.closed && !h2Stream.destroyed) { - h2Stream.end(); - } - })(); -} diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/index.d.ts b/references/upstream-plugins/opencode-cursor-oauth/dist/index.d.ts deleted file mode 100644 index cf1381c7..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * OpenCode Cursor Auth Plugin - * - * Enables using Cursor models (Claude, GPT, etc.) inside OpenCode via: - * 1. Browser-based OAuth login to Cursor - * 2. Local proxy translating OpenAI format → Cursor gRPC protocol - */ -import type { Plugin } from "@opencode-ai/plugin"; -/** - * OpenCode plugin that provides Cursor authentication and model access. - * Register in opencode.json: { "plugin": ["@playwo/opencode-cursor-oauth"] } - */ -export declare const CursorAuthPlugin: Plugin; -export default CursorAuthPlugin; diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/index.js b/references/upstream-plugins/opencode-cursor-oauth/dist/index.js deleted file mode 100644 index f3470fef..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/index.js +++ /dev/null @@ -1,229 +0,0 @@ -import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth"; -import { getCursorModels } from "./models"; -import { startProxy } from "./proxy"; -const CURSOR_PROVIDER_ID = "cursor"; -/** - * OpenCode plugin that provides Cursor authentication and model access. - * Register in opencode.json: { "plugin": ["@playwo/opencode-cursor-oauth"] } - */ -export const CursorAuthPlugin = async (input) => { - return { - auth: { - provider: CURSOR_PROVIDER_ID, - async loader(getAuth, provider) { - const auth = await getAuth(); - if (!auth || auth.type !== "oauth") - return {}; - // Ensure we have a valid access token, refreshing if expired - let accessToken = auth.access; - if (!accessToken || auth.expires < Date.now()) { - const refreshed = await refreshCursorToken(auth.refresh); - await input.client.auth.set({ - path: { id: CURSOR_PROVIDER_ID }, - body: { - type: "oauth", - refresh: refreshed.refresh, - access: refreshed.access, - expires: refreshed.expires, - }, - }); - accessToken = refreshed.access; - } - const models = await getCursorModels(accessToken); - const port = await startProxy(async () => { - const currentAuth = await getAuth(); - if (currentAuth.type !== "oauth") { - throw new Error("Cursor auth not configured"); - } - if (!currentAuth.access || currentAuth.expires < Date.now()) { - const refreshed = await refreshCursorToken(currentAuth.refresh); - await input.client.auth.set({ - path: { id: CURSOR_PROVIDER_ID }, - body: { - type: "oauth", - refresh: refreshed.refresh, - access: refreshed.access, - expires: refreshed.expires, - }, - }); - return refreshed.access; - } - return currentAuth.access; - }, models); - if (provider) { - provider.models = buildCursorProviderModels(models, port); - } - return { - baseURL: `http://localhost:${port}/v1`, - apiKey: "cursor-proxy", - async fetch(requestInput, init) { - if (init?.headers) { - if (init.headers instanceof Headers) { - init.headers.delete("authorization"); - } - else if (Array.isArray(init.headers)) { - init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization"); - } - else { - delete init.headers["authorization"]; - delete init.headers["Authorization"]; - } - } - return fetch(requestInput, init); - }, - }; - }, - methods: [ - { - type: "oauth", - label: "Login with Cursor", - async authorize() { - const { verifier, uuid, loginUrl } = await generateCursorAuthParams(); - return { - url: loginUrl, - instructions: "Complete login in your browser. This window will close automatically.", - method: "auto", - async callback() { - const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier); - return { - type: "success", - refresh: refreshToken, - access: accessToken, - expires: getTokenExpiry(accessToken), - }; - }, - }; - }, - }, - ], - }, - }; -}; -function buildCursorProviderModels(models, port) { - return Object.fromEntries(models.map((model) => [ - model.id, - { - id: model.id, - providerID: CURSOR_PROVIDER_ID, - api: { - id: model.id, - url: `http://localhost:${port}/v1`, - npm: "@ai-sdk/openai-compatible", - }, - name: model.name, - capabilities: { - temperature: true, - reasoning: model.reasoning, - attachment: false, - toolcall: true, - input: { - text: true, - audio: false, - image: false, - video: false, - pdf: false, - }, - output: { - text: true, - audio: false, - image: false, - video: false, - pdf: false, - }, - interleaved: false, - }, - cost: estimateModelCost(model.id), - limit: { - context: model.contextWindow, - output: model.maxTokens, - }, - status: "active", - options: {}, - headers: {}, - release_date: "", - variants: {}, - }, - ])); -} -// $/M token rates from cursor.com/docs/models-and-pricing -const MODEL_COST_TABLE = { - // Anthropic - "claude-4-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } }, - "claude-4-sonnet-1m": { input: 6, output: 22.5, cache: { read: 0.6, write: 7.5 } }, - "claude-4.5-haiku": { input: 1, output: 5, cache: { read: 0.1, write: 1.25 } }, - "claude-4.5-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } }, - "claude-4.5-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } }, - "claude-4.6-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } }, - "claude-4.6-opus-fast": { input: 30, output: 150, cache: { read: 3, write: 37.5 } }, - "claude-4.6-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } }, - // Cursor - "composer-1": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } }, - "composer-1.5": { input: 3.5, output: 17.5, cache: { read: 0.35, write: 0 } }, - "composer-2": { input: 0.5, output: 2.5, cache: { read: 0.2, write: 0 } }, - "composer-2-fast": { input: 1.5, output: 7.5, cache: { read: 0.2, write: 0 } }, - // Google - "gemini-2.5-flash": { input: 0.3, output: 2.5, cache: { read: 0.03, write: 0 } }, - "gemini-3-flash": { input: 0.5, output: 3, cache: { read: 0.05, write: 0 } }, - "gemini-3-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } }, - "gemini-3-pro-image": { input: 2, output: 12, cache: { read: 0.2, write: 0 } }, - "gemini-3.1-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } }, - // OpenAI - "gpt-5": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } }, - "gpt-5-fast": { input: 2.5, output: 20, cache: { read: 0.25, write: 0 } }, - "gpt-5-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } }, - "gpt-5-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } }, - "gpt-5.1-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } }, - "gpt-5.1-codex-max": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } }, - "gpt-5.1-codex-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } }, - "gpt-5.2": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } }, - "gpt-5.2-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } }, - "gpt-5.3-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } }, - "gpt-5.4": { input: 2.5, output: 15, cache: { read: 0.25, write: 0 } }, - "gpt-5.4-mini": { input: 0.75, output: 4.5, cache: { read: 0.075, write: 0 } }, - "gpt-5.4-nano": { input: 0.2, output: 1.25, cache: { read: 0.02, write: 0 } }, - // xAI - "grok-4.20": { input: 2, output: 6, cache: { read: 0.2, write: 0 } }, - // Moonshot - "kimi-k2.5": { input: 0.6, output: 3, cache: { read: 0.1, write: 0 } }, -}; -// Most-specific first -const MODEL_COST_PATTERNS = [ - { match: (id) => /claude.*opus.*fast/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus-fast"] }, - { match: (id) => /claude.*opus/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus"] }, - { match: (id) => /claude.*haiku/i.test(id), cost: MODEL_COST_TABLE["claude-4.5-haiku"] }, - { match: (id) => /claude.*sonnet/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] }, - { match: (id) => /claude/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] }, - { match: (id) => /composer-?2/i.test(id), cost: MODEL_COST_TABLE["composer-2"] }, - { match: (id) => /composer-?1\.5/i.test(id), cost: MODEL_COST_TABLE["composer-1.5"] }, - { match: (id) => /composer/i.test(id), cost: MODEL_COST_TABLE["composer-1"] }, - { match: (id) => /gpt-5\.4.*nano/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-nano"] }, - { match: (id) => /gpt-5\.4.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-mini"] }, - { match: (id) => /gpt-5\.4/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4"] }, - { match: (id) => /gpt-5\.3/i.test(id), cost: MODEL_COST_TABLE["gpt-5.3-codex"] }, - { match: (id) => /gpt-5\.2/i.test(id), cost: MODEL_COST_TABLE["gpt-5.2"] }, - { match: (id) => /gpt-5\.1.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex-mini"] }, - { match: (id) => /gpt-5\.1/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex"] }, - { match: (id) => /gpt-5.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5-mini"] }, - { match: (id) => /gpt-5.*fast/i.test(id), cost: MODEL_COST_TABLE["gpt-5-fast"] }, - { match: (id) => /gpt-5/i.test(id), cost: MODEL_COST_TABLE["gpt-5"] }, - { match: (id) => /gemini.*3\.1/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] }, - { match: (id) => /gemini.*3.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-3-flash"] }, - { match: (id) => /gemini.*3/i.test(id), cost: MODEL_COST_TABLE["gemini-3-pro"] }, - { match: (id) => /gemini.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-2.5-flash"] }, - { match: (id) => /gemini/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] }, - { match: (id) => /grok/i.test(id), cost: MODEL_COST_TABLE["grok-4.20"] }, - { match: (id) => /kimi/i.test(id), cost: MODEL_COST_TABLE["kimi-k2.5"] }, -]; -const DEFAULT_COST = { input: 3, output: 15, cache: { read: 0.3, write: 0 } }; -function estimateModelCost(modelId) { - const normalized = modelId.toLowerCase(); - const exact = MODEL_COST_TABLE[normalized]; - if (exact) - return exact; - const stripped = normalized.replace(/-(high|medium|low|preview|thinking|spark-preview)$/g, ""); - const strippedMatch = MODEL_COST_TABLE[stripped]; - if (strippedMatch) - return strippedMatch; - return MODEL_COST_PATTERNS.find((p) => p.match(normalized))?.cost ?? DEFAULT_COST; -} -export default CursorAuthPlugin; diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/models.d.ts b/references/upstream-plugins/opencode-cursor-oauth/dist/models.d.ts deleted file mode 100644 index 8dd3958f..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/models.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface CursorModel { - id: string; - name: string; - reasoning: boolean; - contextWindow: number; - maxTokens: number; -} -export declare function getCursorModels(apiKey: string): Promise; -/** @internal Test-only. */ -export declare function clearModelCache(): void; diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/models.js b/references/upstream-plugins/opencode-cursor-oauth/dist/models.js deleted file mode 100644 index de379add..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/models.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Cursor model discovery via GetUsableModels. - * Uses the H2 bridge for transport. Falls back to a hardcoded list - * when discovery fails. - */ -import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; -import { z } from "zod"; -import { callCursorUnaryRpc } from "./proxy"; -import { GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, } from "./proto/agent_pb"; -const GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels"; -const DEFAULT_CONTEXT_WINDOW = 200_000; -const DEFAULT_MAX_TOKENS = 64_000; -const CursorModelDetailsSchema = z.object({ - modelId: z.string(), - displayName: z.string().optional().catch(undefined), - displayNameShort: z.string().optional().catch(undefined), - displayModelId: z.string().optional().catch(undefined), - aliases: z - .array(z.unknown()) - .optional() - .catch([]) - .transform((aliases) => (aliases ?? []).filter((alias) => typeof alias === "string")), - thinkingDetails: z.unknown().optional(), -}); -const FALLBACK_MODELS = [ - // Composer models - { id: "composer-1", name: "Composer 1", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 }, - { id: "composer-1.5", name: "Composer 1.5", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 }, - // Claude models - { id: "claude-4.6-opus-high", name: "Claude 4.6 Opus", reasoning: true, contextWindow: 200_000, maxTokens: 128_000 }, - { id: "claude-4.6-sonnet-medium", name: "Claude 4.6 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 }, - { id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 }, - // GPT models - { id: "gpt-5.4-medium", name: "GPT-5.4", reasoning: true, contextWindow: 272_000, maxTokens: 128_000 }, - { id: "gpt-5.2", name: "GPT-5.2", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 }, - { id: "gpt-5.2-codex", name: "GPT-5.2 Codex", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 }, - { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 }, - { id: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark", reasoning: true, contextWindow: 128_000, maxTokens: 128_000 }, - // Other models - { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", reasoning: true, contextWindow: 1_000_000, maxTokens: 64_000 }, - { id: "grok-code-fast-1", name: "Grok Code Fast 1", reasoning: false, contextWindow: 128_000, maxTokens: 64_000 }, -]; -async function fetchCursorUsableModels(apiKey) { - try { - const requestPayload = create(GetUsableModelsRequestSchema, {}); - const requestBody = toBinary(GetUsableModelsRequestSchema, requestPayload); - const response = await callCursorUnaryRpc({ - accessToken: apiKey, - rpcPath: GET_USABLE_MODELS_PATH, - requestBody, - }); - if (response.timedOut || response.exitCode !== 0 || response.body.length === 0) { - return null; - } - const decoded = decodeGetUsableModelsResponse(response.body); - if (!decoded) - return null; - const models = normalizeCursorModels(decoded.models); - return models.length > 0 ? models : null; - } - catch { - return null; - } -} -let cachedModels = null; -export async function getCursorModels(apiKey) { - if (cachedModels) - return cachedModels; - const discovered = await fetchCursorUsableModels(apiKey); - if (discovered && discovered.length > 0) { - cachedModels = discovered; - return cachedModels; - } - return FALLBACK_MODELS; -} -/** @internal Test-only. */ -export function clearModelCache() { - cachedModels = null; -} -function decodeGetUsableModelsResponse(payload) { - try { - return fromBinary(GetUsableModelsResponseSchema, payload); - } - catch { - const framedBody = decodeConnectUnaryBody(payload); - if (!framedBody) - return null; - try { - return fromBinary(GetUsableModelsResponseSchema, framedBody); - } - catch { - return null; - } - } -} -function decodeConnectUnaryBody(payload) { - if (payload.length < 5) - return null; - let offset = 0; - while (offset + 5 <= payload.length) { - const flags = payload[offset]; - const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.byteLength - offset); - const messageLength = view.getUint32(1, false); - const frameEnd = offset + 5 + messageLength; - if (frameEnd > payload.length) - return null; - // Compression flag - if ((flags & 0b0000_0001) !== 0) - return null; - // End-of-stream flag — skip trailer frames - if ((flags & 0b0000_0010) === 0) { - return payload.subarray(offset + 5, frameEnd); - } - offset = frameEnd; - } - return null; -} -function normalizeCursorModels(models) { - if (models.length === 0) - return []; - const byId = new Map(); - for (const model of models) { - const normalized = normalizeSingleModel(model); - if (normalized) - byId.set(normalized.id, normalized); - } - return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id)); -} -function normalizeSingleModel(model) { - const parsed = CursorModelDetailsSchema.safeParse(model); - if (!parsed.success) - return null; - const details = parsed.data; - const id = details.modelId.trim(); - if (!id) - return null; - return { - id, - name: pickDisplayName(details, id), - reasoning: Boolean(details.thinkingDetails), - contextWindow: DEFAULT_CONTEXT_WINDOW, - maxTokens: DEFAULT_MAX_TOKENS, - }; -} -function pickDisplayName(model, fallbackId) { - const candidates = [ - model.displayName, - model.displayNameShort, - model.displayModelId, - ...model.aliases, - fallbackId, - ]; - for (const candidate of candidates) { - if (typeof candidate !== "string") - continue; - const trimmed = candidate.trim(); - if (trimmed) - return trimmed; - } - return fallbackId; -} diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/pkce.d.ts b/references/upstream-plugins/opencode-cursor-oauth/dist/pkce.d.ts deleted file mode 100644 index 0d656da4..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/pkce.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export declare function generatePKCE(): Promise<{ - verifier: string; - challenge: string; -}>; diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/pkce.js b/references/upstream-plugins/opencode-cursor-oauth/dist/pkce.js deleted file mode 100644 index 95766404..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/pkce.js +++ /dev/null @@ -1,9 +0,0 @@ -export async function generatePKCE() { - const verifierBytes = new Uint8Array(96); - crypto.getRandomValues(verifierBytes); - const verifier = Buffer.from(verifierBytes).toString("base64url"); - const data = new TextEncoder().encode(verifier); - const hashBuffer = await crypto.subtle.digest("SHA-256", data); - const challenge = Buffer.from(hashBuffer).toString("base64url"); - return { verifier, challenge }; -} diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/proto/agent_pb.d.ts b/references/upstream-plugins/opencode-cursor-oauth/dist/proto/agent_pb.d.ts deleted file mode 100644 index f241c301..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/proto/agent_pb.d.ts +++ /dev/null @@ -1,13022 +0,0 @@ -import type { Message } from "@bufbuild/protobuf"; -import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; -/** - * Describes the file agent.proto. - */ -export declare const file_agent: GenFile; -/** - * @generated from message agent.v1.GlobToolResult - */ -export type GlobToolResult = Message<"agent.v1.GlobToolResult"> & { - /** - * @generated from oneof agent.v1.GlobToolResult.result - */ - result: { - /** - * @generated from field: agent.v1.GlobToolSuccess success = 1; - */ - value: GlobToolSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.GlobToolError error = 2; - */ - value: GlobToolError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.GlobToolResult. - * Use `create(GlobToolResultSchema)` to create a new message. - */ -export declare const GlobToolResultSchema: GenMessage; -/** - * @generated from message agent.v1.GlobToolError - */ -export type GlobToolError = Message<"agent.v1.GlobToolError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.GlobToolError. - * Use `create(GlobToolErrorSchema)` to create a new message. - */ -export declare const GlobToolErrorSchema: GenMessage; -/** - * Only file results are needed for this tool - * - * @generated from message agent.v1.GlobToolSuccess - */ -export type GlobToolSuccess = Message<"agent.v1.GlobToolSuccess"> & { - /** - * @generated from field: string pattern = 1; - */ - pattern: string; - /** - * @generated from field: string path = 2; - */ - path: string; - /** - * @generated from field: repeated string files = 3; - */ - files: string[]; - /** - * @generated from field: int32 total_files = 4; - */ - totalFiles: number; - /** - * @generated from field: bool client_truncated = 5; - */ - clientTruncated: boolean; - /** - * @generated from field: bool ripgrep_truncated = 6; - */ - ripgrepTruncated: boolean; -}; -/** - * Describes the message agent.v1.GlobToolSuccess. - * Use `create(GlobToolSuccessSchema)` to create a new message. - */ -export declare const GlobToolSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.GlobToolCall - */ -export type GlobToolCall = Message<"agent.v1.GlobToolCall"> & { - /** - * @generated from field: bytes args = 1; - */ - args: Uint8Array; - /** - * @generated from field: agent.v1.GlobToolResult result = 2; - */ - result?: GlobToolResult; -}; -/** - * Describes the message agent.v1.GlobToolCall. - * Use `create(GlobToolCallSchema)` to create a new message. - */ -export declare const GlobToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ReadLintsToolCall - */ -export type ReadLintsToolCall = Message<"agent.v1.ReadLintsToolCall"> & { - /** - * @generated from field: agent.v1.ReadLintsToolArgs args = 1; - */ - args?: ReadLintsToolArgs; - /** - * @generated from field: agent.v1.ReadLintsToolResult result = 2; - */ - result?: ReadLintsToolResult; -}; -/** - * Describes the message agent.v1.ReadLintsToolCall. - * Use `create(ReadLintsToolCallSchema)` to create a new message. - */ -export declare const ReadLintsToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ReadLintsToolArgs - */ -export type ReadLintsToolArgs = Message<"agent.v1.ReadLintsToolArgs"> & { - /** - * @generated from field: repeated string paths = 1; - */ - paths: string[]; -}; -/** - * Describes the message agent.v1.ReadLintsToolArgs. - * Use `create(ReadLintsToolArgsSchema)` to create a new message. - */ -export declare const ReadLintsToolArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ReadLintsToolResult - */ -export type ReadLintsToolResult = Message<"agent.v1.ReadLintsToolResult"> & { - /** - * @generated from oneof agent.v1.ReadLintsToolResult.result - */ - result: { - /** - * @generated from field: agent.v1.ReadLintsToolSuccess success = 1; - */ - value: ReadLintsToolSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ReadLintsToolError error = 2; - */ - value: ReadLintsToolError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReadLintsToolResult. - * Use `create(ReadLintsToolResultSchema)` to create a new message. - */ -export declare const ReadLintsToolResultSchema: GenMessage; -/** - * @generated from message agent.v1.ReadLintsToolSuccess - */ -export type ReadLintsToolSuccess = Message<"agent.v1.ReadLintsToolSuccess"> & { - /** - * @generated from field: repeated agent.v1.FileDiagnostics file_diagnostics = 1; - */ - fileDiagnostics: FileDiagnostics[]; - /** - * @generated from field: int32 total_files = 2; - */ - totalFiles: number; - /** - * @generated from field: int32 total_diagnostics = 3; - */ - totalDiagnostics: number; -}; -/** - * Describes the message agent.v1.ReadLintsToolSuccess. - * Use `create(ReadLintsToolSuccessSchema)` to create a new message. - */ -export declare const ReadLintsToolSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.FileDiagnostics - */ -export type FileDiagnostics = Message<"agent.v1.FileDiagnostics"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: repeated agent.v1.DiagnosticItem diagnostics = 2; - */ - diagnostics: DiagnosticItem[]; - /** - * @generated from field: int32 diagnostics_count = 3; - */ - diagnosticsCount: number; -}; -/** - * Describes the message agent.v1.FileDiagnostics. - * Use `create(FileDiagnosticsSchema)` to create a new message. - */ -export declare const FileDiagnosticsSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticItem - */ -export type DiagnosticItem = Message<"agent.v1.DiagnosticItem"> & { - /** - * @generated from field: agent.v1.DiagnosticSeverity severity = 1; - */ - severity: DiagnosticSeverity; - /** - * @generated from field: agent.v1.DiagnosticRange range = 2; - */ - range?: DiagnosticRange; - /** - * @generated from field: string message = 3; - */ - message: string; - /** - * @generated from field: string source = 4; - */ - source: string; - /** - * @generated from field: string code = 5; - */ - code: string; - /** - * @generated from field: bool is_stale = 6; - */ - isStale: boolean; -}; -/** - * Describes the message agent.v1.DiagnosticItem. - * Use `create(DiagnosticItemSchema)` to create a new message. - */ -export declare const DiagnosticItemSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticRange - */ -export type DiagnosticRange = Message<"agent.v1.DiagnosticRange"> & { - /** - * @generated from field: agent.v1.Position start = 1; - */ - start?: Position; - /** - * @generated from field: agent.v1.Position end = 2; - */ - end?: Position; -}; -/** - * Describes the message agent.v1.DiagnosticRange. - * Use `create(DiagnosticRangeSchema)` to create a new message. - */ -export declare const DiagnosticRangeSchema: GenMessage; -/** - * @generated from message agent.v1.ReadLintsToolError - */ -export type ReadLintsToolError = Message<"agent.v1.ReadLintsToolError"> & { - /** - * @generated from field: string error_message = 1; - */ - errorMessage: string; -}; -/** - * Describes the message agent.v1.ReadLintsToolError. - * Use `create(ReadLintsToolErrorSchema)` to create a new message. - */ -export declare const ReadLintsToolErrorSchema: GenMessage; -/** - * @generated from message agent.v1.McpToolError - */ -export type McpToolError = Message<"agent.v1.McpToolError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.McpToolError. - * Use `create(McpToolErrorSchema)` to create a new message. - */ -export declare const McpToolErrorSchema: GenMessage; -/** - * Result for MCP tool calls (separate from exec results) - * - * @generated from message agent.v1.McpToolResult - */ -export type McpToolResult = Message<"agent.v1.McpToolResult"> & { - /** - * @generated from oneof agent.v1.McpToolResult.result - */ - result: { - /** - * @generated from field: agent.v1.McpSuccess success = 1; - */ - value: McpSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.McpToolError error = 2; - */ - value: McpToolError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.McpRejected rejected = 3; - */ - value: McpRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.McpPermissionDenied permission_denied = 4; - */ - value: McpPermissionDenied; - case: "permissionDenied"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.McpToolResult. - * Use `create(McpToolResultSchema)` to create a new message. - */ -export declare const McpToolResultSchema: GenMessage; -/** - * @generated from message agent.v1.McpToolCall - */ -export type McpToolCall = Message<"agent.v1.McpToolCall"> & { - /** - * @generated from field: agent.v1.McpArgs args = 1; - */ - args?: McpArgs; - /** - * @generated from field: agent.v1.McpToolResult result = 2; - */ - result?: McpToolResult; -}; -/** - * Describes the message agent.v1.McpToolCall. - * Use `create(McpToolCallSchema)` to create a new message. - */ -export declare const McpToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.SemSearchToolCall - */ -export type SemSearchToolCall = Message<"agent.v1.SemSearchToolCall"> & { - /** - * @generated from field: agent.v1.SemSearchToolArgs args = 1; - */ - args?: SemSearchToolArgs; - /** - * @generated from field: agent.v1.SemSearchToolResult result = 2; - */ - result?: SemSearchToolResult; -}; -/** - * Describes the message agent.v1.SemSearchToolCall. - * Use `create(SemSearchToolCallSchema)` to create a new message. - */ -export declare const SemSearchToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.SemSearchToolArgs - */ -export type SemSearchToolArgs = Message<"agent.v1.SemSearchToolArgs"> & { - /** - * @generated from field: string query = 1; - */ - query: string; - /** - * @generated from field: repeated string target_directories = 2; - */ - targetDirectories: string[]; - /** - * @generated from field: string explanation = 3; - */ - explanation: string; -}; -/** - * Describes the message agent.v1.SemSearchToolArgs. - * Use `create(SemSearchToolArgsSchema)` to create a new message. - */ -export declare const SemSearchToolArgsSchema: GenMessage; -/** - * @generated from message agent.v1.SemSearchToolResult - */ -export type SemSearchToolResult = Message<"agent.v1.SemSearchToolResult"> & { - /** - * @generated from oneof agent.v1.SemSearchToolResult.result - */ - result: { - /** - * @generated from field: agent.v1.SemSearchToolSuccess success = 1; - */ - value: SemSearchToolSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.SemSearchToolError error = 2; - */ - value: SemSearchToolError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.SemSearchToolResult. - * Use `create(SemSearchToolResultSchema)` to create a new message. - */ -export declare const SemSearchToolResultSchema: GenMessage; -/** - * @generated from message agent.v1.SemSearchToolSuccess - */ -export type SemSearchToolSuccess = Message<"agent.v1.SemSearchToolSuccess"> & { - /** - * @generated from field: string results = 1; - */ - results: string; - /** - * @generated from field: repeated bytes code_results = 2; - */ - codeResults: Uint8Array[]; -}; -/** - * Describes the message agent.v1.SemSearchToolSuccess. - * Use `create(SemSearchToolSuccessSchema)` to create a new message. - */ -export declare const SemSearchToolSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.SemSearchToolError - */ -export type SemSearchToolError = Message<"agent.v1.SemSearchToolError"> & { - /** - * @generated from field: string error_message = 1; - */ - errorMessage: string; -}; -/** - * Describes the message agent.v1.SemSearchToolError. - * Use `create(SemSearchToolErrorSchema)` to create a new message. - */ -export declare const SemSearchToolErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ListMcpResourcesToolCall - */ -export type ListMcpResourcesToolCall = Message<"agent.v1.ListMcpResourcesToolCall"> & { - /** - * @generated from field: agent.v1.ListMcpResourcesExecArgs args = 1; - */ - args?: ListMcpResourcesExecArgs; - /** - * @generated from field: agent.v1.ListMcpResourcesExecResult result = 2; - */ - result?: ListMcpResourcesExecResult; -}; -/** - * Describes the message agent.v1.ListMcpResourcesToolCall. - * Use `create(ListMcpResourcesToolCallSchema)` to create a new message. - */ -export declare const ListMcpResourcesToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ReadMcpResourceToolCall - */ -export type ReadMcpResourceToolCall = Message<"agent.v1.ReadMcpResourceToolCall"> & { - /** - * @generated from field: agent.v1.ReadMcpResourceExecArgs args = 1; - */ - args?: ReadMcpResourceExecArgs; - /** - * @generated from field: agent.v1.ReadMcpResourceExecResult result = 2; - */ - result?: ReadMcpResourceExecResult; -}; -/** - * Describes the message agent.v1.ReadMcpResourceToolCall. - * Use `create(ReadMcpResourceToolCallSchema)` to create a new message. - */ -export declare const ReadMcpResourceToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.FetchToolCall - */ -export type FetchToolCall = Message<"agent.v1.FetchToolCall"> & { - /** - * @generated from field: agent.v1.FetchArgs args = 1; - */ - args?: FetchArgs; - /** - * @generated from field: agent.v1.FetchResult result = 2; - */ - result?: FetchResult; -}; -/** - * Describes the message agent.v1.FetchToolCall. - * Use `create(FetchToolCallSchema)` to create a new message. - */ -export declare const FetchToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.RecordScreenToolCall - */ -export type RecordScreenToolCall = Message<"agent.v1.RecordScreenToolCall"> & { - /** - * @generated from field: agent.v1.RecordScreenArgs args = 1; - */ - args?: RecordScreenArgs; - /** - * @generated from field: agent.v1.RecordScreenResult result = 2; - */ - result?: RecordScreenResult; -}; -/** - * Describes the message agent.v1.RecordScreenToolCall. - * Use `create(RecordScreenToolCallSchema)` to create a new message. - */ -export declare const RecordScreenToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.WriteShellStdinToolCall - */ -export type WriteShellStdinToolCall = Message<"agent.v1.WriteShellStdinToolCall"> & { - /** - * @generated from field: agent.v1.WriteShellStdinArgs args = 1; - */ - args?: WriteShellStdinArgs; - /** - * @generated from field: agent.v1.WriteShellStdinResult result = 2; - */ - result?: WriteShellStdinResult; -}; -/** - * Describes the message agent.v1.WriteShellStdinToolCall. - * Use `create(WriteShellStdinToolCallSchema)` to create a new message. - */ -export declare const WriteShellStdinToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ReflectArgs - */ -export type ReflectArgs = Message<"agent.v1.ReflectArgs"> & { - /** - * @generated from field: string unexpected_action_outcomes = 1; - */ - unexpectedActionOutcomes: string; - /** - * @generated from field: string relevant_instructions = 2; - */ - relevantInstructions: string; - /** - * @generated from field: string scenario_analysis = 3; - */ - scenarioAnalysis: string; - /** - * @generated from field: string critical_synthesis = 4; - */ - criticalSynthesis: string; - /** - * @generated from field: string next_steps = 5; - */ - nextSteps: string; - /** - * @generated from field: string tool_call_id = 6; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.ReflectArgs. - * Use `create(ReflectArgsSchema)` to create a new message. - */ -export declare const ReflectArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ReflectResult - */ -export type ReflectResult = Message<"agent.v1.ReflectResult"> & { - /** - * @generated from oneof agent.v1.ReflectResult.result - */ - result: { - /** - * @generated from field: agent.v1.ReflectSuccess success = 1; - */ - value: ReflectSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ReflectError error = 2; - */ - value: ReflectError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReflectResult. - * Use `create(ReflectResultSchema)` to create a new message. - */ -export declare const ReflectResultSchema: GenMessage; -/** - * @generated from message agent.v1.ReflectSuccess - */ -export type ReflectSuccess = Message<"agent.v1.ReflectSuccess"> & {}; -/** - * Describes the message agent.v1.ReflectSuccess. - * Use `create(ReflectSuccessSchema)` to create a new message. - */ -export declare const ReflectSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ReflectError - */ -export type ReflectError = Message<"agent.v1.ReflectError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.ReflectError. - * Use `create(ReflectErrorSchema)` to create a new message. - */ -export declare const ReflectErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ReflectToolCall - */ -export type ReflectToolCall = Message<"agent.v1.ReflectToolCall"> & { - /** - * @generated from field: agent.v1.ReflectArgs args = 1; - */ - args?: ReflectArgs; - /** - * @generated from field: agent.v1.ReflectResult result = 2; - */ - result?: ReflectResult; -}; -/** - * Describes the message agent.v1.ReflectToolCall. - * Use `create(ReflectToolCallSchema)` to create a new message. - */ -export declare const ReflectToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindExecutionArgs - */ -export type StartGrindExecutionArgs = Message<"agent.v1.StartGrindExecutionArgs"> & { - /** - * Optional explanation for why the agent is requesting to begin executing. - * - * @generated from field: optional string explanation = 1; - */ - explanation?: string; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.StartGrindExecutionArgs. - * Use `create(StartGrindExecutionArgsSchema)` to create a new message. - */ -export declare const StartGrindExecutionArgsSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindExecutionResult - */ -export type StartGrindExecutionResult = Message<"agent.v1.StartGrindExecutionResult"> & { - /** - * @generated from oneof agent.v1.StartGrindExecutionResult.result - */ - result: { - /** - * @generated from field: agent.v1.StartGrindExecutionSuccess success = 1; - */ - value: StartGrindExecutionSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.StartGrindExecutionError error = 2; - */ - value: StartGrindExecutionError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.StartGrindExecutionResult. - * Use `create(StartGrindExecutionResultSchema)` to create a new message. - */ -export declare const StartGrindExecutionResultSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindExecutionSuccess - */ -export type StartGrindExecutionSuccess = Message<"agent.v1.StartGrindExecutionSuccess"> & {}; -/** - * Describes the message agent.v1.StartGrindExecutionSuccess. - * Use `create(StartGrindExecutionSuccessSchema)` to create a new message. - */ -export declare const StartGrindExecutionSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindExecutionError - */ -export type StartGrindExecutionError = Message<"agent.v1.StartGrindExecutionError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.StartGrindExecutionError. - * Use `create(StartGrindExecutionErrorSchema)` to create a new message. - */ -export declare const StartGrindExecutionErrorSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindExecutionToolCall - */ -export type StartGrindExecutionToolCall = Message<"agent.v1.StartGrindExecutionToolCall"> & { - /** - * @generated from field: agent.v1.StartGrindExecutionArgs args = 1; - */ - args?: StartGrindExecutionArgs; - /** - * @generated from field: agent.v1.StartGrindExecutionResult result = 2; - */ - result?: StartGrindExecutionResult; -}; -/** - * Describes the message agent.v1.StartGrindExecutionToolCall. - * Use `create(StartGrindExecutionToolCallSchema)` to create a new message. - */ -export declare const StartGrindExecutionToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindPlanningArgs - */ -export type StartGrindPlanningArgs = Message<"agent.v1.StartGrindPlanningArgs"> & { - /** - * Optional explanation for why the agent is requesting to return to planning. - * - * @generated from field: optional string explanation = 1; - */ - explanation?: string; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.StartGrindPlanningArgs. - * Use `create(StartGrindPlanningArgsSchema)` to create a new message. - */ -export declare const StartGrindPlanningArgsSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindPlanningResult - */ -export type StartGrindPlanningResult = Message<"agent.v1.StartGrindPlanningResult"> & { - /** - * @generated from oneof agent.v1.StartGrindPlanningResult.result - */ - result: { - /** - * @generated from field: agent.v1.StartGrindPlanningSuccess success = 1; - */ - value: StartGrindPlanningSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.StartGrindPlanningError error = 2; - */ - value: StartGrindPlanningError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.StartGrindPlanningResult. - * Use `create(StartGrindPlanningResultSchema)` to create a new message. - */ -export declare const StartGrindPlanningResultSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindPlanningSuccess - */ -export type StartGrindPlanningSuccess = Message<"agent.v1.StartGrindPlanningSuccess"> & {}; -/** - * Describes the message agent.v1.StartGrindPlanningSuccess. - * Use `create(StartGrindPlanningSuccessSchema)` to create a new message. - */ -export declare const StartGrindPlanningSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindPlanningError - */ -export type StartGrindPlanningError = Message<"agent.v1.StartGrindPlanningError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.StartGrindPlanningError. - * Use `create(StartGrindPlanningErrorSchema)` to create a new message. - */ -export declare const StartGrindPlanningErrorSchema: GenMessage; -/** - * @generated from message agent.v1.StartGrindPlanningToolCall - */ -export type StartGrindPlanningToolCall = Message<"agent.v1.StartGrindPlanningToolCall"> & { - /** - * @generated from field: agent.v1.StartGrindPlanningArgs args = 1; - */ - args?: StartGrindPlanningArgs; - /** - * @generated from field: agent.v1.StartGrindPlanningResult result = 2; - */ - result?: StartGrindPlanningResult; -}; -/** - * Describes the message agent.v1.StartGrindPlanningToolCall. - * Use `create(StartGrindPlanningToolCallSchema)` to create a new message. - */ -export declare const StartGrindPlanningToolCallSchema: GenMessage; -/** - * var AgentMode; (function (AgentMode) { AgentMode[AgentMode["UNSPECIFIED"] = 0] = "UNSPECIFIED"; AgentMode[AgentMode["AGENT"] = 1] = "AGENT"; AgentMode[AgentMode["ASK"] = 2] = "ASK"; AgentMode[AgentMode["PLAN"] = 3] = "PLAN"; AgentMode[AgentMode["DEBUG"] = 4] = "DEBUG"; AgentMode[AgentMode["TRIAGE"] = 5] = "TRIAGE"; AgentMode[AgentMode["PROJECT"] = 6] = "PROJECT"; })(AgentMode || (AgentMode = {})); // Retrieve enum metadata with: proto3.getEnumType(AgentMode) proto3/* int32 *\/.C.util.setEnumType(AgentMode, "agent.v1.AgentMode", [ { no: 0, name: "AGENT_MODE_UNSPECIFIED" }, { no: 1, name: "AGENT_MODE_AGENT" }, { no: 2, name: "AGENT_MODE_ASK" }, { no: 3, name: "AGENT_MODE_PLAN" }, { no: 4, name: "AGENT_MODE_DEBUG" }, { no: 5, name: "AGENT_MODE_TRIAGE" }, { no: 6, name: "AGENT_MODE_PROJECT" }, ]); - * - * @generated from message agent.v1.TaskArgs - */ -export type TaskArgs = Message<"agent.v1.TaskArgs"> & { - /** - * @generated from field: string description = 1; - */ - description: string; - /** - * @generated from field: string prompt = 2; - */ - prompt: string; - /** - * @generated from field: agent.v1.SubagentType subagent_type = 3; - */ - subagentType?: SubagentType; - /** - * @generated from field: optional string model = 4; - */ - model?: string; - /** - * @generated from field: optional string resume = 5; - */ - resume?: string; -}; -/** - * Describes the message agent.v1.TaskArgs. - * Use `create(TaskArgsSchema)` to create a new message. - */ -export declare const TaskArgsSchema: GenMessage; -/** - * @generated from message agent.v1.TaskSuccess - */ -export type TaskSuccess = Message<"agent.v1.TaskSuccess"> & { - /** - * @generated from field: repeated agent.v1.ConversationStep conversation_steps = 1; - */ - conversationSteps: ConversationStep[]; - /** - * @generated from field: optional string agent_id = 2; - */ - agentId?: string; - /** - * @generated from field: bool is_background = 3; - */ - isBackground: boolean; - /** - * @generated from field: optional uint64 duration_ms = 4; - */ - durationMs?: bigint; -}; -/** - * Describes the message agent.v1.TaskSuccess. - * Use `create(TaskSuccessSchema)` to create a new message. - */ -export declare const TaskSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.TaskError - */ -export type TaskError = Message<"agent.v1.TaskError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.TaskError. - * Use `create(TaskErrorSchema)` to create a new message. - */ -export declare const TaskErrorSchema: GenMessage; -/** - * @generated from message agent.v1.TaskResult - */ -export type TaskResult = Message<"agent.v1.TaskResult"> & { - /** - * @generated from oneof agent.v1.TaskResult.result - */ - result: { - /** - * @generated from field: agent.v1.TaskSuccess success = 1; - */ - value: TaskSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.TaskError error = 2; - */ - value: TaskError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.TaskResult. - * Use `create(TaskResultSchema)` to create a new message. - */ -export declare const TaskResultSchema: GenMessage; -/** - * @generated from message agent.v1.TaskToolCall - */ -export type TaskToolCall = Message<"agent.v1.TaskToolCall"> & { - /** - * @generated from field: agent.v1.TaskArgs args = 1; - */ - args?: TaskArgs; - /** - * @generated from field: agent.v1.TaskResult result = 2; - */ - result?: TaskResult; -}; -/** - * Describes the message agent.v1.TaskToolCall. - * Use `create(TaskToolCallSchema)` to create a new message. - */ -export declare const TaskToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.TaskToolCallDelta - */ -export type TaskToolCallDelta = Message<"agent.v1.TaskToolCallDelta"> & { - /** - * @generated from field: agent.v1.InteractionUpdate interaction_update = 1; - */ - interactionUpdate?: InteractionUpdate; -}; -/** - * Describes the message agent.v1.TaskToolCallDelta. - * Use `create(TaskToolCallDeltaSchema)` to create a new message. - */ -export declare const TaskToolCallDeltaSchema: GenMessage; -/** - * Tool messages (from tool.proto) - * - * @generated from message agent.v1.ToolCall - */ -export type ToolCall = Message<"agent.v1.ToolCall"> & { - /** - * @generated from oneof agent.v1.ToolCall.tool - */ - tool: { - /** - * @generated from field: agent.v1.ShellToolCall shell_tool_call = 1; - */ - value: ShellToolCall; - case: "shellToolCall"; - } | { - /** - * @generated from field: agent.v1.DeleteToolCall delete_tool_call = 3; - */ - value: DeleteToolCall; - case: "deleteToolCall"; - } | { - /** - * @generated from field: agent.v1.GlobToolCall glob_tool_call = 4; - */ - value: GlobToolCall; - case: "globToolCall"; - } | { - /** - * @generated from field: agent.v1.GrepToolCall grep_tool_call = 5; - */ - value: GrepToolCall; - case: "grepToolCall"; - } | { - /** - * @generated from field: agent.v1.ReadToolCall read_tool_call = 8; - */ - value: ReadToolCall; - case: "readToolCall"; - } | { - /** - * @generated from field: agent.v1.UpdateTodosToolCall update_todos_tool_call = 9; - */ - value: UpdateTodosToolCall; - case: "updateTodosToolCall"; - } | { - /** - * @generated from field: agent.v1.ReadTodosToolCall read_todos_tool_call = 10; - */ - value: ReadTodosToolCall; - case: "readTodosToolCall"; - } | { - /** - * @generated from field: agent.v1.EditToolCall edit_tool_call = 12; - */ - value: EditToolCall; - case: "editToolCall"; - } | { - /** - * @generated from field: agent.v1.LsToolCall ls_tool_call = 13; - */ - value: LsToolCall; - case: "lsToolCall"; - } | { - /** - * @generated from field: agent.v1.ReadLintsToolCall read_lints_tool_call = 14; - */ - value: ReadLintsToolCall; - case: "readLintsToolCall"; - } | { - /** - * @generated from field: agent.v1.McpToolCall mcp_tool_call = 15; - */ - value: McpToolCall; - case: "mcpToolCall"; - } | { - /** - * @generated from field: agent.v1.SemSearchToolCall sem_search_tool_call = 16; - */ - value: SemSearchToolCall; - case: "semSearchToolCall"; - } | { - /** - * @generated from field: agent.v1.CreatePlanToolCall create_plan_tool_call = 17; - */ - value: CreatePlanToolCall; - case: "createPlanToolCall"; - } | { - /** - * @generated from field: agent.v1.WebSearchToolCall web_search_tool_call = 18; - */ - value: WebSearchToolCall; - case: "webSearchToolCall"; - } | { - /** - * @generated from field: agent.v1.TaskToolCall task_tool_call = 19; - */ - value: TaskToolCall; - case: "taskToolCall"; - } | { - /** - * @generated from field: agent.v1.ListMcpResourcesToolCall list_mcp_resources_tool_call = 20; - */ - value: ListMcpResourcesToolCall; - case: "listMcpResourcesToolCall"; - } | { - /** - * @generated from field: agent.v1.ReadMcpResourceToolCall read_mcp_resource_tool_call = 21; - */ - value: ReadMcpResourceToolCall; - case: "readMcpResourceToolCall"; - } | { - /** - * @generated from field: agent.v1.ApplyAgentDiffToolCall apply_agent_diff_tool_call = 22; - */ - value: ApplyAgentDiffToolCall; - case: "applyAgentDiffToolCall"; - } | { - /** - * @generated from field: agent.v1.AskQuestionToolCall ask_question_tool_call = 23; - */ - value: AskQuestionToolCall; - case: "askQuestionToolCall"; - } | { - /** - * @generated from field: agent.v1.FetchToolCall fetch_tool_call = 24; - */ - value: FetchToolCall; - case: "fetchToolCall"; - } | { - /** - * @generated from field: agent.v1.SwitchModeToolCall switch_mode_tool_call = 25; - */ - value: SwitchModeToolCall; - case: "switchModeToolCall"; - } | { - /** - * @generated from field: agent.v1.ExaSearchToolCall exa_search_tool_call = 26; - */ - value: ExaSearchToolCall; - case: "exaSearchToolCall"; - } | { - /** - * @generated from field: agent.v1.ExaFetchToolCall exa_fetch_tool_call = 27; - */ - value: ExaFetchToolCall; - case: "exaFetchToolCall"; - } | { - /** - * @generated from field: agent.v1.GenerateImageToolCall generate_image_tool_call = 28; - */ - value: GenerateImageToolCall; - case: "generateImageToolCall"; - } | { - /** - * @generated from field: agent.v1.RecordScreenToolCall record_screen_tool_call = 29; - */ - value: RecordScreenToolCall; - case: "recordScreenToolCall"; - } | { - /** - * @generated from field: agent.v1.ComputerUseToolCall computer_use_tool_call = 30; - */ - value: ComputerUseToolCall; - case: "computerUseToolCall"; - } | { - /** - * @generated from field: agent.v1.WriteShellStdinToolCall write_shell_stdin_tool_call = 31; - */ - value: WriteShellStdinToolCall; - case: "writeShellStdinToolCall"; - } | { - /** - * @generated from field: agent.v1.ReflectToolCall reflect_tool_call = 32; - */ - value: ReflectToolCall; - case: "reflectToolCall"; - } | { - /** - * @generated from field: agent.v1.SetupVmEnvironmentToolCall setup_vm_environment_tool_call = 33; - */ - value: SetupVmEnvironmentToolCall; - case: "setupVmEnvironmentToolCall"; - } | { - /** - * @generated from field: agent.v1.TruncatedToolCall truncated_tool_call = 34; - */ - value: TruncatedToolCall; - case: "truncatedToolCall"; - } | { - /** - * @generated from field: agent.v1.StartGrindExecutionToolCall start_grind_execution_tool_call = 35; - */ - value: StartGrindExecutionToolCall; - case: "startGrindExecutionToolCall"; - } | { - /** - * @generated from field: agent.v1.StartGrindPlanningToolCall start_grind_planning_tool_call = 36; - */ - value: StartGrindPlanningToolCall; - case: "startGrindPlanningToolCall"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ToolCall. - * Use `create(ToolCallSchema)` to create a new message. - */ -export declare const ToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.TruncatedToolCallArgs - */ -export type TruncatedToolCallArgs = Message<"agent.v1.TruncatedToolCallArgs"> & {}; -/** - * Describes the message agent.v1.TruncatedToolCallArgs. - * Use `create(TruncatedToolCallArgsSchema)` to create a new message. - */ -export declare const TruncatedToolCallArgsSchema: GenMessage; -/** - * @generated from message agent.v1.TruncatedToolCallSuccess - */ -export type TruncatedToolCallSuccess = Message<"agent.v1.TruncatedToolCallSuccess"> & {}; -/** - * Describes the message agent.v1.TruncatedToolCallSuccess. - * Use `create(TruncatedToolCallSuccessSchema)` to create a new message. - */ -export declare const TruncatedToolCallSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.TruncatedToolCallError - */ -export type TruncatedToolCallError = Message<"agent.v1.TruncatedToolCallError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.TruncatedToolCallError. - * Use `create(TruncatedToolCallErrorSchema)` to create a new message. - */ -export declare const TruncatedToolCallErrorSchema: GenMessage; -/** - * @generated from message agent.v1.TruncatedToolCallResult - */ -export type TruncatedToolCallResult = Message<"agent.v1.TruncatedToolCallResult"> & { - /** - * @generated from oneof agent.v1.TruncatedToolCallResult.result - */ - result: { - /** - * @generated from field: agent.v1.TruncatedToolCallSuccess success = 1; - */ - value: TruncatedToolCallSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.TruncatedToolCallError error = 2; - */ - value: TruncatedToolCallError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.TruncatedToolCallResult. - * Use `create(TruncatedToolCallResultSchema)` to create a new message. - */ -export declare const TruncatedToolCallResultSchema: GenMessage; -/** - * Placeholder for tool calls that were truncated due to size limits. - * - * @generated from message agent.v1.TruncatedToolCall - */ -export type TruncatedToolCall = Message<"agent.v1.TruncatedToolCall"> & { - /** - * @generated from field: bytes original_step_blob_id = 1; - */ - originalStepBlobId: Uint8Array; - /** - * unused, just matches the discriminated union for other tool calls - * - * @generated from field: agent.v1.TruncatedToolCallArgs args = 2; - */ - args?: TruncatedToolCallArgs; - /** - * @generated from field: agent.v1.TruncatedToolCallResult result = 3; - */ - result?: TruncatedToolCallResult; -}; -/** - * Describes the message agent.v1.TruncatedToolCall. - * Use `create(TruncatedToolCallSchema)` to create a new message. - */ -export declare const TruncatedToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ToolCallDelta - */ -export type ToolCallDelta = Message<"agent.v1.ToolCallDelta"> & { - /** - * @generated from oneof agent.v1.ToolCallDelta.delta - */ - delta: { - /** - * @generated from field: agent.v1.ShellToolCallDelta shell_tool_call_delta = 1; - */ - value: ShellToolCallDelta; - case: "shellToolCallDelta"; - } | { - /** - * @generated from field: agent.v1.TaskToolCallDelta task_tool_call_delta = 2; - */ - value: TaskToolCallDelta; - case: "taskToolCallDelta"; - } | { - /** - * @generated from field: agent.v1.EditToolCallDelta edit_tool_call_delta = 3; - */ - value: EditToolCallDelta; - case: "editToolCallDelta"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ToolCallDelta. - * Use `create(ToolCallDeltaSchema)` to create a new message. - */ -export declare const ToolCallDeltaSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationStep - */ -export type ConversationStep = Message<"agent.v1.ConversationStep"> & { - /** - * @generated from oneof agent.v1.ConversationStep.message - */ - message: { - /** - * @generated from field: agent.v1.AssistantMessage assistant_message = 1; - */ - value: AssistantMessage; - case: "assistantMessage"; - } | { - /** - * @generated from field: agent.v1.ToolCall tool_call = 2; - */ - value: ToolCall; - case: "toolCall"; - } | { - /** - * @generated from field: agent.v1.ThinkingMessage thinking_message = 3; - */ - value: ThinkingMessage; - case: "thinkingMessage"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ConversationStep. - * Use `create(ConversationStepSchema)` to create a new message. - */ -export declare const ConversationStepSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationAction - */ -export type ConversationAction = Message<"agent.v1.ConversationAction"> & { - /** - * @generated from oneof agent.v1.ConversationAction.action - */ - action: { - /** - * @generated from field: agent.v1.UserMessageAction user_message_action = 1; - */ - value: UserMessageAction; - case: "userMessageAction"; - } | { - /** - * @generated from field: agent.v1.ResumeAction resume_action = 2; - */ - value: ResumeAction; - case: "resumeAction"; - } | { - /** - * @generated from field: agent.v1.CancelAction cancel_action = 3; - */ - value: CancelAction; - case: "cancelAction"; - } | { - /** - * @generated from field: agent.v1.SummarizeAction summarize_action = 4; - */ - value: SummarizeAction; - case: "summarizeAction"; - } | { - /** - * @generated from field: agent.v1.ShellCommandAction shell_command_action = 5; - */ - value: ShellCommandAction; - case: "shellCommandAction"; - } | { - /** - * @generated from field: agent.v1.StartPlanAction start_plan_action = 6; - */ - value: StartPlanAction; - case: "startPlanAction"; - } | { - /** - * @generated from field: agent.v1.ExecutePlanAction execute_plan_action = 7; - */ - value: ExecutePlanAction; - case: "executePlanAction"; - } | { - /** - * @generated from field: agent.v1.AsyncAskQuestionCompletionAction async_ask_question_completion_action = 8; - */ - value: AsyncAskQuestionCompletionAction; - case: "asyncAskQuestionCompletionAction"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ConversationAction. - * Use `create(ConversationActionSchema)` to create a new message. - */ -export declare const ConversationActionSchema: GenMessage; -/** - * @generated from message agent.v1.UserMessageAction - */ -export type UserMessageAction = Message<"agent.v1.UserMessageAction"> & { - /** - * @generated from field: agent.v1.UserMessage user_message = 1; - */ - userMessage?: UserMessage; - /** - * @generated from field: agent.v1.RequestContext request_context = 2; - */ - requestContext?: RequestContext; - /** - * @generated from field: optional bool send_to_interaction_listener = 3; - */ - sendToInteractionListener?: boolean; -}; -/** - * Describes the message agent.v1.UserMessageAction. - * Use `create(UserMessageActionSchema)` to create a new message. - */ -export declare const UserMessageActionSchema: GenMessage; -/** - * @generated from message agent.v1.CancelAction - */ -export type CancelAction = Message<"agent.v1.CancelAction"> & {}; -/** - * Describes the message agent.v1.CancelAction. - * Use `create(CancelActionSchema)` to create a new message. - */ -export declare const CancelActionSchema: GenMessage; -/** - * @generated from message agent.v1.ResumeAction - */ -export type ResumeAction = Message<"agent.v1.ResumeAction"> & { - /** - * @generated from field: agent.v1.RequestContext request_context = 2; - */ - requestContext?: RequestContext; -}; -/** - * Describes the message agent.v1.ResumeAction. - * Use `create(ResumeActionSchema)` to create a new message. - */ -export declare const ResumeActionSchema: GenMessage; -/** - * @generated from message agent.v1.AsyncAskQuestionCompletionAction - */ -export type AsyncAskQuestionCompletionAction = Message<"agent.v1.AsyncAskQuestionCompletionAction"> & { - /** - * Contains the original tool call ID and the result from the user - * - * @generated from field: string original_tool_call_id = 1; - */ - originalToolCallId: string; - /** - * @generated from field: agent.v1.AskQuestionArgs original_args = 2; - */ - originalArgs?: AskQuestionArgs; - /** - * @generated from field: agent.v1.AskQuestionResult result = 3; - */ - result?: AskQuestionResult; -}; -/** - * Describes the message agent.v1.AsyncAskQuestionCompletionAction. - * Use `create(AsyncAskQuestionCompletionActionSchema)` to create a new message. - */ -export declare const AsyncAskQuestionCompletionActionSchema: GenMessage; -/** - * @generated from message agent.v1.SummarizeAction - */ -export type SummarizeAction = Message<"agent.v1.SummarizeAction"> & {}; -/** - * Describes the message agent.v1.SummarizeAction. - * Use `create(SummarizeActionSchema)` to create a new message. - */ -export declare const SummarizeActionSchema: GenMessage; -/** - * @generated from message agent.v1.ShellCommandAction - */ -export type ShellCommandAction = Message<"agent.v1.ShellCommandAction"> & { - /** - * @generated from field: agent.v1.ShellCommand shell_command = 1; - */ - shellCommand?: ShellCommand; - /** - * unique identifier for preemptive exec attachment - * - * @generated from field: string exec_id = 2; - */ - execId: string; -}; -/** - * Describes the message agent.v1.ShellCommandAction. - * Use `create(ShellCommandActionSchema)` to create a new message. - */ -export declare const ShellCommandActionSchema: GenMessage; -/** - * @generated from message agent.v1.StartPlanAction - */ -export type StartPlanAction = Message<"agent.v1.StartPlanAction"> & { - /** - * @generated from field: agent.v1.UserMessage user_message = 1; - */ - userMessage?: UserMessage; - /** - * @generated from field: agent.v1.RequestContext request_context = 2; - */ - requestContext?: RequestContext; - /** - * @generated from field: bool is_spec = 3; - */ - isSpec: boolean; -}; -/** - * Describes the message agent.v1.StartPlanAction. - * Use `create(StartPlanActionSchema)` to create a new message. - */ -export declare const StartPlanActionSchema: GenMessage; -/** - * @generated from message agent.v1.ExecutePlanAction - */ -export type ExecutePlanAction = Message<"agent.v1.ExecutePlanAction"> & { - /** - * @generated from field: agent.v1.RequestContext request_context = 1; - */ - requestContext?: RequestContext; - /** - * @generated from field: optional agent.v1.ConversationPlan plan = 2; - */ - plan?: ConversationPlan; - /** - * e.g., "cursor-plan://composerId/plan.md" - * - * @generated from field: optional string plan_file_uri = 3; - */ - planFileUri?: string; - /** - * The actual plan content from the file - * - * @generated from field: optional string plan_file_content = 4; - */ - planFileContent?: string; -}; -/** - * Describes the message agent.v1.ExecutePlanAction. - * Use `create(ExecutePlanActionSchema)` to create a new message. - */ -export declare const ExecutePlanActionSchema: GenMessage; -/** - * @generated from message agent.v1.UserMessage - */ -export type UserMessage = Message<"agent.v1.UserMessage"> & { - /** - * @generated from field: string text = 1; - */ - text: string; - /** - * @generated from field: string message_id = 2; - */ - messageId: string; - /** - * @generated from field: optional agent.v1.SelectedContext selected_context = 3; - */ - selectedContext?: SelectedContext; - /** - * @generated from field: int32 mode = 4; - */ - mode: number; - /** - * @generated from field: optional bool is_simulated_msg = 5; - */ - isSimulatedMsg?: boolean; - /** - * @generated from field: optional string best_of_n_group_id = 6; - */ - bestOfNGroupId?: string; - /** - * @generated from field: optional bool try_use_best_of_n_promotion = 7; - */ - tryUseBestOfNPromotion?: boolean; - /** - * @generated from field: optional string rich_text = 8; - */ - richText?: string; -}; -/** - * Describes the message agent.v1.UserMessage. - * Use `create(UserMessageSchema)` to create a new message. - */ -export declare const UserMessageSchema: GenMessage; -/** - * @generated from message agent.v1.AssistantMessage - */ -export type AssistantMessage = Message<"agent.v1.AssistantMessage"> & { - /** - * @generated from field: string text = 1; - */ - text: string; -}; -/** - * Describes the message agent.v1.AssistantMessage. - * Use `create(AssistantMessageSchema)` to create a new message. - */ -export declare const AssistantMessageSchema: GenMessage; -/** - * @generated from message agent.v1.ThinkingMessage - */ -export type ThinkingMessage = Message<"agent.v1.ThinkingMessage"> & { - /** - * @generated from field: string text = 1; - */ - text: string; - /** - * @generated from field: uint32 duration_ms = 2; - */ - durationMs: number; -}; -/** - * Describes the message agent.v1.ThinkingMessage. - * Use `create(ThinkingMessageSchema)` to create a new message. - */ -export declare const ThinkingMessageSchema: GenMessage; -/** - * @generated from message agent.v1.ShellCommand - */ -export type ShellCommand = Message<"agent.v1.ShellCommand"> & { - /** - * @generated from field: string command = 1; - */ - command: string; -}; -/** - * Describes the message agent.v1.ShellCommand. - * Use `create(ShellCommandSchema)` to create a new message. - */ -export declare const ShellCommandSchema: GenMessage; -/** - * @generated from message agent.v1.ShellOutput - */ -export type ShellOutput = Message<"agent.v1.ShellOutput"> & { - /** - * @generated from field: string stdout = 1; - */ - stdout: string; - /** - * @generated from field: string stderr = 2; - */ - stderr: string; - /** - * @generated from field: int32 exit_code = 3; - */ - exitCode: number; -}; -/** - * Describes the message agent.v1.ShellOutput. - * Use `create(ShellOutputSchema)` to create a new message. - */ -export declare const ShellOutputSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationTurn - */ -export type ConversationTurn = Message<"agent.v1.ConversationTurn"> & { - /** - * @generated from oneof agent.v1.ConversationTurn.turn - */ - turn: { - /** - * @generated from field: agent.v1.AgentConversationTurn agent_conversation_turn = 1; - */ - value: AgentConversationTurn; - case: "agentConversationTurn"; - } | { - /** - * @generated from field: agent.v1.ShellConversationTurn shell_conversation_turn = 2; - */ - value: ShellConversationTurn; - case: "shellConversationTurn"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ConversationTurn. - * Use `create(ConversationTurnSchema)` to create a new message. - */ -export declare const ConversationTurnSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationPlan - */ -export type ConversationPlan = Message<"agent.v1.ConversationPlan"> & { - /** - * @generated from field: string plan = 1; - */ - plan: string; -}; -/** - * Describes the message agent.v1.ConversationPlan. - * Use `create(ConversationPlanSchema)` to create a new message. - */ -export declare const ConversationPlanSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationTurnStructure - */ -export type ConversationTurnStructure = Message<"agent.v1.ConversationTurnStructure"> & { - /** - * @generated from oneof agent.v1.ConversationTurnStructure.turn - */ - turn: { - /** - * @generated from field: agent.v1.AgentConversationTurnStructure agent_conversation_turn = 1; - */ - value: AgentConversationTurnStructure; - case: "agentConversationTurn"; - } | { - /** - * @generated from field: agent.v1.ShellConversationTurnStructure shell_conversation_turn = 2; - */ - value: ShellConversationTurnStructure; - case: "shellConversationTurn"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ConversationTurnStructure. - * Use `create(ConversationTurnStructureSchema)` to create a new message. - */ -export declare const ConversationTurnStructureSchema: GenMessage; -/** - * @generated from message agent.v1.AgentConversationTurn - */ -export type AgentConversationTurn = Message<"agent.v1.AgentConversationTurn"> & { - /** - * @generated from field: agent.v1.UserMessage user_message = 1; - */ - userMessage?: UserMessage; - /** - * @generated from field: repeated agent.v1.ConversationStep steps = 2; - */ - steps: ConversationStep[]; - /** - * The request ID associated with this turn, used for analytics tracking - * - * @generated from field: optional string request_id = 3; - */ - requestId?: string; -}; -/** - * Describes the message agent.v1.AgentConversationTurn. - * Use `create(AgentConversationTurnSchema)` to create a new message. - */ -export declare const AgentConversationTurnSchema: GenMessage; -/** - * @generated from message agent.v1.AgentConversationTurnStructure - */ -export type AgentConversationTurnStructure = Message<"agent.v1.AgentConversationTurnStructure"> & { - /** - * @generated from field: bytes user_message = 1; - */ - userMessage: Uint8Array; - /** - * @generated from field: repeated bytes steps = 2; - */ - steps: Uint8Array[]; - /** - * The request ID associated with this turn, used for analytics tracking - * - * @generated from field: optional string request_id = 3; - */ - requestId?: string; -}; -/** - * Describes the message agent.v1.AgentConversationTurnStructure. - * Use `create(AgentConversationTurnStructureSchema)` to create a new message. - */ -export declare const AgentConversationTurnStructureSchema: GenMessage; -/** - * @generated from message agent.v1.ShellConversationTurn - */ -export type ShellConversationTurn = Message<"agent.v1.ShellConversationTurn"> & { - /** - * @generated from field: agent.v1.ShellCommand shell_command = 1; - */ - shellCommand?: ShellCommand; - /** - * @generated from field: agent.v1.ShellOutput shell_output = 2; - */ - shellOutput?: ShellOutput; -}; -/** - * Describes the message agent.v1.ShellConversationTurn. - * Use `create(ShellConversationTurnSchema)` to create a new message. - */ -export declare const ShellConversationTurnSchema: GenMessage; -/** - * @generated from message agent.v1.ShellConversationTurnStructure - */ -export type ShellConversationTurnStructure = Message<"agent.v1.ShellConversationTurnStructure"> & { - /** - * @generated from field: bytes shell_command = 1; - */ - shellCommand: Uint8Array; - /** - * @generated from field: bytes shell_output = 2; - */ - shellOutput: Uint8Array; -}; -/** - * Describes the message agent.v1.ShellConversationTurnStructure. - * Use `create(ShellConversationTurnStructureSchema)` to create a new message. - */ -export declare const ShellConversationTurnStructureSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationSummary - */ -export type ConversationSummary = Message<"agent.v1.ConversationSummary"> & { - /** - * @generated from field: string summary = 1; - */ - summary: string; -}; -/** - * Describes the message agent.v1.ConversationSummary. - * Use `create(ConversationSummarySchema)` to create a new message. - */ -export declare const ConversationSummarySchema: GenMessage; -/** - * @generated from message agent.v1.ConversationSummaryArchive - */ -export type ConversationSummaryArchive = Message<"agent.v1.ConversationSummaryArchive"> & { - /** - * @generated from field: repeated bytes summarized_messages = 1; - */ - summarizedMessages: Uint8Array[]; - /** - * @generated from field: string summary = 2; - */ - summary: string; - /** - * @generated from field: uint32 window_tail = 3; - */ - windowTail: number; - /** - * @generated from field: bytes summary_message = 4; - */ - summaryMessage: Uint8Array; -}; -/** - * Describes the message agent.v1.ConversationSummaryArchive. - * Use `create(ConversationSummaryArchiveSchema)` to create a new message. - */ -export declare const ConversationSummaryArchiveSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationTokenDetails - */ -export type ConversationTokenDetails = Message<"agent.v1.ConversationTokenDetails"> & { - /** - * @generated from field: uint32 used_tokens = 1; - */ - usedTokens: number; - /** - * @generated from field: uint32 max_tokens = 2; - */ - maxTokens: number; -}; -/** - * Describes the message agent.v1.ConversationTokenDetails. - * Use `create(ConversationTokenDetailsSchema)` to create a new message. - */ -export declare const ConversationTokenDetailsSchema: GenMessage; -/** - * @generated from message agent.v1.FileState - */ -export type FileState = Message<"agent.v1.FileState"> & { - /** - * Optional content. If not set or undefined, the file is considered deleted. - * - * @generated from field: optional string content = 1; - */ - content?: string; - /** - * Optional initial content captured when the file was first tracked. If not set or undefined, the file did not exist when tracking began. - * - * @generated from field: optional string initial_content = 2; - */ - initialContent?: string; -}; -/** - * Describes the message agent.v1.FileState. - * Use `create(FileStateSchema)` to create a new message. - */ -export declare const FileStateSchema: GenMessage; -/** - * @generated from message agent.v1.FileStateStructure - */ -export type FileStateStructure = Message<"agent.v1.FileStateStructure"> & { - /** - * Optional content. If not set or undefined, the file is considered deleted. - * - * @generated from field: optional bytes content = 1; - */ - content?: Uint8Array; - /** - * Optional initial content captured when the file was first tracked. If not set or undefined, the file did not exist when tracking began. - * - * @generated from field: optional bytes initial_content = 2; - */ - initialContent?: Uint8Array; -}; -/** - * Describes the message agent.v1.FileStateStructure. - * Use `create(FileStateStructureSchema)` to create a new message. - */ -export declare const FileStateStructureSchema: GenMessage; -/** - * @generated from message agent.v1.StepTiming - */ -export type StepTiming = Message<"agent.v1.StepTiming"> & { - /** - * @generated from field: uint64 duration_ms = 1; - */ - durationMs: bigint; - /** - * @generated from field: uint64 timestamp_ms = 2; - */ - timestampMs: bigint; -}; -/** - * Describes the message agent.v1.StepTiming. - * Use `create(StepTimingSchema)` to create a new message. - */ -export declare const StepTimingSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationState - */ -export type ConversationState = Message<"agent.v1.ConversationState"> & { - /** - * @generated from field: repeated string root_prompt_messages_json = 1; - */ - rootPromptMessagesJson: string[]; - /** - * @generated from field: repeated agent.v1.ConversationTurn turns = 8; - */ - turns: ConversationTurn[]; - /** - * @generated from field: repeated agent.v1.TodoItem todos = 3; - */ - todos: TodoItem[]; - /** - * Raw JSON stringified tool-call content parts awaiting execution - * - * @generated from field: repeated string pending_tool_calls = 4; - */ - pendingToolCalls: string[]; - /** - * @generated from field: agent.v1.ConversationTokenDetails token_details = 5; - */ - tokenDetails?: ConversationTokenDetails; - /** - * only for when the user explicitly asks for a summary through the summary action - * - * @generated from field: optional agent.v1.ConversationSummary summary = 6; - */ - summary?: ConversationSummary; - /** - * @generated from field: optional agent.v1.ConversationPlan plan = 7; - */ - plan?: ConversationPlan; - /** - * @generated from field: optional agent.v1.ConversationSummaryArchive summary_archive = 9; - */ - summaryArchive?: ConversationSummaryArchive; - /** - * Deprecated, use summary_archives instead @deprecated summaryArchive; - * - * @generated from field: map file_states = 10; - */ - fileStates: { - [key: string]: FileState; - }; - /** - * @generated from field: repeated agent.v1.ConversationSummaryArchive summary_archives = 11; - */ - summaryArchives: ConversationSummaryArchive[]; -}; -/** - * Describes the message agent.v1.ConversationState. - * Use `create(ConversationStateSchema)` to create a new message. - */ -export declare const ConversationStateSchema: GenMessage; -/** - * @generated from message agent.v1.SubagentPersistedState - */ -export type SubagentPersistedState = Message<"agent.v1.SubagentPersistedState"> & { - /** - * The subagent's conversation state structure - * - * @generated from field: agent.v1.ConversationStateStructure conversation_state = 1; - */ - conversationState?: ConversationStateStructure; - /** - * Timestamp when this subagent was first created - * - * @generated from field: uint64 created_timestamp_ms = 2; - */ - createdTimestampMs: bigint; - /** - * Timestamp when this subagent was last used (by task tool call) - * - * @generated from field: uint64 last_used_timestamp_ms = 3; - */ - lastUsedTimestampMs: bigint; - /** - * The subagent type (e.g., computerUse, custom with name) - * - * @generated from field: agent.v1.SubagentType subagent_type = 4; - */ - subagentType?: SubagentType; -}; -/** - * Describes the message agent.v1.SubagentPersistedState. - * Use `create(SubagentPersistedStateSchema)` to create a new message. - */ -export declare const SubagentPersistedStateSchema: GenMessage; -/** - * @generated from message agent.v1.ConversationStateStructure - */ -export type ConversationStateStructure = Message<"agent.v1.ConversationStateStructure"> & { - /** - * @generated from field: repeated bytes turns_old = 2; - */ - turnsOld: Uint8Array[]; - /** - * @deprecated turnsOld = []; - * - * @generated from field: repeated bytes root_prompt_messages_json = 1; - */ - rootPromptMessagesJson: Uint8Array[]; - /** - * @generated from field: repeated bytes turns = 8; - */ - turns: Uint8Array[]; - /** - * @generated from field: repeated bytes todos = 3; - */ - todos: Uint8Array[]; - /** - * Raw JSON stringified tool-call content parts awaiting execution - * - * @generated from field: repeated string pending_tool_calls = 4; - */ - pendingToolCalls: string[]; - /** - * @generated from field: agent.v1.ConversationTokenDetails token_details = 5; - */ - tokenDetails?: ConversationTokenDetails; - /** - * only for when the user explicitly asks for a summary through the summary action - * - * @generated from field: optional bytes summary = 6; - */ - summary?: Uint8Array; - /** - * @generated from field: optional bytes plan = 7; - */ - plan?: Uint8Array; - /** - * @generated from field: repeated string previous_workspace_uris = 9; - */ - previousWorkspaceUris: string[]; - /** - * Current mode of the conversation - * - * @generated from field: optional int32 mode = 10; - */ - mode?: number; - /** - * @generated from field: optional bytes summary_archive = 11; - */ - summaryArchive?: Uint8Array; - /** - * @generated from field: map file_states = 12; - */ - fileStates: { - [key: string]: Uint8Array; - }; - /** - * Deprecated, use summary_archives instead @deprecated summaryArchive; Map of file paths to their latest content (stored as blob IDs in the KV store) Each blob contains a serialized FileState message @deprecated fileStates = {}; Map of file paths to their latest content (stored as FileStateStructure) - * - * @generated from field: map file_states_v2 = 15; - */ - fileStatesV2: { - [key: string]: FileStateStructure; - }; - /** - * @generated from field: repeated bytes summary_archives = 13; - */ - summaryArchives: Uint8Array[]; - /** - * @generated from field: repeated agent.v1.StepTiming turn_timings = 14; - */ - turnTimings: StepTiming[]; - /** - * Subagent resume tracking Map of subagent ID to the persisted subagent state (stored inline) - * - * @generated from field: map subagent_states = 16; - */ - subagentStates: { - [key: string]: SubagentPersistedState; - }; - /** - * Count of self-summaries generated for this conversation - * - * @generated from field: uint32 self_summary_count = 17; - */ - selfSummaryCount: number; - /** - * Set of file paths that have been read during this conversation - * - * @generated from field: repeated string read_paths = 18; - */ - readPaths: string[]; -}; -/** - * Describes the message agent.v1.ConversationStateStructure. - * Use `create(ConversationStateStructureSchema)` to create a new message. - */ -export declare const ConversationStateStructureSchema: GenMessage; -/** - * @generated from message agent.v1.ThinkingDetails - */ -export type ThinkingDetails = Message<"agent.v1.ThinkingDetails"> & {}; -/** - * Describes the message agent.v1.ThinkingDetails. - * Use `create(ThinkingDetailsSchema)` to create a new message. - */ -export declare const ThinkingDetailsSchema: GenMessage; -/** - * @generated from message agent.v1.ApiKeyCredentials - */ -export type ApiKeyCredentials = Message<"agent.v1.ApiKeyCredentials"> & { - /** - * @generated from field: string api_key = 1; - */ - apiKey: string; - /** - * For OpenAI-compatible endpoints - * - * @generated from field: optional string base_url = 2; - */ - baseUrl?: string; -}; -/** - * Describes the message agent.v1.ApiKeyCredentials. - * Use `create(ApiKeyCredentialsSchema)` to create a new message. - */ -export declare const ApiKeyCredentialsSchema: GenMessage; -/** - * @generated from message agent.v1.AzureCredentials - */ -export type AzureCredentials = Message<"agent.v1.AzureCredentials"> & { - /** - * @generated from field: string api_key = 1; - */ - apiKey: string; - /** - * @generated from field: string base_url = 2; - */ - baseUrl: string; - /** - * @generated from field: string deployment = 3; - */ - deployment: string; -}; -/** - * Describes the message agent.v1.AzureCredentials. - * Use `create(AzureCredentialsSchema)` to create a new message. - */ -export declare const AzureCredentialsSchema: GenMessage; -/** - * @generated from message agent.v1.BedrockCredentials - */ -export type BedrockCredentials = Message<"agent.v1.BedrockCredentials"> & { - /** - * @generated from field: string access_key = 1; - */ - accessKey: string; - /** - * @generated from field: string secret_key = 2; - */ - secretKey: string; - /** - * @generated from field: string region = 3; - */ - region: string; - /** - * @generated from field: optional string session_token = 4; - */ - sessionToken?: string; -}; -/** - * Describes the message agent.v1.BedrockCredentials. - * Use `create(BedrockCredentialsSchema)` to create a new message. - */ -export declare const BedrockCredentialsSchema: GenMessage; -/** - * @generated from message agent.v1.ModelDetails - */ -export type ModelDetails = Message<"agent.v1.ModelDetails"> & { - /** - * @generated from field: string model_id = 1; - */ - modelId: string; - /** - * @generated from field: string display_model_id = 3; - */ - displayModelId: string; - /** - * @generated from field: string display_name = 4; - */ - displayName: string; - /** - * @generated from field: string display_name_short = 5; - */ - displayNameShort: string; - /** - * @generated from field: repeated string aliases = 6; - */ - aliases: string[]; - /** - * @generated from field: optional agent.v1.ThinkingDetails thinking_details = 2; - */ - thinkingDetails?: ThinkingDetails; - /** - * @generated from field: optional bool max_mode = 7; - */ - maxMode?: boolean; - /** - * @generated from oneof agent.v1.ModelDetails.credentials - */ - credentials: { - /** - * @generated from field: agent.v1.ApiKeyCredentials api_key_credentials = 8; - */ - value: ApiKeyCredentials; - case: "apiKeyCredentials"; - } | { - /** - * @generated from field: agent.v1.AzureCredentials azure_credentials = 9; - */ - value: AzureCredentials; - case: "azureCredentials"; - } | { - /** - * @generated from field: agent.v1.BedrockCredentials bedrock_credentials = 10; - */ - value: BedrockCredentials; - case: "bedrockCredentials"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ModelDetails. - * Use `create(ModelDetailsSchema)` to create a new message. - */ -export declare const ModelDetailsSchema: GenMessage; -/** - * @generated from message agent.v1.RequestedModel - */ -export type RequestedModel = Message<"agent.v1.RequestedModel"> & { - /** - * @generated from field: string model_id = 1; - */ - modelId: string; - /** - * @generated from field: bool max_mode = 2; - */ - maxMode: boolean; - /** - * @generated from field: repeated agent.v1.RequestedModel_ModelParameterbytes parameters = 3; - */ - parameters: RequestedModel_ModelParameterbytes[]; - /** - * @generated from oneof agent.v1.RequestedModel.credentials - */ - credentials: { - /** - * @generated from field: agent.v1.ApiKeyCredentials api_key_credentials = 4; - */ - value: ApiKeyCredentials; - case: "apiKeyCredentials"; - } | { - /** - * @generated from field: agent.v1.AzureCredentials azure_credentials = 5; - */ - value: AzureCredentials; - case: "azureCredentials"; - } | { - /** - * @generated from field: agent.v1.BedrockCredentials bedrock_credentials = 6; - */ - value: BedrockCredentials; - case: "bedrockCredentials"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.RequestedModel. - * Use `create(RequestedModelSchema)` to create a new message. - */ -export declare const RequestedModelSchema: GenMessage; -/** - * @generated from message agent.v1.RequestedModel_ModelParameterbytes - */ -export type RequestedModel_ModelParameterbytes = Message<"agent.v1.RequestedModel_ModelParameterbytes"> & { - /** - * @generated from field: string id = 1; - */ - id: string; - /** - * All paramters are encoded as strings. For boolean parameters, the value is either "true" or "false". For enum parameters, the value is one of the values in the enum. - * - * @generated from field: string value = 2; - */ - value: string; -}; -/** - * Describes the message agent.v1.RequestedModel_ModelParameterbytes. - * Use `create(RequestedModel_ModelParameterbytesSchema)` to create a new message. - */ -export declare const RequestedModel_ModelParameterbytesSchema: GenMessage; -/** - * @generated from message agent.v1.AgentRunRequest - */ -export type AgentRunRequest = Message<"agent.v1.AgentRunRequest"> & { - /** - * @generated from field: agent.v1.ConversationStateStructure conversation_state = 1; - */ - conversationState?: ConversationStateStructure; - /** - * @generated from field: agent.v1.ConversationAction action = 2; - */ - action?: ConversationAction; - /** - * TODO: Today we use model_details, but we are getting ready to deprecate that and use requested_model instead. - * - * @generated from field: agent.v1.ModelDetails model_details = 3; - */ - modelDetails?: ModelDetails; - /** - * @generated from field: optional agent.v1.RequestedModel requested_model = 9; - */ - requestedModel?: RequestedModel; - /** - * @generated from field: agent.v1.McpTools mcp_tools = 4; - */ - mcpTools?: McpTools; - /** - * @generated from field: optional string conversation_id = 5; - */ - conversationId?: string; - /** - * @generated from field: optional agent.v1.McpFileSystemOptions mcp_file_system_options = 6; - */ - mcpFileSystemOptions?: McpFileSystemOptions; - /** - * Deprecated, use the one in RequestContext message in request_context_exec.proto instead - * - * @generated from field: optional agent.v1.SkillOptions skill_options = 7; - */ - skillOptions?: SkillOptions; - /** - * Custom system prompt override. Allowlisted for specific teams only. - * - * @generated from field: optional string custom_system_prompt = 8; - */ - customSystemPrompt?: string; -}; -/** - * Describes the message agent.v1.AgentRunRequest. - * Use `create(AgentRunRequestSchema)` to create a new message. - */ -export declare const AgentRunRequestSchema: GenMessage; -/** - * @generated from message agent.v1.TextDeltaUpdate - */ -export type TextDeltaUpdate = Message<"agent.v1.TextDeltaUpdate"> & { - /** - * @generated from field: string text = 1; - */ - text: string; -}; -/** - * Describes the message agent.v1.TextDeltaUpdate. - * Use `create(TextDeltaUpdateSchema)` to create a new message. - */ -export declare const TextDeltaUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.ToolCallStartedUpdate - */ -export type ToolCallStartedUpdate = Message<"agent.v1.ToolCallStartedUpdate"> & { - /** - * @generated from field: string call_id = 1; - */ - callId: string; - /** - * @generated from field: agent.v1.ToolCall tool_call = 2; - */ - toolCall?: ToolCall; - /** - * groups tool calls that originate from the same model provider call - * - * @generated from field: string model_call_id = 3; - */ - modelCallId: string; -}; -/** - * Describes the message agent.v1.ToolCallStartedUpdate. - * Use `create(ToolCallStartedUpdateSchema)` to create a new message. - */ -export declare const ToolCallStartedUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.ToolCallCompletedUpdate - */ -export type ToolCallCompletedUpdate = Message<"agent.v1.ToolCallCompletedUpdate"> & { - /** - * @generated from field: string call_id = 1; - */ - callId: string; - /** - * @generated from field: agent.v1.ToolCall tool_call = 2; - */ - toolCall?: ToolCall; - /** - * groups tool calls that originate from the same model provider call - * - * @generated from field: string model_call_id = 3; - */ - modelCallId: string; -}; -/** - * Describes the message agent.v1.ToolCallCompletedUpdate. - * Use `create(ToolCallCompletedUpdateSchema)` to create a new message. - */ -export declare const ToolCallCompletedUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.ToolCallDeltaUpdate - */ -export type ToolCallDeltaUpdate = Message<"agent.v1.ToolCallDeltaUpdate"> & { - /** - * @generated from field: string call_id = 1; - */ - callId: string; - /** - * @generated from field: agent.v1.ToolCallDelta tool_call_delta = 2; - */ - toolCallDelta?: ToolCallDelta; - /** - * groups tool calls that originate from the same model provider call - * - * @generated from field: string model_call_id = 3; - */ - modelCallId: string; -}; -/** - * Describes the message agent.v1.ToolCallDeltaUpdate. - * Use `create(ToolCallDeltaUpdateSchema)` to create a new message. - */ -export declare const ToolCallDeltaUpdateSchema: GenMessage; -/** - * Streaming update for partial tool call arguments - * - * @generated from message agent.v1.PartialToolCallUpdate - */ -export type PartialToolCallUpdate = Message<"agent.v1.PartialToolCallUpdate"> & { - /** - * @generated from field: string call_id = 1; - */ - callId: string; - /** - * @generated from field: agent.v1.ToolCall tool_call = 2; - */ - toolCall?: ToolCall; - /** - * Aggregated args text so far (as JSON text). May be incomplete until final tool call. - * - * @generated from field: string args_text_delta = 3; - */ - argsTextDelta: string; - /** - * groups tool calls that originate from the same model provider call - * - * @generated from field: string model_call_id = 4; - */ - modelCallId: string; -}; -/** - * Describes the message agent.v1.PartialToolCallUpdate. - * Use `create(PartialToolCallUpdateSchema)` to create a new message. - */ -export declare const PartialToolCallUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.ThinkingDeltaUpdate - */ -export type ThinkingDeltaUpdate = Message<"agent.v1.ThinkingDeltaUpdate"> & { - /** - * @generated from field: string text = 1; - */ - text: string; -}; -/** - * Describes the message agent.v1.ThinkingDeltaUpdate. - * Use `create(ThinkingDeltaUpdateSchema)` to create a new message. - */ -export declare const ThinkingDeltaUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.ThinkingCompletedUpdate - */ -export type ThinkingCompletedUpdate = Message<"agent.v1.ThinkingCompletedUpdate"> & { - /** - * @generated from field: int32 thinking_duration_ms = 1; - */ - thinkingDurationMs: number; -}; -/** - * Describes the message agent.v1.ThinkingCompletedUpdate. - * Use `create(ThinkingCompletedUpdateSchema)` to create a new message. - */ -export declare const ThinkingCompletedUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.TokenDeltaUpdate - */ -export type TokenDeltaUpdate = Message<"agent.v1.TokenDeltaUpdate"> & { - /** - * @generated from field: int32 tokens = 1; - */ - tokens: number; -}; -/** - * Describes the message agent.v1.TokenDeltaUpdate. - * Use `create(TokenDeltaUpdateSchema)` to create a new message. - */ -export declare const TokenDeltaUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.SummaryUpdate - */ -export type SummaryUpdate = Message<"agent.v1.SummaryUpdate"> & { - /** - * @generated from field: string summary = 1; - */ - summary: string; -}; -/** - * Describes the message agent.v1.SummaryUpdate. - * Use `create(SummaryUpdateSchema)` to create a new message. - */ -export declare const SummaryUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.SummaryStartedUpdate - */ -export type SummaryStartedUpdate = Message<"agent.v1.SummaryStartedUpdate"> & {}; -/** - * Describes the message agent.v1.SummaryStartedUpdate. - * Use `create(SummaryStartedUpdateSchema)` to create a new message. - */ -export declare const SummaryStartedUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.HeartbeatUpdate - */ -export type HeartbeatUpdate = Message<"agent.v1.HeartbeatUpdate"> & {}; -/** - * Describes the message agent.v1.HeartbeatUpdate. - * Use `create(HeartbeatUpdateSchema)` to create a new message. - */ -export declare const HeartbeatUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.SummaryCompletedUpdate - */ -export type SummaryCompletedUpdate = Message<"agent.v1.SummaryCompletedUpdate"> & {}; -/** - * Describes the message agent.v1.SummaryCompletedUpdate. - * Use `create(SummaryCompletedUpdateSchema)` to create a new message. - */ -export declare const SummaryCompletedUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.ShellOutputDeltaUpdate - */ -export type ShellOutputDeltaUpdate = Message<"agent.v1.ShellOutputDeltaUpdate"> & { - /** - * @generated from oneof agent.v1.ShellOutputDeltaUpdate.event - */ - event: { - /** - * @generated from field: agent.v1.ShellStreamStdout stdout = 1; - */ - value: ShellStreamStdout; - case: "stdout"; - } | { - /** - * @generated from field: agent.v1.ShellStreamStderr stderr = 2; - */ - value: ShellStreamStderr; - case: "stderr"; - } | { - /** - * @generated from field: agent.v1.ShellStreamExit exit = 3; - */ - value: ShellStreamExit; - case: "exit"; - } | { - /** - * @generated from field: agent.v1.ShellStreamStart start = 4; - */ - value: ShellStreamStart; - case: "start"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ShellOutputDeltaUpdate. - * Use `create(ShellOutputDeltaUpdateSchema)` to create a new message. - */ -export declare const ShellOutputDeltaUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.TurnEndedUpdate - */ -export type TurnEndedUpdate = Message<"agent.v1.TurnEndedUpdate"> & {}; -/** - * Describes the message agent.v1.TurnEndedUpdate. - * Use `create(TurnEndedUpdateSchema)` to create a new message. - */ -export declare const TurnEndedUpdateSchema: GenMessage; -/** - * Only: user message appended update - * - * @generated from message agent.v1.UserMessageAppendedUpdate - */ -export type UserMessageAppendedUpdate = Message<"agent.v1.UserMessageAppendedUpdate"> & { - /** - * @generated from field: agent.v1.UserMessage user_message = 1; - */ - userMessage?: UserMessage; -}; -/** - * Describes the message agent.v1.UserMessageAppendedUpdate. - * Use `create(UserMessageAppendedUpdateSchema)` to create a new message. - */ -export declare const UserMessageAppendedUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.StepStartedUpdate - */ -export type StepStartedUpdate = Message<"agent.v1.StepStartedUpdate"> & { - /** - * @generated from field: uint64 step_id = 1; - */ - stepId: bigint; -}; -/** - * Describes the message agent.v1.StepStartedUpdate. - * Use `create(StepStartedUpdateSchema)` to create a new message. - */ -export declare const StepStartedUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.StepCompletedUpdate - */ -export type StepCompletedUpdate = Message<"agent.v1.StepCompletedUpdate"> & { - /** - * @generated from field: uint64 step_id = 1; - */ - stepId: bigint; - /** - * @generated from field: int64 step_duration_ms = 2; - */ - stepDurationMs: bigint; -}; -/** - * Describes the message agent.v1.StepCompletedUpdate. - * Use `create(StepCompletedUpdateSchema)` to create a new message. - */ -export declare const StepCompletedUpdateSchema: GenMessage; -/** - * @generated from message agent.v1.InteractionUpdate - */ -export type InteractionUpdate = Message<"agent.v1.InteractionUpdate"> & { - /** - * @generated from oneof agent.v1.InteractionUpdate.message - */ - message: { - /** - * @generated from field: agent.v1.TextDeltaUpdate text_delta = 1; - */ - value: TextDeltaUpdate; - case: "textDelta"; - } | { - /** - * @generated from field: agent.v1.PartialToolCallUpdate partial_tool_call = 7; - */ - value: PartialToolCallUpdate; - case: "partialToolCall"; - } | { - /** - * @generated from field: agent.v1.ToolCallDeltaUpdate tool_call_delta = 15; - */ - value: ToolCallDeltaUpdate; - case: "toolCallDelta"; - } | { - /** - * @generated from field: agent.v1.ToolCallStartedUpdate tool_call_started = 2; - */ - value: ToolCallStartedUpdate; - case: "toolCallStarted"; - } | { - /** - * @generated from field: agent.v1.ToolCallCompletedUpdate tool_call_completed = 3; - */ - value: ToolCallCompletedUpdate; - case: "toolCallCompleted"; - } | { - /** - * @generated from field: agent.v1.ThinkingDeltaUpdate thinking_delta = 4; - */ - value: ThinkingDeltaUpdate; - case: "thinkingDelta"; - } | { - /** - * @generated from field: agent.v1.ThinkingCompletedUpdate thinking_completed = 5; - */ - value: ThinkingCompletedUpdate; - case: "thinkingCompleted"; - } | { - /** - * @generated from field: agent.v1.UserMessageAppendedUpdate user_message_appended = 6; - */ - value: UserMessageAppendedUpdate; - case: "userMessageAppended"; - } | { - /** - * @generated from field: agent.v1.TokenDeltaUpdate token_delta = 8; - */ - value: TokenDeltaUpdate; - case: "tokenDelta"; - } | { - /** - * @generated from field: agent.v1.SummaryUpdate summary = 9; - */ - value: SummaryUpdate; - case: "summary"; - } | { - /** - * @generated from field: agent.v1.SummaryStartedUpdate summary_started = 10; - */ - value: SummaryStartedUpdate; - case: "summaryStarted"; - } | { - /** - * @generated from field: agent.v1.SummaryCompletedUpdate summary_completed = 11; - */ - value: SummaryCompletedUpdate; - case: "summaryCompleted"; - } | { - /** - * @generated from field: agent.v1.ShellOutputDeltaUpdate shell_output_delta = 12; - */ - value: ShellOutputDeltaUpdate; - case: "shellOutputDelta"; - } | { - /** - * @generated from field: agent.v1.HeartbeatUpdate heartbeat = 13; - */ - value: HeartbeatUpdate; - case: "heartbeat"; - } | { - /** - * @generated from field: agent.v1.TurnEndedUpdate turn_ended = 14; - */ - value: TurnEndedUpdate; - case: "turnEnded"; - } | { - /** - * @generated from field: agent.v1.StepStartedUpdate step_started = 16; - */ - value: StepStartedUpdate; - case: "stepStarted"; - } | { - /** - * @generated from field: agent.v1.StepCompletedUpdate step_completed = 17; - */ - value: StepCompletedUpdate; - case: "stepCompleted"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.InteractionUpdate. - * Use `create(InteractionUpdateSchema)` to create a new message. - */ -export declare const InteractionUpdateSchema: GenMessage; -/** - * Interaction query messages for bidirectional communication - * - * @generated from message agent.v1.InteractionQuery - */ -export type InteractionQuery = Message<"agent.v1.InteractionQuery"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; - /** - * @generated from oneof agent.v1.InteractionQuery.query - */ - query: { - /** - * @generated from field: agent.v1.WebSearchRequestQuery web_search_request_query = 2; - */ - value: WebSearchRequestQuery; - case: "webSearchRequestQuery"; - } | { - /** - * @generated from field: agent.v1.AskQuestionInteractionQuery ask_question_interaction_query = 3; - */ - value: AskQuestionInteractionQuery; - case: "askQuestionInteractionQuery"; - } | { - /** - * @generated from field: agent.v1.SwitchModeRequestQuery switch_mode_request_query = 4; - */ - value: SwitchModeRequestQuery; - case: "switchModeRequestQuery"; - } | { - /** - * @generated from field: agent.v1.ExaSearchRequestQuery exa_search_request_query = 5; - */ - value: ExaSearchRequestQuery; - case: "exaSearchRequestQuery"; - } | { - /** - * @generated from field: agent.v1.ExaFetchRequestQuery exa_fetch_request_query = 6; - */ - value: ExaFetchRequestQuery; - case: "exaFetchRequestQuery"; - } | { - /** - * @generated from field: agent.v1.CreatePlanRequestQuery create_plan_request_query = 7; - */ - value: CreatePlanRequestQuery; - case: "createPlanRequestQuery"; - } | { - /** - * @generated from field: agent.v1.SetupVmEnvironmentArgs setup_vm_environment_args = 8; - */ - value: SetupVmEnvironmentArgs; - case: "setupVmEnvironmentArgs"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.InteractionQuery. - * Use `create(InteractionQuerySchema)` to create a new message. - */ -export declare const InteractionQuerySchema: GenMessage; -/** - * @generated from message agent.v1.InteractionResponse - */ -export type InteractionResponse = Message<"agent.v1.InteractionResponse"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; - /** - * @generated from oneof agent.v1.InteractionResponse.result - */ - result: { - /** - * @generated from field: agent.v1.WebSearchRequestResponse web_search_request_response = 2; - */ - value: WebSearchRequestResponse; - case: "webSearchRequestResponse"; - } | { - /** - * @generated from field: agent.v1.AskQuestionInteractionResponse ask_question_interaction_response = 3; - */ - value: AskQuestionInteractionResponse; - case: "askQuestionInteractionResponse"; - } | { - /** - * @generated from field: agent.v1.SwitchModeRequestResponse switch_mode_request_response = 4; - */ - value: SwitchModeRequestResponse; - case: "switchModeRequestResponse"; - } | { - /** - * @generated from field: agent.v1.ExaSearchRequestResponse exa_search_request_response = 5; - */ - value: ExaSearchRequestResponse; - case: "exaSearchRequestResponse"; - } | { - /** - * @generated from field: agent.v1.ExaFetchRequestResponse exa_fetch_request_response = 6; - */ - value: ExaFetchRequestResponse; - case: "exaFetchRequestResponse"; - } | { - /** - * @generated from field: agent.v1.CreatePlanRequestResponse create_plan_request_response = 7; - */ - value: CreatePlanRequestResponse; - case: "createPlanRequestResponse"; - } | { - /** - * @generated from field: agent.v1.SetupVmEnvironmentResult setup_vm_environment_result = 8; - */ - value: SetupVmEnvironmentResult; - case: "setupVmEnvironmentResult"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.InteractionResponse. - * Use `create(InteractionResponseSchema)` to create a new message. - */ -export declare const InteractionResponseSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionInteractionQuery - */ -export type AskQuestionInteractionQuery = Message<"agent.v1.AskQuestionInteractionQuery"> & { - /** - * @generated from field: agent.v1.AskQuestionArgs args = 1; - */ - args?: AskQuestionArgs; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.AskQuestionInteractionQuery. - * Use `create(AskQuestionInteractionQuerySchema)` to create a new message. - */ -export declare const AskQuestionInteractionQuerySchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionInteractionResponse - */ -export type AskQuestionInteractionResponse = Message<"agent.v1.AskQuestionInteractionResponse"> & { - /** - * @generated from field: agent.v1.AskQuestionResult result = 1; - */ - result?: AskQuestionResult; -}; -/** - * Describes the message agent.v1.AskQuestionInteractionResponse. - * Use `create(AskQuestionInteractionResponseSchema)` to create a new message. - */ -export declare const AskQuestionInteractionResponseSchema: GenMessage; -/** - * @generated from message agent.v1.ClientHeartbeat - */ -export type ClientHeartbeat = Message<"agent.v1.ClientHeartbeat"> & {}; -/** - * Describes the message agent.v1.ClientHeartbeat. - * Use `create(ClientHeartbeatSchema)` to create a new message. - */ -export declare const ClientHeartbeatSchema: GenMessage; -/** - * Prewarm request - sent before the actual action to prepare the backend Contains all config needed for auth, model routing, and session building The actual ConversationAction is sent separately after prewarming completes - * - * @generated from message agent.v1.PrewarmRequest - */ -export type PrewarmRequest = Message<"agent.v1.PrewarmRequest"> & { - /** - * @generated from field: agent.v1.ModelDetails model_details = 1; - */ - modelDetails?: ModelDetails; - /** - * @generated from field: optional agent.v1.RequestedModel requested_model = 9; - */ - requestedModel?: RequestedModel; - /** - * @generated from field: optional string conversation_id = 2; - */ - conversationId?: string; - /** - * @generated from field: agent.v1.ConversationStateStructure conversation_state = 3; - */ - conversationState?: ConversationStateStructure; - /** - * @generated from field: agent.v1.McpTools mcp_tools = 4; - */ - mcpTools?: McpTools; - /** - * @generated from field: optional agent.v1.McpFileSystemOptions mcp_file_system_options = 5; - */ - mcpFileSystemOptions?: McpFileSystemOptions; - /** - * Best-of-N context for usage billing (same fields as UserMessage) - * - * @generated from field: optional string best_of_n_group_id = 6; - */ - bestOfNGroupId?: string; - /** - * @generated from field: optional bool try_use_best_of_n_promotion = 7; - */ - tryUseBestOfNPromotion?: boolean; - /** - * Custom system prompt override. Allowlisted for specific teams only. - * - * @generated from field: optional string custom_system_prompt = 8; - */ - customSystemPrompt?: string; -}; -/** - * Describes the message agent.v1.PrewarmRequest. - * Use `create(PrewarmRequestSchema)` to create a new message. - */ -export declare const PrewarmRequestSchema: GenMessage; -/** - * @generated from message agent.v1.ExecServerAbort - */ -export type ExecServerAbort = Message<"agent.v1.ExecServerAbort"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; -}; -/** - * Describes the message agent.v1.ExecServerAbort. - * Use `create(ExecServerAbortSchema)` to create a new message. - */ -export declare const ExecServerAbortSchema: GenMessage; -/** - * @generated from message agent.v1.ExecServerControlMessage - */ -export type ExecServerControlMessage = Message<"agent.v1.ExecServerControlMessage"> & { - /** - * @generated from oneof agent.v1.ExecServerControlMessage.message - */ - message: { - /** - * @generated from field: agent.v1.ExecServerAbort abort = 1; - */ - value: ExecServerAbort; - case: "abort"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExecServerControlMessage. - * Use `create(ExecServerControlMessageSchema)` to create a new message. - */ -export declare const ExecServerControlMessageSchema: GenMessage; -/** - * @generated from message agent.v1.AgentClientMessage - */ -export type AgentClientMessage = Message<"agent.v1.AgentClientMessage"> & { - /** - * @generated from oneof agent.v1.AgentClientMessage.message - */ - message: { - /** - * @generated from field: agent.v1.AgentRunRequest run_request = 1; - */ - value: AgentRunRequest; - case: "runRequest"; - } | { - /** - * @generated from field: agent.v1.ExecClientMessage exec_client_message = 2; - */ - value: ExecClientMessage; - case: "execClientMessage"; - } | { - /** - * @generated from field: agent.v1.ExecClientControlMessage exec_client_control_message = 5; - */ - value: ExecClientControlMessage; - case: "execClientControlMessage"; - } | { - /** - * @generated from field: agent.v1.KvClientMessage kv_client_message = 3; - */ - value: KvClientMessage; - case: "kvClientMessage"; - } | { - /** - * @generated from field: agent.v1.ConversationAction conversation_action = 4; - */ - value: ConversationAction; - case: "conversationAction"; - } | { - /** - * @generated from field: agent.v1.InteractionResponse interaction_response = 6; - */ - value: InteractionResponse; - case: "interactionResponse"; - } | { - /** - * @generated from field: agent.v1.ClientHeartbeat client_heartbeat = 7; - */ - value: ClientHeartbeat; - case: "clientHeartbeat"; - } | { - /** - * @generated from field: agent.v1.PrewarmRequest prewarm_request = 8; - */ - value: PrewarmRequest; - case: "prewarmRequest"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.AgentClientMessage. - * Use `create(AgentClientMessageSchema)` to create a new message. - */ -export declare const AgentClientMessageSchema: GenMessage; -/** - * @generated from message agent.v1.AgentServerMessage - */ -export type AgentServerMessage = Message<"agent.v1.AgentServerMessage"> & { - /** - * @generated from oneof agent.v1.AgentServerMessage.message - */ - message: { - /** - * @generated from field: agent.v1.InteractionUpdate interaction_update = 1; - */ - value: InteractionUpdate; - case: "interactionUpdate"; - } | { - /** - * @generated from field: agent.v1.ExecServerMessage exec_server_message = 2; - */ - value: ExecServerMessage; - case: "execServerMessage"; - } | { - /** - * @generated from field: agent.v1.ExecServerControlMessage exec_server_control_message = 5; - */ - value: ExecServerControlMessage; - case: "execServerControlMessage"; - } | { - /** - * @generated from field: agent.v1.ConversationStateStructure conversation_checkpoint_update = 3; - */ - value: ConversationStateStructure; - case: "conversationCheckpointUpdate"; - } | { - /** - * @generated from field: agent.v1.KvServerMessage kv_server_message = 4; - */ - value: KvServerMessage; - case: "kvServerMessage"; - } | { - /** - * @generated from field: agent.v1.InteractionQuery interaction_query = 7; - */ - value: InteractionQuery; - case: "interactionQuery"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.AgentServerMessage. - * Use `create(AgentServerMessageSchema)` to create a new message. - */ -export declare const AgentServerMessageSchema: GenMessage; -/** - * New unary API for naming an agent from a user message - * - * @generated from message agent.v1.NameAgentRequest - */ -export type NameAgentRequest = Message<"agent.v1.NameAgentRequest"> & { - /** - * @generated from field: string user_message = 1; - */ - userMessage: string; -}; -/** - * Describes the message agent.v1.NameAgentRequest. - * Use `create(NameAgentRequestSchema)` to create a new message. - */ -export declare const NameAgentRequestSchema: GenMessage; -/** - * @generated from message agent.v1.NameAgentResponse - */ -export type NameAgentResponse = Message<"agent.v1.NameAgentResponse"> & { - /** - * @generated from field: string name = 1; - */ - name: string; -}; -/** - * Describes the message agent.v1.NameAgentResponse. - * Use `create(NameAgentResponseSchema)` to create a new message. - */ -export declare const NameAgentResponseSchema: GenMessage; -/** - * @generated from message agent.v1.GetUsableModelsRequest - */ -export type GetUsableModelsRequest = Message<"agent.v1.GetUsableModelsRequest"> & { - /** - * Not used right now, but can use to populate info about custom models the user passes in that we don't send down by default - * - * @generated from field: repeated string custom_model_ids = 1; - */ - customModelIds: string[]; -}; -/** - * Describes the message agent.v1.GetUsableModelsRequest. - * Use `create(GetUsableModelsRequestSchema)` to create a new message. - */ -export declare const GetUsableModelsRequestSchema: GenMessage; -/** - * @generated from message agent.v1.GetUsableModelsResponse - */ -export type GetUsableModelsResponse = Message<"agent.v1.GetUsableModelsResponse"> & { - /** - * @generated from field: repeated agent.v1.ModelDetails models = 1; - */ - models: ModelDetails[]; -}; -/** - * Describes the message agent.v1.GetUsableModelsResponse. - * Use `create(GetUsableModelsResponseSchema)` to create a new message. - */ -export declare const GetUsableModelsResponseSchema: GenMessage; -/** - * @generated from message agent.v1.GetDefaultModelForCliRequest - */ -export type GetDefaultModelForCliRequest = Message<"agent.v1.GetDefaultModelForCliRequest"> & {}; -/** - * Describes the message agent.v1.GetDefaultModelForCliRequest. - * Use `create(GetDefaultModelForCliRequestSchema)` to create a new message. - */ -export declare const GetDefaultModelForCliRequestSchema: GenMessage; -/** - * @generated from message agent.v1.GetDefaultModelForCliResponse - */ -export type GetDefaultModelForCliResponse = Message<"agent.v1.GetDefaultModelForCliResponse"> & { - /** - * @generated from field: agent.v1.ModelDetails model = 1; - */ - model?: ModelDetails; -}; -/** - * Describes the message agent.v1.GetDefaultModelForCliResponse. - * Use `create(GetDefaultModelForCliResponseSchema)` to create a new message. - */ -export declare const GetDefaultModelForCliResponseSchema: GenMessage; -/** - * Internal endpoint: returns all allowed model intents for devs - * - * @generated from message agent.v1.GetAllowedModelIntentsRequest - */ -export type GetAllowedModelIntentsRequest = Message<"agent.v1.GetAllowedModelIntentsRequest"> & {}; -/** - * Describes the message agent.v1.GetAllowedModelIntentsRequest. - * Use `create(GetAllowedModelIntentsRequestSchema)` to create a new message. - */ -export declare const GetAllowedModelIntentsRequestSchema: GenMessage; -/** - * @generated from message agent.v1.GetAllowedModelIntentsResponse - */ -export type GetAllowedModelIntentsResponse = Message<"agent.v1.GetAllowedModelIntentsResponse"> & { - /** - * @generated from field: repeated string model_intents = 1; - */ - modelIntents: string[]; -}; -/** - * Describes the message agent.v1.GetAllowedModelIntentsResponse. - * Use `create(GetAllowedModelIntentsResponseSchema)` to create a new message. - */ -export declare const GetAllowedModelIntentsResponseSchema: GenMessage; -/** - * IDE state persistence for clients (CLI / VSCode integration) Mirrors a subset of aiserver.v1.ConversationMessage.IdeEditorsState, but only contains the recently viewed files and avoids any deprecated fields. - * - * @generated from message agent.v1.IdeEditorsStateFile - */ -export type IdeEditorsStateFile = Message<"agent.v1.IdeEditorsStateFile"> & { - /** - * @generated from field: string relative_path = 1; - */ - relativePath: string; - /** - * @generated from field: string absolute_path = 2; - */ - absolutePath: string; - /** - * @generated from field: optional bool is_currently_focused = 3; - */ - isCurrentlyFocused?: boolean; - /** - * @generated from field: optional int32 current_line_number = 4; - */ - currentLineNumber?: number; - /** - * @generated from field: optional string current_line_text = 5; - */ - currentLineText?: string; - /** - * @generated from field: optional int32 line_count = 6; - */ - lineCount?: number; -}; -/** - * Describes the message agent.v1.IdeEditorsStateFile. - * Use `create(IdeEditorsStateFileSchema)` to create a new message. - */ -export declare const IdeEditorsStateFileSchema: GenMessage; -/** - * @generated from message agent.v1.IdeEditorsStateLite - */ -export type IdeEditorsStateLite = Message<"agent.v1.IdeEditorsStateLite"> & { - /** - * @generated from field: repeated agent.v1.IdeEditorsStateFile recently_viewed_files = 1; - */ - recentlyViewedFiles: IdeEditorsStateFile[]; -}; -/** - * Describes the message agent.v1.IdeEditorsStateLite. - * Use `create(IdeEditorsStateLiteSchema)` to create a new message. - */ -export declare const IdeEditorsStateLiteSchema: GenMessage; -/** - * @generated from message agent.v1.ApplyAgentDiffToolCall - */ -export type ApplyAgentDiffToolCall = Message<"agent.v1.ApplyAgentDiffToolCall"> & { - /** - * @generated from field: agent.v1.ApplyAgentDiffArgs args = 1; - */ - args?: ApplyAgentDiffArgs; - /** - * @generated from field: agent.v1.ApplyAgentDiffResult result = 2; - */ - result?: ApplyAgentDiffResult; -}; -/** - * Describes the message agent.v1.ApplyAgentDiffToolCall. - * Use `create(ApplyAgentDiffToolCallSchema)` to create a new message. - */ -export declare const ApplyAgentDiffToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ApplyAgentDiffArgs - */ -export type ApplyAgentDiffArgs = Message<"agent.v1.ApplyAgentDiffArgs"> & { - /** - * @generated from field: string agent_id = 1; - */ - agentId: string; -}; -/** - * Describes the message agent.v1.ApplyAgentDiffArgs. - * Use `create(ApplyAgentDiffArgsSchema)` to create a new message. - */ -export declare const ApplyAgentDiffArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ApplyAgentDiffResult - */ -export type ApplyAgentDiffResult = Message<"agent.v1.ApplyAgentDiffResult"> & { - /** - * @generated from oneof agent.v1.ApplyAgentDiffResult.result - */ - result: { - /** - * @generated from field: agent.v1.ApplyAgentDiffSuccess success = 1; - */ - value: ApplyAgentDiffSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ApplyAgentDiffError error = 2; - */ - value: ApplyAgentDiffError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ApplyAgentDiffResult. - * Use `create(ApplyAgentDiffResultSchema)` to create a new message. - */ -export declare const ApplyAgentDiffResultSchema: GenMessage; -/** - * @generated from message agent.v1.ApplyAgentDiffSuccess - */ -export type ApplyAgentDiffSuccess = Message<"agent.v1.ApplyAgentDiffSuccess"> & { - /** - * @generated from field: repeated agent.v1.AppliedAgentChange applied_changes = 1; - */ - appliedChanges: AppliedAgentChange[]; -}; -/** - * Describes the message agent.v1.ApplyAgentDiffSuccess. - * Use `create(ApplyAgentDiffSuccessSchema)` to create a new message. - */ -export declare const ApplyAgentDiffSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.AppliedAgentChange - */ -export type AppliedAgentChange = Message<"agent.v1.AppliedAgentChange"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: int32 change_type = 2; - */ - changeType: number; - /** - * @generated from field: optional string before_content = 3; - */ - beforeContent?: string; - /** - * @generated from field: optional string after_content = 4; - */ - afterContent?: string; - /** - * @generated from field: optional string error = 5; - */ - error?: string; - /** - * Detailed result message from the execution (e.g., "Successfully deleted file: path (123 bytes)") - * - * @generated from field: optional string message_for_model = 6; - */ - messageForModel?: string; -}; -/** - * Describes the message agent.v1.AppliedAgentChange. - * Use `create(AppliedAgentChangeSchema)` to create a new message. - */ -export declare const AppliedAgentChangeSchema: GenMessage; -/** - * @generated from message agent.v1.ApplyAgentDiffError - */ -export type ApplyAgentDiffError = Message<"agent.v1.ApplyAgentDiffError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; - /** - * @generated from field: repeated agent.v1.AppliedAgentChange applied_changes = 2; - */ - appliedChanges: AppliedAgentChange[]; -}; -/** - * Describes the message agent.v1.ApplyAgentDiffError. - * Use `create(ApplyAgentDiffErrorSchema)` to create a new message. - */ -export declare const ApplyAgentDiffErrorSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionToolCall - */ -export type AskQuestionToolCall = Message<"agent.v1.AskQuestionToolCall"> & { - /** - * @generated from field: agent.v1.AskQuestionArgs args = 1; - */ - args?: AskQuestionArgs; - /** - * @generated from field: agent.v1.AskQuestionResult result = 2; - */ - result?: AskQuestionResult; -}; -/** - * Describes the message agent.v1.AskQuestionToolCall. - * Use `create(AskQuestionToolCallSchema)` to create a new message. - */ -export declare const AskQuestionToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionArgs - */ -export type AskQuestionArgs = Message<"agent.v1.AskQuestionArgs"> & { - /** - * optional form title - * - * @generated from field: string title = 1; - */ - title: string; - /** - * 1+ questions - * - * @generated from field: repeated agent.v1.AskQuestionArgs_Question questions = 2; - */ - questions: AskQuestionArgs_Question[]; - /** - * if true, return immediately with async marker instead of blocking - * - * @generated from field: bool run_async = 5; - */ - runAsync: boolean; - /** - * if set, indicates this is a synthetic completion for the original async tool call with this ID - * - * @generated from field: string async_original_tool_call_id = 6; - */ - asyncOriginalToolCallId: string; -}; -/** - * Describes the message agent.v1.AskQuestionArgs. - * Use `create(AskQuestionArgsSchema)` to create a new message. - */ -export declare const AskQuestionArgsSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionArgs_Question - */ -export type AskQuestionArgs_Question = Message<"agent.v1.AskQuestionArgs_Question"> & { - /** - * unique, model-provided - * - * @generated from field: string id = 1; - */ - id: string; - /** - * the question text - * - * @generated from field: string prompt = 2; - */ - prompt: string; - /** - * choices - * - * @generated from field: repeated agent.v1.AskQuestionArgs_Option options = 3; - */ - options: AskQuestionArgs_Option[]; - /** - * multi-select vs single-select - * - * @generated from field: bool allow_multiple = 4; - */ - allowMultiple: boolean; -}; -/** - * Describes the message agent.v1.AskQuestionArgs_Question. - * Use `create(AskQuestionArgs_QuestionSchema)` to create a new message. - */ -export declare const AskQuestionArgs_QuestionSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionArgs_Option - */ -export type AskQuestionArgs_Option = Message<"agent.v1.AskQuestionArgs_Option"> & { - /** - * stable option id - * - * @generated from field: string id = 1; - */ - id: string; - /** - * display text - * - * @generated from field: string label = 2; - */ - label: string; -}; -/** - * Describes the message agent.v1.AskQuestionArgs_Option. - * Use `create(AskQuestionArgs_OptionSchema)` to create a new message. - */ -export declare const AskQuestionArgs_OptionSchema: GenMessage; -/** - * Marker indicating that questions have been sent asynchronously Answers will arrive later as a separate ask_question tool call - * - * @generated from message agent.v1.AskQuestionAsync - */ -export type AskQuestionAsync = Message<"agent.v1.AskQuestionAsync"> & {}; -/** - * Describes the message agent.v1.AskQuestionAsync. - * Use `create(AskQuestionAsyncSchema)` to create a new message. - */ -export declare const AskQuestionAsyncSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionResult - */ -export type AskQuestionResult = Message<"agent.v1.AskQuestionResult"> & { - /** - * @generated from oneof agent.v1.AskQuestionResult.result - */ - result: { - /** - * @generated from field: agent.v1.AskQuestionSuccess success = 1; - */ - value: AskQuestionSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.AskQuestionError error = 2; - */ - value: AskQuestionError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.AskQuestionRejected rejected = 3; - */ - value: AskQuestionRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.AskQuestionAsync async = 4; - */ - value: AskQuestionAsync; - case: "async"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.AskQuestionResult. - * Use `create(AskQuestionResultSchema)` to create a new message. - */ -export declare const AskQuestionResultSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionSuccess - */ -export type AskQuestionSuccess = Message<"agent.v1.AskQuestionSuccess"> & { - /** - * @generated from field: repeated agent.v1.AskQuestionSuccess_Answer answers = 1; - */ - answers: AskQuestionSuccess_Answer[]; -}; -/** - * Describes the message agent.v1.AskQuestionSuccess. - * Use `create(AskQuestionSuccessSchema)` to create a new message. - */ -export declare const AskQuestionSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionSuccess_Answer - */ -export type AskQuestionSuccess_Answer = Message<"agent.v1.AskQuestionSuccess_Answer"> & { - /** - * @generated from field: string question_id = 1; - */ - questionId: string; - /** - * empty if unanswered - * - * @generated from field: repeated string selected_option_ids = 2; - */ - selectedOptionIds: string[]; -}; -/** - * Describes the message agent.v1.AskQuestionSuccess_Answer. - * Use `create(AskQuestionSuccess_AnswerSchema)` to create a new message. - */ -export declare const AskQuestionSuccess_AnswerSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionError - */ -export type AskQuestionError = Message<"agent.v1.AskQuestionError"> & { - /** - * @generated from field: string error_message = 1; - */ - errorMessage: string; -}; -/** - * Describes the message agent.v1.AskQuestionError. - * Use `create(AskQuestionErrorSchema)` to create a new message. - */ -export declare const AskQuestionErrorSchema: GenMessage; -/** - * @generated from message agent.v1.AskQuestionRejected - */ -export type AskQuestionRejected = Message<"agent.v1.AskQuestionRejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.AskQuestionRejected. - * Use `create(AskQuestionRejectedSchema)` to create a new message. - */ -export declare const AskQuestionRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.BackgroundShellSpawnArgs - */ -export type BackgroundShellSpawnArgs = Message<"agent.v1.BackgroundShellSpawnArgs"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: string tool_call_id = 3; - */ - toolCallId: string; - /** - * @generated from field: agent.v1.ShellCommandParsingResult parsing_result = 4; - */ - parsingResult?: ShellCommandParsingResult; - /** - * @generated from field: optional agent.v1.SandboxPolicy sandbox_policy = 5; - */ - sandboxPolicy?: SandboxPolicy; - /** - * @generated from field: bool enable_write_shell_stdin_tool = 6; - */ - enableWriteShellStdinTool: boolean; -}; -/** - * Describes the message agent.v1.BackgroundShellSpawnArgs. - * Use `create(BackgroundShellSpawnArgsSchema)` to create a new message. - */ -export declare const BackgroundShellSpawnArgsSchema: GenMessage; -/** - * Result of spawning a background shell - * - * @generated from message agent.v1.BackgroundShellSpawnResult - */ -export type BackgroundShellSpawnResult = Message<"agent.v1.BackgroundShellSpawnResult"> & { - /** - * @generated from oneof agent.v1.BackgroundShellSpawnResult.result - */ - result: { - /** - * @generated from field: agent.v1.BackgroundShellSpawnSuccess success = 1; - */ - value: BackgroundShellSpawnSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.BackgroundShellSpawnError error = 2; - */ - value: BackgroundShellSpawnError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.ShellRejected rejected = 3; - */ - value: ShellRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.ShellPermissionDenied permission_denied = 4; - */ - value: ShellPermissionDenied; - case: "permissionDenied"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.BackgroundShellSpawnResult. - * Use `create(BackgroundShellSpawnResultSchema)` to create a new message. - */ -export declare const BackgroundShellSpawnResultSchema: GenMessage; -/** - * @generated from message agent.v1.BackgroundShellSpawnSuccess - */ -export type BackgroundShellSpawnSuccess = Message<"agent.v1.BackgroundShellSpawnSuccess"> & { - /** - * @generated from field: uint32 shell_id = 1; - */ - shellId: number; - /** - * @generated from field: string command = 2; - */ - command: string; - /** - * @generated from field: string working_directory = 3; - */ - workingDirectory: string; - /** - * Process ID of the spawned shell - * - * @generated from field: optional uint32 pid = 4; - */ - pid?: number; -}; -/** - * Describes the message agent.v1.BackgroundShellSpawnSuccess. - * Use `create(BackgroundShellSpawnSuccessSchema)` to create a new message. - */ -export declare const BackgroundShellSpawnSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.BackgroundShellSpawnError - */ -export type BackgroundShellSpawnError = Message<"agent.v1.BackgroundShellSpawnError"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: string error = 3; - */ - error: string; -}; -/** - * Describes the message agent.v1.BackgroundShellSpawnError. - * Use `create(BackgroundShellSpawnErrorSchema)` to create a new message. - */ -export declare const BackgroundShellSpawnErrorSchema: GenMessage; -/** - * @generated from message agent.v1.WriteShellStdinArgs - */ -export type WriteShellStdinArgs = Message<"agent.v1.WriteShellStdinArgs"> & { - /** - * @generated from field: uint32 shell_id = 1; - */ - shellId: number; - /** - * @generated from field: string chars = 2; - */ - chars: string; -}; -/** - * Describes the message agent.v1.WriteShellStdinArgs. - * Use `create(WriteShellStdinArgsSchema)` to create a new message. - */ -export declare const WriteShellStdinArgsSchema: GenMessage; -/** - * @generated from message agent.v1.WriteShellStdinResult - */ -export type WriteShellStdinResult = Message<"agent.v1.WriteShellStdinResult"> & { - /** - * @generated from oneof agent.v1.WriteShellStdinResult.result - */ - result: { - /** - * @generated from field: agent.v1.WriteShellStdinSuccess success = 1; - */ - value: WriteShellStdinSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.WriteShellStdinError error = 2; - */ - value: WriteShellStdinError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.WriteShellStdinResult. - * Use `create(WriteShellStdinResultSchema)` to create a new message. - */ -export declare const WriteShellStdinResultSchema: GenMessage; -/** - * @generated from message agent.v1.WriteShellStdinSuccess - */ -export type WriteShellStdinSuccess = Message<"agent.v1.WriteShellStdinSuccess"> & { - /** - * @generated from field: uint32 shell_id = 1; - */ - shellId: number; - /** - * @generated from field: uint32 terminal_file_length_before_input_written = 2; - */ - terminalFileLengthBeforeInputWritten: number; -}; -/** - * Describes the message agent.v1.WriteShellStdinSuccess. - * Use `create(WriteShellStdinSuccessSchema)` to create a new message. - */ -export declare const WriteShellStdinSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.WriteShellStdinError - */ -export type WriteShellStdinError = Message<"agent.v1.WriteShellStdinError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.WriteShellStdinError. - * Use `create(WriteShellStdinErrorSchema)` to create a new message. - */ -export declare const WriteShellStdinErrorSchema: GenMessage; -/** - * @generated from message agent.v1.Coordinate - */ -export type Coordinate = Message<"agent.v1.Coordinate"> & { - /** - * @generated from field: int32 x = 1; - */ - x: number; - /** - * @generated from field: int32 y = 2; - */ - y: number; -}; -/** - * Describes the message agent.v1.Coordinate. - * Use `create(CoordinateSchema)` to create a new message. - */ -export declare const CoordinateSchema: GenMessage; -/** - * Arguments for the computer-use tool - * - * @generated from message agent.v1.ComputerUseArgs - */ -export type ComputerUseArgs = Message<"agent.v1.ComputerUseArgs"> & { - /** - * @generated from field: string tool_call_id = 1; - */ - toolCallId: string; - /** - * @generated from field: repeated agent.v1.ComputerUseAction actions = 2; - */ - actions: ComputerUseAction[]; -}; -/** - * Describes the message agent.v1.ComputerUseArgs. - * Use `create(ComputerUseArgsSchema)` to create a new message. - */ -export declare const ComputerUseArgsSchema: GenMessage; -/** - * A single computer-use action. This is our internal canonical representation. Provider-specific formats are converted to this by adapters. - * - * @generated from message agent.v1.ComputerUseAction - */ -export type ComputerUseAction = Message<"agent.v1.ComputerUseAction"> & { - /** - * @generated from oneof agent.v1.ComputerUseAction.action - */ - action: { - /** - * @generated from field: agent.v1.MouseMoveAction mouse_move = 1; - */ - value: MouseMoveAction; - case: "mouseMove"; - } | { - /** - * @generated from field: agent.v1.ClickAction click = 2; - */ - value: ClickAction; - case: "click"; - } | { - /** - * @generated from field: agent.v1.MouseDownAction mouse_down = 3; - */ - value: MouseDownAction; - case: "mouseDown"; - } | { - /** - * @generated from field: agent.v1.MouseUpAction mouse_up = 4; - */ - value: MouseUpAction; - case: "mouseUp"; - } | { - /** - * @generated from field: agent.v1.DragAction drag = 5; - */ - value: DragAction; - case: "drag"; - } | { - /** - * @generated from field: agent.v1.ScrollAction scroll = 6; - */ - value: ScrollAction; - case: "scroll"; - } | { - /** - * @generated from field: agent.v1.TypeAction type = 7; - */ - value: TypeAction; - case: "type"; - } | { - /** - * @generated from field: agent.v1.KeyAction key = 8; - */ - value: KeyAction; - case: "key"; - } | { - /** - * @generated from field: agent.v1.WaitAction wait = 9; - */ - value: WaitAction; - case: "wait"; - } | { - /** - * @generated from field: agent.v1.ScreenshotAction screenshot = 10; - */ - value: ScreenshotAction; - case: "screenshot"; - } | { - /** - * @generated from field: agent.v1.CursorPositionAction cursor_position = 11; - */ - value: CursorPositionAction; - case: "cursorPosition"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ComputerUseAction. - * Use `create(ComputerUseActionSchema)` to create a new message. - */ -export declare const ComputerUseActionSchema: GenMessage; -/** - * Move mouse to coordinate (required) - * - * @generated from message agent.v1.MouseMoveAction - */ -export type MouseMoveAction = Message<"agent.v1.MouseMoveAction"> & { - /** - * @generated from field: agent.v1.Coordinate coordinate = 1; - */ - coordinate?: Coordinate; -}; -/** - * Describes the message agent.v1.MouseMoveAction. - * Use `create(MouseMoveActionSchema)` to create a new message. - */ -export declare const MouseMoveActionSchema: GenMessage; -/** - * Unified click action - coordinate: optional, clicks at current cursor if omitted - button: which mouse button (default: LEFT) - count: click count (1=single, 2=double, 3=triple, default: 1) - modifier_keys: optional, held during click (e.g., "ctrl", "shift", "ctrl+shift") - * - * @generated from message agent.v1.ClickAction - */ -export type ClickAction = Message<"agent.v1.ClickAction"> & { - /** - * @generated from field: optional agent.v1.Coordinate coordinate = 1; - */ - coordinate?: Coordinate; - /** - * @generated from field: int32 button = 2; - */ - button: number; - /** - * @generated from field: int32 count = 3; - */ - count: number; - /** - * @generated from field: optional string modifier_keys = 4; - */ - modifierKeys?: string; -}; -/** - * Describes the message agent.v1.ClickAction. - * Use `create(ClickActionSchema)` to create a new message. - */ -export declare const ClickActionSchema: GenMessage; -/** - * Press mouse button down (for fine-grained drag control) - * - * @generated from message agent.v1.MouseDownAction - */ -export type MouseDownAction = Message<"agent.v1.MouseDownAction"> & { - /** - * @generated from field: int32 button = 1; - */ - button: number; -}; -/** - * Describes the message agent.v1.MouseDownAction. - * Use `create(MouseDownActionSchema)` to create a new message. - */ -export declare const MouseDownActionSchema: GenMessage; -/** - * Release mouse button (for fine-grained drag control) - * - * @generated from message agent.v1.MouseUpAction - */ -export type MouseUpAction = Message<"agent.v1.MouseUpAction"> & { - /** - * @generated from field: int32 button = 1; - */ - button: number; -}; -/** - * Describes the message agent.v1.MouseUpAction. - * Use `create(MouseUpActionSchema)` to create a new message. - */ -export declare const MouseUpActionSchema: GenMessage; -/** - * Drag action - path of coordinates (at least 2 points: [start, ..., end]) - * - * @generated from message agent.v1.DragAction - */ -export type DragAction = Message<"agent.v1.DragAction"> & { - /** - * @generated from field: repeated agent.v1.Coordinate path = 1; - */ - path: Coordinate[]; - /** - * @generated from field: int32 button = 2; - */ - button: number; -}; -/** - * Describes the message agent.v1.DragAction. - * Use `create(DragActionSchema)` to create a new message. - */ -export declare const DragActionSchema: GenMessage; -/** - * Scroll action - coordinate: optional, scrolls at current cursor if omitted - direction: scroll direction (required) - amount: number of scroll "clicks" (default: 3) - modifier_keys: optional, held during scroll (e.g., "ctrl" for zoom) - * - * @generated from message agent.v1.ScrollAction - */ -export type ScrollAction = Message<"agent.v1.ScrollAction"> & { - /** - * @generated from field: optional agent.v1.Coordinate coordinate = 1; - */ - coordinate?: Coordinate; - /** - * @generated from field: int32 direction = 2; - */ - direction: number; - /** - * @generated from field: int32 amount = 3; - */ - amount: number; - /** - * @generated from field: optional string modifier_keys = 4; - */ - modifierKeys?: string; -}; -/** - * Describes the message agent.v1.ScrollAction. - * Use `create(ScrollActionSchema)` to create a new message. - */ -export declare const ScrollActionSchema: GenMessage; -/** - * Type text - * - * @generated from message agent.v1.TypeAction - */ -export type TypeAction = Message<"agent.v1.TypeAction"> & { - /** - * @generated from field: string text = 1; - */ - text: string; -}; -/** - * Describes the message agent.v1.TypeAction. - * Use `create(TypeActionSchema)` to create a new message. - */ -export declare const TypeActionSchema: GenMessage; -/** - * Press key or key combination (xdotool-style: "ctrl+a", "Return", "Alt+Left") If hold_duration_ms is set, holds the key for that duration - * - * @generated from message agent.v1.KeyAction - */ -export type KeyAction = Message<"agent.v1.KeyAction"> & { - /** - * @generated from field: string key = 1; - */ - key: string; - /** - * @generated from field: optional int32 hold_duration_ms = 2; - */ - holdDurationMs?: number; -}; -/** - * Describes the message agent.v1.KeyAction. - * Use `create(KeyActionSchema)` to create a new message. - */ -export declare const KeyActionSchema: GenMessage; -/** - * Wait for a duration - * - * @generated from message agent.v1.WaitAction - */ -export type WaitAction = Message<"agent.v1.WaitAction"> & { - /** - * @generated from field: int32 duration_ms = 1; - */ - durationMs: number; -}; -/** - * Describes the message agent.v1.WaitAction. - * Use `create(WaitActionSchema)` to create a new message. - */ -export declare const WaitActionSchema: GenMessage; -/** - * Take a screenshot - * - * @generated from message agent.v1.ScreenshotAction - */ -export type ScreenshotAction = Message<"agent.v1.ScreenshotAction"> & {}; -/** - * Describes the message agent.v1.ScreenshotAction. - * Use `create(ScreenshotActionSchema)` to create a new message. - */ -export declare const ScreenshotActionSchema: GenMessage; -/** - * Get current cursor position - * - * @generated from message agent.v1.CursorPositionAction - */ -export type CursorPositionAction = Message<"agent.v1.CursorPositionAction"> & {}; -/** - * Describes the message agent.v1.CursorPositionAction. - * Use `create(CursorPositionActionSchema)` to create a new message. - */ -export declare const CursorPositionActionSchema: GenMessage; -/** - * Result of computer-use execution - * - * @generated from message agent.v1.ComputerUseResult - */ -export type ComputerUseResult = Message<"agent.v1.ComputerUseResult"> & { - /** - * @generated from oneof agent.v1.ComputerUseResult.result - */ - result: { - /** - * @generated from field: agent.v1.ComputerUseSuccess success = 1; - */ - value: ComputerUseSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ComputerUseError error = 2; - */ - value: ComputerUseError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ComputerUseResult. - * Use `create(ComputerUseResultSchema)` to create a new message. - */ -export declare const ComputerUseResultSchema: GenMessage; -/** - * @generated from message agent.v1.ComputerUseSuccess - */ -export type ComputerUseSuccess = Message<"agent.v1.ComputerUseSuccess"> & { - /** - * @generated from field: int32 action_count = 1; - */ - actionCount: number; - /** - * @generated from field: int32 duration_ms = 2; - */ - durationMs: number; - /** - * Base64 WebP at API resolution - * - * @generated from field: optional string screenshot = 3; - */ - screenshot?: string; - /** - * @generated from field: optional string log = 4; - */ - log?: string; - /** - * @generated from field: optional string screenshot_path = 5; - */ - screenshotPath?: string; - /** - * In API resolution - * - * @generated from field: optional agent.v1.Coordinate cursor_position = 6; - */ - cursorPosition?: Coordinate; -}; -/** - * Describes the message agent.v1.ComputerUseSuccess. - * Use `create(ComputerUseSuccessSchema)` to create a new message. - */ -export declare const ComputerUseSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ComputerUseError - */ -export type ComputerUseError = Message<"agent.v1.ComputerUseError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; - /** - * @generated from field: int32 action_count = 2; - */ - actionCount: number; - /** - * @generated from field: int32 duration_ms = 3; - */ - durationMs: number; - /** - * @generated from field: optional string log = 4; - */ - log?: string; - /** - * Base64 WebP of screen state at error time - * - * @generated from field: optional string screenshot = 5; - */ - screenshot?: string; - /** - * Path where screenshot was saved - * - * @generated from field: optional string screenshot_path = 6; - */ - screenshotPath?: string; -}; -/** - * Describes the message agent.v1.ComputerUseError. - * Use `create(ComputerUseErrorSchema)` to create a new message. - */ -export declare const ComputerUseErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ComputerUseToolCall - */ -export type ComputerUseToolCall = Message<"agent.v1.ComputerUseToolCall"> & { - /** - * @generated from field: agent.v1.ComputerUseArgs args = 1; - */ - args?: ComputerUseArgs; - /** - * @generated from field: agent.v1.ComputerUseResult result = 2; - */ - result?: ComputerUseResult; -}; -/** - * Describes the message agent.v1.ComputerUseToolCall. - * Use `create(ComputerUseToolCallSchema)` to create a new message. - */ -export declare const ComputerUseToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.CreatePlanToolCall - */ -export type CreatePlanToolCall = Message<"agent.v1.CreatePlanToolCall"> & { - /** - * @generated from field: agent.v1.CreatePlanArgs args = 1; - */ - args?: CreatePlanArgs; - /** - * @generated from field: agent.v1.CreatePlanResult result = 2; - */ - result?: CreatePlanResult; -}; -/** - * Describes the message agent.v1.CreatePlanToolCall. - * Use `create(CreatePlanToolCallSchema)` to create a new message. - */ -export declare const CreatePlanToolCallSchema: GenMessage; -/** - * A phase groups related todos together for project-mode plans - * - * @generated from message agent.v1.Phase - */ -export type Phase = Message<"agent.v1.Phase"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: repeated agent.v1.TodoItem todos = 2; - */ - todos: TodoItem[]; -}; -/** - * Describes the message agent.v1.Phase. - * Use `create(PhaseSchema)` to create a new message. - */ -export declare const PhaseSchema: GenMessage; -/** - * @generated from message agent.v1.CreatePlanArgs - */ -export type CreatePlanArgs = Message<"agent.v1.CreatePlanArgs"> & { - /** - * @generated from field: string plan = 1; - */ - plan: string; - /** - * @generated from field: repeated agent.v1.TodoItem todos = 2; - */ - todos: TodoItem[]; - /** - * @generated from field: string overview = 3; - */ - overview: string; - /** - * @generated from field: string name = 4; - */ - name: string; - /** - * When true, uses phases instead of flat todos (mutually exclusive) - * - * @generated from field: bool is_project = 5; - */ - isProject: boolean; - /** - * Implementation phases (only valid when is_project=true) - * - * @generated from field: repeated agent.v1.Phase phases = 6; - */ - phases: Phase[]; -}; -/** - * Describes the message agent.v1.CreatePlanArgs. - * Use `create(CreatePlanArgsSchema)` to create a new message. - */ -export declare const CreatePlanArgsSchema: GenMessage; -/** - * @generated from message agent.v1.CreatePlanResult - */ -export type CreatePlanResult = Message<"agent.v1.CreatePlanResult"> & { - /** - * URI of the plan file (returned when file_based_plan_edits is enabled) - * - * @generated from field: string plan_uri = 3; - */ - planUri: string; - /** - * @generated from oneof agent.v1.CreatePlanResult.result - */ - result: { - /** - * @generated from field: agent.v1.CreatePlanSuccess success = 1; - */ - value: CreatePlanSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.CreatePlanError error = 2; - */ - value: CreatePlanError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.CreatePlanResult. - * Use `create(CreatePlanResultSchema)` to create a new message. - */ -export declare const CreatePlanResultSchema: GenMessage; -/** - * @generated from message agent.v1.CreatePlanSuccess - */ -export type CreatePlanSuccess = Message<"agent.v1.CreatePlanSuccess"> & {}; -/** - * Describes the message agent.v1.CreatePlanSuccess. - * Use `create(CreatePlanSuccessSchema)` to create a new message. - */ -export declare const CreatePlanSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.CreatePlanError - */ -export type CreatePlanError = Message<"agent.v1.CreatePlanError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.CreatePlanError. - * Use `create(CreatePlanErrorSchema)` to create a new message. - */ -export declare const CreatePlanErrorSchema: GenMessage; -/** - * Query sent from server to client to create a plan file - * - * @generated from message agent.v1.CreatePlanRequestQuery - */ -export type CreatePlanRequestQuery = Message<"agent.v1.CreatePlanRequestQuery"> & { - /** - * @generated from field: agent.v1.CreatePlanArgs args = 1; - */ - args?: CreatePlanArgs; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.CreatePlanRequestQuery. - * Use `create(CreatePlanRequestQuerySchema)` to create a new message. - */ -export declare const CreatePlanRequestQuerySchema: GenMessage; -/** - * Response from client with the created plan URI - * - * @generated from message agent.v1.CreatePlanRequestResponse - */ -export type CreatePlanRequestResponse = Message<"agent.v1.CreatePlanRequestResponse"> & { - /** - * @generated from field: agent.v1.CreatePlanResult result = 1; - */ - result?: CreatePlanResult; -}; -/** - * Describes the message agent.v1.CreatePlanRequestResponse. - * Use `create(CreatePlanRequestResponseSchema)` to create a new message. - */ -export declare const CreatePlanRequestResponseSchema: GenMessage; -/** - * @generated from message agent.v1.CursorRuleTypeGlobal - */ -export type CursorRuleTypeGlobal = Message<"agent.v1.CursorRuleTypeGlobal"> & {}; -/** - * Describes the message agent.v1.CursorRuleTypeGlobal. - * Use `create(CursorRuleTypeGlobalSchema)` to create a new message. - */ -export declare const CursorRuleTypeGlobalSchema: GenMessage; -/** - * @generated from message agent.v1.CursorRuleTypeFileGlobs - */ -export type CursorRuleTypeFileGlobs = Message<"agent.v1.CursorRuleTypeFileGlobs"> & { - /** - * @generated from field: repeated string globs = 1; - */ - globs: string[]; -}; -/** - * Describes the message agent.v1.CursorRuleTypeFileGlobs. - * Use `create(CursorRuleTypeFileGlobsSchema)` to create a new message. - */ -export declare const CursorRuleTypeFileGlobsSchema: GenMessage; -/** - * @generated from message agent.v1.CursorRuleTypeAgentFetched - */ -export type CursorRuleTypeAgentFetched = Message<"agent.v1.CursorRuleTypeAgentFetched"> & { - /** - * @generated from field: string description = 1; - */ - description: string; -}; -/** - * Describes the message agent.v1.CursorRuleTypeAgentFetched. - * Use `create(CursorRuleTypeAgentFetchedSchema)` to create a new message. - */ -export declare const CursorRuleTypeAgentFetchedSchema: GenMessage; -/** - * @generated from message agent.v1.CursorRuleTypeManuallyAttached - */ -export type CursorRuleTypeManuallyAttached = Message<"agent.v1.CursorRuleTypeManuallyAttached"> & {}; -/** - * Describes the message agent.v1.CursorRuleTypeManuallyAttached. - * Use `create(CursorRuleTypeManuallyAttachedSchema)` to create a new message. - */ -export declare const CursorRuleTypeManuallyAttachedSchema: GenMessage; -/** - * @generated from message agent.v1.CursorRuleType - */ -export type CursorRuleType = Message<"agent.v1.CursorRuleType"> & { - /** - * @generated from oneof agent.v1.CursorRuleType.type - */ - type: { - /** - * @generated from field: agent.v1.CursorRuleTypeGlobal global = 1; - */ - value: CursorRuleTypeGlobal; - case: "global"; - } | { - /** - * @generated from field: agent.v1.CursorRuleTypeFileGlobs file_globbed = 2; - */ - value: CursorRuleTypeFileGlobs; - case: "fileGlobbed"; - } | { - /** - * @generated from field: agent.v1.CursorRuleTypeAgentFetched agent_fetched = 3; - */ - value: CursorRuleTypeAgentFetched; - case: "agentFetched"; - } | { - /** - * @generated from field: agent.v1.CursorRuleTypeManuallyAttached manually_attached = 4; - */ - value: CursorRuleTypeManuallyAttached; - case: "manuallyAttached"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.CursorRuleType. - * Use `create(CursorRuleTypeSchema)` to create a new message. - */ -export declare const CursorRuleTypeSchema: GenMessage; -/** - * @generated from message agent.v1.CursorRule - */ -export type CursorRule = Message<"agent.v1.CursorRule"> & { - /** - * absolute path to the .mdc file - * - * @generated from field: string full_path = 1; - */ - fullPath: string; - /** - * rule body, trimmed to reasonable size if needed by client - * - * @generated from field: string content = 2; - */ - content: string; - /** - * classification of rule - * - * @generated from field: agent.v1.CursorRuleType type = 3; - */ - type?: CursorRuleType; - /** - * source of the rule - * - * @generated from field: int32 source = 4; - */ - source: number; - /** - * Git remote origin URL for the repository containing this rule, if available. Normalized to host/path format (e.g., "github.com/owner/repo"). - * - * @generated from field: optional string git_remote_origin = 5; - */ - gitRemoteOrigin?: string; - /** - * @generated from field: optional string parse_error = 6; - */ - parseError?: string; -}; -/** - * Describes the message agent.v1.CursorRule. - * Use `create(CursorRuleSchema)` to create a new message. - */ -export declare const CursorRuleSchema: GenMessage; -/** - * @generated from message agent.v1.DeleteArgs - */ -export type DeleteArgs = Message<"agent.v1.DeleteArgs"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.DeleteArgs. - * Use `create(DeleteArgsSchema)` to create a new message. - */ -export declare const DeleteArgsSchema: GenMessage; -/** - * @generated from message agent.v1.DeleteResult - */ -export type DeleteResult = Message<"agent.v1.DeleteResult"> & { - /** - * @generated from oneof agent.v1.DeleteResult.result - */ - result: { - /** - * @generated from field: agent.v1.DeleteSuccess success = 1; - */ - value: DeleteSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.DeleteFileNotFound file_not_found = 2; - */ - value: DeleteFileNotFound; - case: "fileNotFound"; - } | { - /** - * @generated from field: agent.v1.DeleteNotFile not_file = 3; - */ - value: DeleteNotFile; - case: "notFile"; - } | { - /** - * @generated from field: agent.v1.DeletePermissionDenied permission_denied = 4; - */ - value: DeletePermissionDenied; - case: "permissionDenied"; - } | { - /** - * @generated from field: agent.v1.DeleteFileBusy file_busy = 5; - */ - value: DeleteFileBusy; - case: "fileBusy"; - } | { - /** - * @generated from field: agent.v1.DeleteRejected rejected = 6; - */ - value: DeleteRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.DeleteError error = 7; - */ - value: DeleteError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.DeleteResult. - * Use `create(DeleteResultSchema)` to create a new message. - */ -export declare const DeleteResultSchema: GenMessage; -/** - * @generated from message agent.v1.DeleteSuccess - */ -export type DeleteSuccess = Message<"agent.v1.DeleteSuccess"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string deleted_file = 2; - */ - deletedFile: string; - /** - * @generated from field: int64 file_size = 3; - */ - fileSize: bigint; - /** - * @generated from field: string prev_content = 4; - */ - prevContent: string; -}; -/** - * Describes the message agent.v1.DeleteSuccess. - * Use `create(DeleteSuccessSchema)` to create a new message. - */ -export declare const DeleteSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.DeleteFileNotFound - */ -export type DeleteFileNotFound = Message<"agent.v1.DeleteFileNotFound"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.DeleteFileNotFound. - * Use `create(DeleteFileNotFoundSchema)` to create a new message. - */ -export declare const DeleteFileNotFoundSchema: GenMessage; -/** - * @generated from message agent.v1.DeleteNotFile - */ -export type DeleteNotFile = Message<"agent.v1.DeleteNotFile"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * "directory" or "other" - * - * @generated from field: string actual_type = 2; - */ - actualType: string; -}; -/** - * Describes the message agent.v1.DeleteNotFile. - * Use `create(DeleteNotFileSchema)` to create a new message. - */ -export declare const DeleteNotFileSchema: GenMessage; -/** - * @generated from message agent.v1.DeletePermissionDenied - */ -export type DeletePermissionDenied = Message<"agent.v1.DeletePermissionDenied"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string client_visible_error = 2; - */ - clientVisibleError: string; - /** - * @generated from field: bool is_readonly = 3; - */ - isReadonly: boolean; -}; -/** - * Describes the message agent.v1.DeletePermissionDenied. - * Use `create(DeletePermissionDeniedSchema)` to create a new message. - */ -export declare const DeletePermissionDeniedSchema: GenMessage; -/** - * @generated from message agent.v1.DeleteFileBusy - */ -export type DeleteFileBusy = Message<"agent.v1.DeleteFileBusy"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.DeleteFileBusy. - * Use `create(DeleteFileBusySchema)` to create a new message. - */ -export declare const DeleteFileBusySchema: GenMessage; -/** - * @generated from message agent.v1.DeleteRejected - */ -export type DeleteRejected = Message<"agent.v1.DeleteRejected"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string reason = 2; - */ - reason: string; -}; -/** - * Describes the message agent.v1.DeleteRejected. - * Use `create(DeleteRejectedSchema)` to create a new message. - */ -export declare const DeleteRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.DeleteError - */ -export type DeleteError = Message<"agent.v1.DeleteError"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string error = 2; - */ - error: string; -}; -/** - * Describes the message agent.v1.DeleteError. - * Use `create(DeleteErrorSchema)` to create a new message. - */ -export declare const DeleteErrorSchema: GenMessage; -/** - * @generated from message agent.v1.DeleteToolCall - */ -export type DeleteToolCall = Message<"agent.v1.DeleteToolCall"> & { - /** - * @generated from field: agent.v1.DeleteArgs args = 1; - */ - args?: DeleteArgs; - /** - * @generated from field: agent.v1.DeleteResult result = 2; - */ - result?: DeleteResult; -}; -/** - * Describes the message agent.v1.DeleteToolCall. - * Use `create(DeleteToolCallSchema)` to create a new message. - */ -export declare const DeleteToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticsArgs - */ -export type DiagnosticsArgs = Message<"agent.v1.DiagnosticsArgs"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.DiagnosticsArgs. - * Use `create(DiagnosticsArgsSchema)` to create a new message. - */ -export declare const DiagnosticsArgsSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticsResult - */ -export type DiagnosticsResult = Message<"agent.v1.DiagnosticsResult"> & { - /** - * @generated from oneof agent.v1.DiagnosticsResult.result - */ - result: { - /** - * @generated from field: agent.v1.DiagnosticsSuccess success = 1; - */ - value: DiagnosticsSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.DiagnosticsError error = 2; - */ - value: DiagnosticsError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.DiagnosticsRejected rejected = 3; - */ - value: DiagnosticsRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.DiagnosticsFileNotFound file_not_found = 4; - */ - value: DiagnosticsFileNotFound; - case: "fileNotFound"; - } | { - /** - * @generated from field: agent.v1.DiagnosticsPermissionDenied permission_denied = 5; - */ - value: DiagnosticsPermissionDenied; - case: "permissionDenied"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.DiagnosticsResult. - * Use `create(DiagnosticsResultSchema)` to create a new message. - */ -export declare const DiagnosticsResultSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticsSuccess - */ -export type DiagnosticsSuccess = Message<"agent.v1.DiagnosticsSuccess"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: repeated agent.v1.Diagnostic diagnostics = 2; - */ - diagnostics: Diagnostic[]; - /** - * @generated from field: int32 total_diagnostics = 3; - */ - totalDiagnostics: number; -}; -/** - * Describes the message agent.v1.DiagnosticsSuccess. - * Use `create(DiagnosticsSuccessSchema)` to create a new message. - */ -export declare const DiagnosticsSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.Diagnostic - */ -export type Diagnostic = Message<"agent.v1.Diagnostic"> & { - /** - * @generated from field: int32 severity = 1; - */ - severity: number; - /** - * @generated from field: agent.v1.Range range = 2; - */ - range?: Range; - /** - * @generated from field: string message = 3; - */ - message: string; - /** - * @generated from field: string source = 4; - */ - source: string; - /** - * @generated from field: string code = 5; - */ - code: string; - /** - * @generated from field: bool is_stale = 6; - */ - isStale: boolean; -}; -/** - * Describes the message agent.v1.Diagnostic. - * Use `create(DiagnosticSchema)` to create a new message. - */ -export declare const DiagnosticSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticsError - */ -export type DiagnosticsError = Message<"agent.v1.DiagnosticsError"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string error = 2; - */ - error: string; -}; -/** - * Describes the message agent.v1.DiagnosticsError. - * Use `create(DiagnosticsErrorSchema)` to create a new message. - */ -export declare const DiagnosticsErrorSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticsRejected - */ -export type DiagnosticsRejected = Message<"agent.v1.DiagnosticsRejected"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string reason = 2; - */ - reason: string; -}; -/** - * Describes the message agent.v1.DiagnosticsRejected. - * Use `create(DiagnosticsRejectedSchema)` to create a new message. - */ -export declare const DiagnosticsRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticsFileNotFound - */ -export type DiagnosticsFileNotFound = Message<"agent.v1.DiagnosticsFileNotFound"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.DiagnosticsFileNotFound. - * Use `create(DiagnosticsFileNotFoundSchema)` to create a new message. - */ -export declare const DiagnosticsFileNotFoundSchema: GenMessage; -/** - * @generated from message agent.v1.DiagnosticsPermissionDenied - */ -export type DiagnosticsPermissionDenied = Message<"agent.v1.DiagnosticsPermissionDenied"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.DiagnosticsPermissionDenied. - * Use `create(DiagnosticsPermissionDeniedSchema)` to create a new message. - */ -export declare const DiagnosticsPermissionDeniedSchema: GenMessage; -/** - * @generated from message agent.v1.EditArgs - */ -export type EditArgs = Message<"agent.v1.EditArgs"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: optional string stream_content = 6; - */ - streamContent?: string; -}; -/** - * Describes the message agent.v1.EditArgs. - * Use `create(EditArgsSchema)` to create a new message. - */ -export declare const EditArgsSchema: GenMessage; -/** - * @generated from message agent.v1.EditResult - */ -export type EditResult = Message<"agent.v1.EditResult"> & { - /** - * @generated from oneof agent.v1.EditResult.result - */ - result: { - /** - * @generated from field: agent.v1.EditSuccess success = 1; - */ - value: EditSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.EditFileNotFound file_not_found = 2; - */ - value: EditFileNotFound; - case: "fileNotFound"; - } | { - /** - * @generated from field: agent.v1.EditReadPermissionDenied read_permission_denied = 3; - */ - value: EditReadPermissionDenied; - case: "readPermissionDenied"; - } | { - /** - * @generated from field: agent.v1.EditWritePermissionDenied write_permission_denied = 4; - */ - value: EditWritePermissionDenied; - case: "writePermissionDenied"; - } | { - /** - * @generated from field: agent.v1.EditRejected rejected = 6; - */ - value: EditRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.EditError error = 7; - */ - value: EditError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.EditResult. - * Use `create(EditResultSchema)` to create a new message. - */ -export declare const EditResultSchema: GenMessage; -/** - * @generated from message agent.v1.EditSuccess - */ -export type EditSuccess = Message<"agent.v1.EditSuccess"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: optional int32 lines_added = 3; - */ - linesAdded?: number; - /** - * @generated from field: optional int32 lines_removed = 4; - */ - linesRemoved?: number; - /** - * Concatenated chunk diff strings separated by "\n...\n" - * - * @generated from field: optional string diff_string = 5; - */ - diffString?: string; - /** - * undefined if file didn't exist before the edit - * - * @generated from field: optional string before_full_file_content = 6; - */ - beforeFullFileContent?: string; - /** - * @generated from field: string after_full_file_content = 7; - */ - afterFullFileContent: string; - /** - * Formatted message for display to model (resultForModel from EditTransformResult) - * - * @generated from field: optional string message = 8; - */ - message?: string; -}; -/** - * Describes the message agent.v1.EditSuccess. - * Use `create(EditSuccessSchema)` to create a new message. - */ -export declare const EditSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.EditFileNotFound - */ -export type EditFileNotFound = Message<"agent.v1.EditFileNotFound"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.EditFileNotFound. - * Use `create(EditFileNotFoundSchema)` to create a new message. - */ -export declare const EditFileNotFoundSchema: GenMessage; -/** - * @generated from message agent.v1.EditReadPermissionDenied - */ -export type EditReadPermissionDenied = Message<"agent.v1.EditReadPermissionDenied"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.EditReadPermissionDenied. - * Use `create(EditReadPermissionDeniedSchema)` to create a new message. - */ -export declare const EditReadPermissionDeniedSchema: GenMessage; -/** - * @generated from message agent.v1.EditWritePermissionDenied - */ -export type EditWritePermissionDenied = Message<"agent.v1.EditWritePermissionDenied"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string error = 2; - */ - error: string; - /** - * @generated from field: bool is_readonly = 3; - */ - isReadonly: boolean; -}; -/** - * Describes the message agent.v1.EditWritePermissionDenied. - * Use `create(EditWritePermissionDeniedSchema)` to create a new message. - */ -export declare const EditWritePermissionDeniedSchema: GenMessage; -/** - * @generated from message agent.v1.EditRejected - */ -export type EditRejected = Message<"agent.v1.EditRejected"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string reason = 2; - */ - reason: string; -}; -/** - * Describes the message agent.v1.EditRejected. - * Use `create(EditRejectedSchema)` to create a new message. - */ -export declare const EditRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.EditError - */ -export type EditError = Message<"agent.v1.EditError"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string error = 2; - */ - error: string; - /** - * @generated from field: optional string model_visible_error = 5; - */ - modelVisibleError?: string; -}; -/** - * Describes the message agent.v1.EditError. - * Use `create(EditErrorSchema)` to create a new message. - */ -export declare const EditErrorSchema: GenMessage; -/** - * @generated from message agent.v1.EditToolCall - */ -export type EditToolCall = Message<"agent.v1.EditToolCall"> & { - /** - * @generated from field: agent.v1.EditArgs args = 1; - */ - args?: EditArgs; - /** - * @generated from field: agent.v1.EditResult result = 2; - */ - result?: EditResult; -}; -/** - * Describes the message agent.v1.EditToolCall. - * Use `create(EditToolCallSchema)` to create a new message. - */ -export declare const EditToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.EditToolCallDelta - */ -export type EditToolCallDelta = Message<"agent.v1.EditToolCallDelta"> & { - /** - * @generated from field: string stream_content_delta = 1; - */ - streamContentDelta: string; -}; -/** - * Describes the message agent.v1.EditToolCallDelta. - * Use `create(EditToolCallDeltaSchema)` to create a new message. - */ -export declare const EditToolCallDeltaSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchArgs - */ -export type ExaFetchArgs = Message<"agent.v1.ExaFetchArgs"> & { - /** - * @generated from field: repeated string ids = 1; - */ - ids: string[]; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.ExaFetchArgs. - * Use `create(ExaFetchArgsSchema)` to create a new message. - */ -export declare const ExaFetchArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchResult - */ -export type ExaFetchResult = Message<"agent.v1.ExaFetchResult"> & { - /** - * @generated from oneof agent.v1.ExaFetchResult.result - */ - result: { - /** - * @generated from field: agent.v1.ExaFetchSuccess success = 1; - */ - value: ExaFetchSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ExaFetchError error = 2; - */ - value: ExaFetchError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.ExaFetchRejected rejected = 3; - */ - value: ExaFetchRejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExaFetchResult. - * Use `create(ExaFetchResultSchema)` to create a new message. - */ -export declare const ExaFetchResultSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchSuccess - */ -export type ExaFetchSuccess = Message<"agent.v1.ExaFetchSuccess"> & { - /** - * @generated from field: repeated agent.v1.ExaFetchContent contents = 1; - */ - contents: ExaFetchContent[]; -}; -/** - * Describes the message agent.v1.ExaFetchSuccess. - * Use `create(ExaFetchSuccessSchema)` to create a new message. - */ -export declare const ExaFetchSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchError - */ -export type ExaFetchError = Message<"agent.v1.ExaFetchError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.ExaFetchError. - * Use `create(ExaFetchErrorSchema)` to create a new message. - */ -export declare const ExaFetchErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchRejected - */ -export type ExaFetchRejected = Message<"agent.v1.ExaFetchRejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.ExaFetchRejected. - * Use `create(ExaFetchRejectedSchema)` to create a new message. - */ -export declare const ExaFetchRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchContent - */ -export type ExaFetchContent = Message<"agent.v1.ExaFetchContent"> & { - /** - * @generated from field: string title = 1; - */ - title: string; - /** - * @generated from field: string url = 2; - */ - url: string; - /** - * @generated from field: string text = 3; - */ - text: string; - /** - * @generated from field: string published_date = 4; - */ - publishedDate: string; -}; -/** - * Describes the message agent.v1.ExaFetchContent. - * Use `create(ExaFetchContentSchema)` to create a new message. - */ -export declare const ExaFetchContentSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchToolCall - */ -export type ExaFetchToolCall = Message<"agent.v1.ExaFetchToolCall"> & { - /** - * @generated from field: agent.v1.ExaFetchArgs args = 1; - */ - args?: ExaFetchArgs; - /** - * @generated from field: agent.v1.ExaFetchResult result = 2; - */ - result?: ExaFetchResult; -}; -/** - * Describes the message agent.v1.ExaFetchToolCall. - * Use `create(ExaFetchToolCallSchema)` to create a new message. - */ -export declare const ExaFetchToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchRequestQuery - */ -export type ExaFetchRequestQuery = Message<"agent.v1.ExaFetchRequestQuery"> & { - /** - * @generated from field: agent.v1.ExaFetchArgs args = 1; - */ - args?: ExaFetchArgs; -}; -/** - * Describes the message agent.v1.ExaFetchRequestQuery. - * Use `create(ExaFetchRequestQuerySchema)` to create a new message. - */ -export declare const ExaFetchRequestQuerySchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchRequestResponse - */ -export type ExaFetchRequestResponse = Message<"agent.v1.ExaFetchRequestResponse"> & { - /** - * @generated from oneof agent.v1.ExaFetchRequestResponse.result - */ - result: { - /** - * @generated from field: agent.v1.ExaFetchRequestResponse_Approved approved = 1; - */ - value: ExaFetchRequestResponse_Approved; - case: "approved"; - } | { - /** - * @generated from field: agent.v1.ExaFetchRequestResponse_Rejected rejected = 2; - */ - value: ExaFetchRequestResponse_Rejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExaFetchRequestResponse. - * Use `create(ExaFetchRequestResponseSchema)` to create a new message. - */ -export declare const ExaFetchRequestResponseSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchRequestResponse_Approved - */ -export type ExaFetchRequestResponse_Approved = Message<"agent.v1.ExaFetchRequestResponse_Approved"> & {}; -/** - * Describes the message agent.v1.ExaFetchRequestResponse_Approved. - * Use `create(ExaFetchRequestResponse_ApprovedSchema)` to create a new message. - */ -export declare const ExaFetchRequestResponse_ApprovedSchema: GenMessage; -/** - * @generated from message agent.v1.ExaFetchRequestResponse_Rejected - */ -export type ExaFetchRequestResponse_Rejected = Message<"agent.v1.ExaFetchRequestResponse_Rejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.ExaFetchRequestResponse_Rejected. - * Use `create(ExaFetchRequestResponse_RejectedSchema)` to create a new message. - */ -export declare const ExaFetchRequestResponse_RejectedSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchArgs - */ -export type ExaSearchArgs = Message<"agent.v1.ExaSearchArgs"> & { - /** - * @generated from field: string query = 1; - */ - query: string; - /** - * "auto", "neural", or "keyword" - * - * @generated from field: string type = 2; - */ - type: string; - /** - * @generated from field: int32 num_results = 3; - */ - numResults: number; - /** - * @generated from field: string tool_call_id = 4; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.ExaSearchArgs. - * Use `create(ExaSearchArgsSchema)` to create a new message. - */ -export declare const ExaSearchArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchResult - */ -export type ExaSearchResult = Message<"agent.v1.ExaSearchResult"> & { - /** - * @generated from oneof agent.v1.ExaSearchResult.result - */ - result: { - /** - * @generated from field: agent.v1.ExaSearchSuccess success = 1; - */ - value: ExaSearchSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ExaSearchError error = 2; - */ - value: ExaSearchError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.ExaSearchRejected rejected = 3; - */ - value: ExaSearchRejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExaSearchResult. - * Use `create(ExaSearchResultSchema)` to create a new message. - */ -export declare const ExaSearchResultSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchSuccess - */ -export type ExaSearchSuccess = Message<"agent.v1.ExaSearchSuccess"> & { - /** - * @generated from field: repeated agent.v1.ExaSearchReference references = 1; - */ - references: ExaSearchReference[]; -}; -/** - * Describes the message agent.v1.ExaSearchSuccess. - * Use `create(ExaSearchSuccessSchema)` to create a new message. - */ -export declare const ExaSearchSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchError - */ -export type ExaSearchError = Message<"agent.v1.ExaSearchError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.ExaSearchError. - * Use `create(ExaSearchErrorSchema)` to create a new message. - */ -export declare const ExaSearchErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchRejected - */ -export type ExaSearchRejected = Message<"agent.v1.ExaSearchRejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.ExaSearchRejected. - * Use `create(ExaSearchRejectedSchema)` to create a new message. - */ -export declare const ExaSearchRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchReference - */ -export type ExaSearchReference = Message<"agent.v1.ExaSearchReference"> & { - /** - * @generated from field: string title = 1; - */ - title: string; - /** - * @generated from field: string url = 2; - */ - url: string; - /** - * @generated from field: string text = 3; - */ - text: string; - /** - * @generated from field: string published_date = 4; - */ - publishedDate: string; -}; -/** - * Describes the message agent.v1.ExaSearchReference. - * Use `create(ExaSearchReferenceSchema)` to create a new message. - */ -export declare const ExaSearchReferenceSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchToolCall - */ -export type ExaSearchToolCall = Message<"agent.v1.ExaSearchToolCall"> & { - /** - * @generated from field: agent.v1.ExaSearchArgs args = 1; - */ - args?: ExaSearchArgs; - /** - * @generated from field: agent.v1.ExaSearchResult result = 2; - */ - result?: ExaSearchResult; -}; -/** - * Describes the message agent.v1.ExaSearchToolCall. - * Use `create(ExaSearchToolCallSchema)` to create a new message. - */ -export declare const ExaSearchToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchRequestQuery - */ -export type ExaSearchRequestQuery = Message<"agent.v1.ExaSearchRequestQuery"> & { - /** - * @generated from field: agent.v1.ExaSearchArgs args = 1; - */ - args?: ExaSearchArgs; -}; -/** - * Describes the message agent.v1.ExaSearchRequestQuery. - * Use `create(ExaSearchRequestQuerySchema)` to create a new message. - */ -export declare const ExaSearchRequestQuerySchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchRequestResponse - */ -export type ExaSearchRequestResponse = Message<"agent.v1.ExaSearchRequestResponse"> & { - /** - * @generated from oneof agent.v1.ExaSearchRequestResponse.result - */ - result: { - /** - * @generated from field: agent.v1.ExaSearchRequestResponse_Approved approved = 1; - */ - value: ExaSearchRequestResponse_Approved; - case: "approved"; - } | { - /** - * @generated from field: agent.v1.ExaSearchRequestResponse_Rejected rejected = 2; - */ - value: ExaSearchRequestResponse_Rejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExaSearchRequestResponse. - * Use `create(ExaSearchRequestResponseSchema)` to create a new message. - */ -export declare const ExaSearchRequestResponseSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchRequestResponse_Approved - */ -export type ExaSearchRequestResponse_Approved = Message<"agent.v1.ExaSearchRequestResponse_Approved"> & {}; -/** - * Describes the message agent.v1.ExaSearchRequestResponse_Approved. - * Use `create(ExaSearchRequestResponse_ApprovedSchema)` to create a new message. - */ -export declare const ExaSearchRequestResponse_ApprovedSchema: GenMessage; -/** - * @generated from message agent.v1.ExaSearchRequestResponse_Rejected - */ -export type ExaSearchRequestResponse_Rejected = Message<"agent.v1.ExaSearchRequestResponse_Rejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.ExaSearchRequestResponse_Rejected. - * Use `create(ExaSearchRequestResponse_RejectedSchema)` to create a new message. - */ -export declare const ExaSearchRequestResponse_RejectedSchema: GenMessage; -/** - * @generated from message agent.v1.ExecClientStreamClose - */ -export type ExecClientStreamClose = Message<"agent.v1.ExecClientStreamClose"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; -}; -/** - * Describes the message agent.v1.ExecClientStreamClose. - * Use `create(ExecClientStreamCloseSchema)` to create a new message. - */ -export declare const ExecClientStreamCloseSchema: GenMessage; -/** - * @generated from message agent.v1.ExecClientThrow - */ -export type ExecClientThrow = Message<"agent.v1.ExecClientThrow"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; - /** - * @generated from field: string error = 2; - */ - error: string; - /** - * @generated from field: optional string stack_trace = 3; - */ - stackTrace?: string; -}; -/** - * Describes the message agent.v1.ExecClientThrow. - * Use `create(ExecClientThrowSchema)` to create a new message. - */ -export declare const ExecClientThrowSchema: GenMessage; -/** - * @generated from message agent.v1.ExecClientHeartbeat - */ -export type ExecClientHeartbeat = Message<"agent.v1.ExecClientHeartbeat"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; -}; -/** - * Describes the message agent.v1.ExecClientHeartbeat. - * Use `create(ExecClientHeartbeatSchema)` to create a new message. - */ -export declare const ExecClientHeartbeatSchema: GenMessage; -/** - * @generated from message agent.v1.ExecClientControlMessage - */ -export type ExecClientControlMessage = Message<"agent.v1.ExecClientControlMessage"> & { - /** - * @generated from oneof agent.v1.ExecClientControlMessage.message - */ - message: { - /** - * @generated from field: agent.v1.ExecClientStreamClose stream_close = 1; - */ - value: ExecClientStreamClose; - case: "streamClose"; - } | { - /** - * @generated from field: agent.v1.ExecClientThrow throw = 2; - */ - value: ExecClientThrow; - case: "throw"; - } | { - /** - * @generated from field: agent.v1.ExecClientHeartbeat heartbeat = 3; - */ - value: ExecClientHeartbeat; - case: "heartbeat"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExecClientControlMessage. - * Use `create(ExecClientControlMessageSchema)` to create a new message. - */ -export declare const ExecClientControlMessageSchema: GenMessage; -/** - * Simplified span context for tracing exec calls - * - * @generated from message agent.v1.SpanContext - */ -export type SpanContext = Message<"agent.v1.SpanContext"> & { - /** - * Trace identifier (128-bit as hex string, same for all spans in a trace) - * - * @generated from field: string trace_id = 1; - */ - traceId: string; - /** - * Unique span identifier (64-bit as hex string) - * - * @generated from field: string span_id = 2; - */ - spanId: string; - /** - * Trace flags bit field following OTEL SPAN_FLAGS_* semantics - * - * @generated from field: optional uint32 trace_flags = 3; - */ - traceFlags?: number; - /** - * W3C trace-state header string (optional) - * - * @generated from field: optional string trace_state = 4; - */ - traceState?: string; -}; -/** - * Describes the message agent.v1.SpanContext. - * Use `create(SpanContextSchema)` to create a new message. - */ -export declare const SpanContextSchema: GenMessage; -/** - * Empty abort message for aborting running execs - * - * @generated from message agent.v1.AbortArgs - */ -export type AbortArgs = Message<"agent.v1.AbortArgs"> & {}; -/** - * Describes the message agent.v1.AbortArgs. - * Use `create(AbortArgsSchema)` to create a new message. - */ -export declare const AbortArgsSchema: GenMessage; -/** - * @generated from message agent.v1.AbortResult - */ -export type AbortResult = Message<"agent.v1.AbortResult"> & {}; -/** - * Describes the message agent.v1.AbortResult. - * Use `create(AbortResultSchema)` to create a new message. - */ -export declare const AbortResultSchema: GenMessage; -/** - * @generated from message agent.v1.ExecServerMessage - */ -export type ExecServerMessage = Message<"agent.v1.ExecServerMessage"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; - /** - * Optional exec ID for attachable executions - * - * @generated from field: string exec_id = 15; - */ - execId: string; - /** - * Optional parent span context for tracing - * - * @generated from field: optional agent.v1.SpanContext span_context = 19; - */ - spanContext?: SpanContext; - /** - * @generated from oneof agent.v1.ExecServerMessage.message - */ - message: { - /** - * @generated from field: agent.v1.ShellArgs shell_args = 2; - */ - value: ShellArgs; - case: "shellArgs"; - } | { - /** - * @generated from field: agent.v1.WriteArgs write_args = 3; - */ - value: WriteArgs; - case: "writeArgs"; - } | { - /** - * @generated from field: agent.v1.DeleteArgs delete_args = 4; - */ - value: DeleteArgs; - case: "deleteArgs"; - } | { - /** - * @generated from field: agent.v1.GrepArgs grep_args = 5; - */ - value: GrepArgs; - case: "grepArgs"; - } | { - /** - * @generated from field: agent.v1.ReadArgs read_args = 7; - */ - value: ReadArgs; - case: "readArgs"; - } | { - /** - * @generated from field: agent.v1.LsArgs ls_args = 8; - */ - value: LsArgs; - case: "lsArgs"; - } | { - /** - * @generated from field: agent.v1.DiagnosticsArgs diagnostics_args = 9; - */ - value: DiagnosticsArgs; - case: "diagnosticsArgs"; - } | { - /** - * @generated from field: agent.v1.RequestContextArgs request_context_args = 10; - */ - value: RequestContextArgs; - case: "requestContextArgs"; - } | { - /** - * @generated from field: agent.v1.McpArgs mcp_args = 11; - */ - value: McpArgs; - case: "mcpArgs"; - } | { - /** - * @generated from field: agent.v1.ShellArgs shell_stream_args = 14; - */ - value: ShellArgs; - case: "shellStreamArgs"; - } | { - /** - * @generated from field: agent.v1.BackgroundShellSpawnArgs background_shell_spawn_args = 16; - */ - value: BackgroundShellSpawnArgs; - case: "backgroundShellSpawnArgs"; - } | { - /** - * @generated from field: agent.v1.ListMcpResourcesExecArgs list_mcp_resources_exec_args = 17; - */ - value: ListMcpResourcesExecArgs; - case: "listMcpResourcesExecArgs"; - } | { - /** - * @generated from field: agent.v1.ReadMcpResourceExecArgs read_mcp_resource_exec_args = 18; - */ - value: ReadMcpResourceExecArgs; - case: "readMcpResourceExecArgs"; - } | { - /** - * @generated from field: agent.v1.FetchArgs fetch_args = 20; - */ - value: FetchArgs; - case: "fetchArgs"; - } | { - /** - * @generated from field: agent.v1.RecordScreenArgs record_screen_args = 21; - */ - value: RecordScreenArgs; - case: "recordScreenArgs"; - } | { - /** - * @generated from field: agent.v1.ComputerUseArgs computer_use_args = 22; - */ - value: ComputerUseArgs; - case: "computerUseArgs"; - } | { - /** - * @generated from field: agent.v1.WriteShellStdinArgs write_shell_stdin_args = 23; - */ - value: WriteShellStdinArgs; - case: "writeShellStdinArgs"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExecServerMessage. - * Use `create(ExecServerMessageSchema)` to create a new message. - */ -export declare const ExecServerMessageSchema: GenMessage; -/** - * @generated from message agent.v1.ExecClientMessage - */ -export type ExecClientMessage = Message<"agent.v1.ExecClientMessage"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; - /** - * Optional exec ID for attachable executions - * - * @generated from field: string exec_id = 15; - */ - execId: string; - /** - * @generated from oneof agent.v1.ExecClientMessage.message - */ - message: { - /** - * @generated from field: agent.v1.ShellResult shell_result = 2; - */ - value: ShellResult; - case: "shellResult"; - } | { - /** - * @generated from field: agent.v1.WriteResult write_result = 3; - */ - value: WriteResult; - case: "writeResult"; - } | { - /** - * @generated from field: agent.v1.DeleteResult delete_result = 4; - */ - value: DeleteResult; - case: "deleteResult"; - } | { - /** - * @generated from field: agent.v1.GrepResult grep_result = 5; - */ - value: GrepResult; - case: "grepResult"; - } | { - /** - * @generated from field: agent.v1.ReadResult read_result = 7; - */ - value: ReadResult; - case: "readResult"; - } | { - /** - * @generated from field: agent.v1.LsResult ls_result = 8; - */ - value: LsResult; - case: "lsResult"; - } | { - /** - * @generated from field: agent.v1.DiagnosticsResult diagnostics_result = 9; - */ - value: DiagnosticsResult; - case: "diagnosticsResult"; - } | { - /** - * @generated from field: agent.v1.RequestContextResult request_context_result = 10; - */ - value: RequestContextResult; - case: "requestContextResult"; - } | { - /** - * @generated from field: agent.v1.McpResult mcp_result = 11; - */ - value: McpResult; - case: "mcpResult"; - } | { - /** - * @generated from field: agent.v1.ShellStream shell_stream = 14; - */ - value: ShellStream; - case: "shellStream"; - } | { - /** - * @generated from field: agent.v1.BackgroundShellSpawnResult background_shell_spawn_result = 16; - */ - value: BackgroundShellSpawnResult; - case: "backgroundShellSpawnResult"; - } | { - /** - * @generated from field: agent.v1.ListMcpResourcesExecResult list_mcp_resources_exec_result = 17; - */ - value: ListMcpResourcesExecResult; - case: "listMcpResourcesExecResult"; - } | { - /** - * @generated from field: agent.v1.ReadMcpResourceExecResult read_mcp_resource_exec_result = 18; - */ - value: ReadMcpResourceExecResult; - case: "readMcpResourceExecResult"; - } | { - /** - * @generated from field: agent.v1.FetchResult fetch_result = 20; - */ - value: FetchResult; - case: "fetchResult"; - } | { - /** - * @generated from field: agent.v1.RecordScreenResult record_screen_result = 21; - */ - value: RecordScreenResult; - case: "recordScreenResult"; - } | { - /** - * @generated from field: agent.v1.ComputerUseResult computer_use_result = 22; - */ - value: ComputerUseResult; - case: "computerUseResult"; - } | { - /** - * @generated from field: agent.v1.WriteShellStdinResult write_shell_stdin_result = 23; - */ - value: WriteShellStdinResult; - case: "writeShellStdinResult"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExecClientMessage. - * Use `create(ExecClientMessageSchema)` to create a new message. - */ -export declare const ExecClientMessageSchema: GenMessage; -/** - * @generated from message agent.v1.FetchArgs - */ -export type FetchArgs = Message<"agent.v1.FetchArgs"> & { - /** - * @generated from field: string url = 1; - */ - url: string; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.FetchArgs. - * Use `create(FetchArgsSchema)` to create a new message. - */ -export declare const FetchArgsSchema: GenMessage; -/** - * @generated from message agent.v1.FetchResult - */ -export type FetchResult = Message<"agent.v1.FetchResult"> & { - /** - * @generated from oneof agent.v1.FetchResult.result - */ - result: { - /** - * @generated from field: agent.v1.FetchSuccess success = 1; - */ - value: FetchSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.FetchError error = 2; - */ - value: FetchError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.FetchResult. - * Use `create(FetchResultSchema)` to create a new message. - */ -export declare const FetchResultSchema: GenMessage; -/** - * @generated from message agent.v1.FetchSuccess - */ -export type FetchSuccess = Message<"agent.v1.FetchSuccess"> & { - /** - * @generated from field: string url = 1; - */ - url: string; - /** - * @generated from field: string content = 2; - */ - content: string; - /** - * @generated from field: int32 status_code = 3; - */ - statusCode: number; - /** - * @generated from field: string content_type = 4; - */ - contentType: string; -}; -/** - * Describes the message agent.v1.FetchSuccess. - * Use `create(FetchSuccessSchema)` to create a new message. - */ -export declare const FetchSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.FetchError - */ -export type FetchError = Message<"agent.v1.FetchError"> & { - /** - * @generated from field: string url = 1; - */ - url: string; - /** - * @generated from field: string error = 2; - */ - error: string; -}; -/** - * Describes the message agent.v1.FetchError. - * Use `create(FetchErrorSchema)` to create a new message. - */ -export declare const FetchErrorSchema: GenMessage; -/** - * @generated from message agent.v1.GenerateImageArgs - */ -export type GenerateImageArgs = Message<"agent.v1.GenerateImageArgs"> & { - /** - * @generated from field: string description = 1; - */ - description: string; - /** - * @generated from field: optional string file_path = 2; - */ - filePath?: string; - /** - * Optional paths to reference images to use as input for image-to-image generation - * - * @generated from field: repeated string reference_image_paths = 5; - */ - referenceImagePaths: string[]; -}; -/** - * Describes the message agent.v1.GenerateImageArgs. - * Use `create(GenerateImageArgsSchema)` to create a new message. - */ -export declare const GenerateImageArgsSchema: GenMessage; -/** - * @generated from message agent.v1.GenerateImageResult - */ -export type GenerateImageResult = Message<"agent.v1.GenerateImageResult"> & { - /** - * @generated from oneof agent.v1.GenerateImageResult.result - */ - result: { - /** - * @generated from field: agent.v1.GenerateImageSuccess success = 1; - */ - value: GenerateImageSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.GenerateImageError error = 2; - */ - value: GenerateImageError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.GenerateImageResult. - * Use `create(GenerateImageResultSchema)` to create a new message. - */ -export declare const GenerateImageResultSchema: GenMessage; -/** - * @generated from message agent.v1.GenerateImageSuccess - */ -export type GenerateImageSuccess = Message<"agent.v1.GenerateImageSuccess"> & { - /** - * Actual file path where the image was saved (e.g., /path/to/project/assets/image.png) - * - * @generated from field: string file_path = 1; - */ - filePath: string; - /** - * Base64-encoded image data - * - * @generated from field: string image_data = 2; - */ - imageData: string; -}; -/** - * Describes the message agent.v1.GenerateImageSuccess. - * Use `create(GenerateImageSuccessSchema)` to create a new message. - */ -export declare const GenerateImageSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.GenerateImageError - */ -export type GenerateImageError = Message<"agent.v1.GenerateImageError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.GenerateImageError. - * Use `create(GenerateImageErrorSchema)` to create a new message. - */ -export declare const GenerateImageErrorSchema: GenMessage; -/** - * @generated from message agent.v1.GenerateImageToolCall - */ -export type GenerateImageToolCall = Message<"agent.v1.GenerateImageToolCall"> & { - /** - * @generated from field: agent.v1.GenerateImageArgs args = 1; - */ - args?: GenerateImageArgs; - /** - * @generated from field: agent.v1.GenerateImageResult result = 2; - */ - result?: GenerateImageResult; -}; -/** - * Describes the message agent.v1.GenerateImageToolCall. - * Use `create(GenerateImageToolCallSchema)` to create a new message. - */ -export declare const GenerateImageToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.GrepArgs - */ -export type GrepArgs = Message<"agent.v1.GrepArgs"> & { - /** - * @generated from field: string pattern = 1; - */ - pattern: string; - /** - * @generated from field: optional string path = 2; - */ - path?: string; - /** - * @generated from field: optional string glob = 3; - */ - glob?: string; - /** - * "content", "files_with_matches", "count" - * - * @generated from field: optional string output_mode = 4; - */ - outputMode?: string; - /** - * @generated from field: optional int32 context_before = 5; - */ - contextBefore?: number; - /** - * @generated from field: optional int32 context_after = 6; - */ - contextAfter?: number; - /** - * @generated from field: optional int32 context = 7; - */ - context?: number; - /** - * @generated from field: optional bool case_insensitive = 8; - */ - caseInsensitive?: boolean; - /** - * --type - * - * @generated from field: optional string type = 9; - */ - type?: string; - /** - * | head -N - * - * @generated from field: optional int32 head_limit = 10; - */ - headLimit?: number; - /** - * -U --multiline-dotall - * - * @generated from field: optional bool multiline = 11; - */ - multiline?: boolean; - /** - * --sort: "none", "path", "modified", "accessed", "created" - * - * @generated from field: optional string sort = 12; - */ - sort?: string; - /** - * if false, use --sortr for reverse sort - * - * @generated from field: optional bool sort_ascending = 13; - */ - sortAscending?: boolean; - /** - * @generated from field: string tool_call_id = 14; - */ - toolCallId: string; - /** - * @generated from field: optional agent.v1.SandboxPolicy sandbox_policy = 15; - */ - sandboxPolicy?: SandboxPolicy; -}; -/** - * Describes the message agent.v1.GrepArgs. - * Use `create(GrepArgsSchema)` to create a new message. - */ -export declare const GrepArgsSchema: GenMessage; -/** - * @generated from message agent.v1.GrepResult - */ -export type GrepResult = Message<"agent.v1.GrepResult"> & { - /** - * @generated from oneof agent.v1.GrepResult.result - */ - result: { - /** - * @generated from field: agent.v1.GrepSuccess success = 1; - */ - value: GrepSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.GrepError error = 2; - */ - value: GrepError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.GrepResult. - * Use `create(GrepResultSchema)` to create a new message. - */ -export declare const GrepResultSchema: GenMessage; -/** - * @generated from message agent.v1.GrepError - */ -export type GrepError = Message<"agent.v1.GrepError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.GrepError. - * Use `create(GrepErrorSchema)` to create a new message. - */ -export declare const GrepErrorSchema: GenMessage; -/** - * @generated from message agent.v1.GrepSuccess - */ -export type GrepSuccess = Message<"agent.v1.GrepSuccess"> & { - /** - * @generated from field: string pattern = 1; - */ - pattern: string; - /** - * @generated from field: string path = 2; - */ - path: string; - /** - * "content", "files_with_matches", or "count" - * - * @generated from field: string output_mode = 3; - */ - outputMode: string; - /** - * @generated from field: map workspace_results = 4; - */ - workspaceResults: { - [key: string]: GrepUnionResult; - }; - /** - * @generated from field: optional agent.v1.GrepUnionResult active_editor_result = 5; - */ - activeEditorResult?: GrepUnionResult; -}; -/** - * Describes the message agent.v1.GrepSuccess. - * Use `create(GrepSuccessSchema)` to create a new message. - */ -export declare const GrepSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.GrepUnionResult - */ -export type GrepUnionResult = Message<"agent.v1.GrepUnionResult"> & { - /** - * @generated from oneof agent.v1.GrepUnionResult.result - */ - result: { - /** - * @generated from field: agent.v1.GrepCountResult count = 1; - */ - value: GrepCountResult; - case: "count"; - } | { - /** - * @generated from field: agent.v1.GrepFilesResult files = 2; - */ - value: GrepFilesResult; - case: "files"; - } | { - /** - * @generated from field: agent.v1.GrepContentResult content = 3; - */ - value: GrepContentResult; - case: "content"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.GrepUnionResult. - * Use `create(GrepUnionResultSchema)` to create a new message. - */ -export declare const GrepUnionResultSchema: GenMessage; -/** - * @generated from message agent.v1.GrepCountResult - */ -export type GrepCountResult = Message<"agent.v1.GrepCountResult"> & { - /** - * ordered by relevance - * - * @generated from field: repeated agent.v1.GrepFileCount counts = 1; - */ - counts: GrepFileCount[]; - /** - * The total count of files that the client found from the ripgrep call This is a lower bound if we truncated the output from the ripgrep call itself, but is accurate if client_truncated is true (but may be more than the number of files returned to the server) - * - * @generated from field: int32 total_files = 2; - */ - totalFiles: number; - /** - * The total count of matches that the client found from the ripgrep call This is a lower bound if we truncated the output from the ripgrep call itself, but is accurate if client_truncated is true (but may be more than the number of matches returned to the server) - * - * @generated from field: int32 total_matches = 3; - */ - totalMatches: number; - /** - * true if the client truncated the output sent to the server - * - * @generated from field: bool client_truncated = 4; - */ - clientTruncated: boolean; - /** - * true if we truncated the output from the ripgrep call itself - * - * @generated from field: bool ripgrep_truncated = 5; - */ - ripgrepTruncated: boolean; -}; -/** - * Describes the message agent.v1.GrepCountResult. - * Use `create(GrepCountResultSchema)` to create a new message. - */ -export declare const GrepCountResultSchema: GenMessage; -/** - * @generated from message agent.v1.GrepFileCount - */ -export type GrepFileCount = Message<"agent.v1.GrepFileCount"> & { - /** - * @generated from field: string file = 1; - */ - file: string; - /** - * @generated from field: int32 count = 2; - */ - count: number; -}; -/** - * Describes the message agent.v1.GrepFileCount. - * Use `create(GrepFileCountSchema)` to create a new message. - */ -export declare const GrepFileCountSchema: GenMessage; -/** - * @generated from message agent.v1.GrepFilesResult - */ -export type GrepFilesResult = Message<"agent.v1.GrepFilesResult"> & { - /** - * ordered by relevance - * - * @generated from field: repeated string files = 1; - */ - files: string[]; - /** - * The total count of files that the client found from the ripgrep call This is a lower bound if we truncated the output from the ripgrep call itself, but is accurate if client_truncated is true (but may be more than the number of files returned to the server) - * - * @generated from field: int32 total_files = 2; - */ - totalFiles: number; - /** - * true if the client truncated the output sent to the server - * - * @generated from field: bool client_truncated = 3; - */ - clientTruncated: boolean; - /** - * true if we truncated the output from the ripgrep call itself - * - * @generated from field: bool ripgrep_truncated = 4; - */ - ripgrepTruncated: boolean; -}; -/** - * Describes the message agent.v1.GrepFilesResult. - * Use `create(GrepFilesResultSchema)` to create a new message. - */ -export declare const GrepFilesResultSchema: GenMessage; -/** - * @generated from message agent.v1.GrepContentResult - */ -export type GrepContentResult = Message<"agent.v1.GrepContentResult"> & { - /** - * ordered by relevance - * - * @generated from field: repeated agent.v1.GrepFileMatch matches = 1; - */ - matches: GrepFileMatch[]; - /** - * The total count of lines that the client found from the ripgrep call This is a lower bound if we truncated the output from the ripgrep call itself, but is accurate if client_truncated is true (but may be more than the number of lines returned to the server) - * - * @generated from field: int32 total_lines = 2; - */ - totalLines: number; - /** - * The total count of matches that the client found from the ripgrep call This is a lower bound if we truncated the output from the ripgrep call itself, but is accurate if client_truncated is true (but may be more than the number of matches returned to the server) - * - * @generated from field: int32 total_matched_lines = 3; - */ - totalMatchedLines: number; - /** - * true if the client truncated the output sent to the server - * - * @generated from field: bool client_truncated = 4; - */ - clientTruncated: boolean; - /** - * true if we truncated the output from the ripgrep call itself - * - * @generated from field: bool ripgrep_truncated = 5; - */ - ripgrepTruncated: boolean; -}; -/** - * Describes the message agent.v1.GrepContentResult. - * Use `create(GrepContentResultSchema)` to create a new message. - */ -export declare const GrepContentResultSchema: GenMessage; -/** - * @generated from message agent.v1.GrepFileMatch - */ -export type GrepFileMatch = Message<"agent.v1.GrepFileMatch"> & { - /** - * @generated from field: string file = 1; - */ - file: string; - /** - * @generated from field: repeated agent.v1.GrepContentMatch matches = 2; - */ - matches: GrepContentMatch[]; -}; -/** - * Describes the message agent.v1.GrepFileMatch. - * Use `create(GrepFileMatchSchema)` to create a new message. - */ -export declare const GrepFileMatchSchema: GenMessage; -/** - * @generated from message agent.v1.GrepContentMatch - */ -export type GrepContentMatch = Message<"agent.v1.GrepContentMatch"> & { - /** - * @generated from field: int32 line_number = 1; - */ - lineNumber: number; - /** - * @generated from field: string content = 2; - */ - content: string; - /** - * @generated from field: bool content_truncated = 3; - */ - contentTruncated: boolean; - /** - * true for context lines (-A/B/C) - * - * @generated from field: bool is_context_line = 4; - */ - isContextLine: boolean; -}; -/** - * Describes the message agent.v1.GrepContentMatch. - * Use `create(GrepContentMatchSchema)` to create a new message. - */ -export declare const GrepContentMatchSchema: GenMessage; -/** - * @generated from message agent.v1.GrepStream - */ -export type GrepStream = Message<"agent.v1.GrepStream"> & { - /** - * @generated from field: string pattern = 1; - */ - pattern: string; -}; -/** - * Describes the message agent.v1.GrepStream. - * Use `create(GrepStreamSchema)` to create a new message. - */ -export declare const GrepStreamSchema: GenMessage; -/** - * @generated from message agent.v1.GrepToolCall - */ -export type GrepToolCall = Message<"agent.v1.GrepToolCall"> & { - /** - * @generated from field: agent.v1.GrepArgs args = 1; - */ - args?: GrepArgs; - /** - * @generated from field: agent.v1.GrepResult result = 2; - */ - result?: GrepResult; -}; -/** - * Describes the message agent.v1.GrepToolCall. - * Use `create(GrepToolCallSchema)` to create a new message. - */ -export declare const GrepToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.GetBlobArgs - */ -export type GetBlobArgs = Message<"agent.v1.GetBlobArgs"> & { - /** - * @generated from field: bytes blob_id = 1; - */ - blobId: Uint8Array; -}; -/** - * Describes the message agent.v1.GetBlobArgs. - * Use `create(GetBlobArgsSchema)` to create a new message. - */ -export declare const GetBlobArgsSchema: GenMessage; -/** - * @generated from message agent.v1.GetBlobResult - */ -export type GetBlobResult = Message<"agent.v1.GetBlobResult"> & { - /** - * @generated from field: optional bytes blob_data = 1; - */ - blobData?: Uint8Array; -}; -/** - * Describes the message agent.v1.GetBlobResult. - * Use `create(GetBlobResultSchema)` to create a new message. - */ -export declare const GetBlobResultSchema: GenMessage; -/** - * @generated from message agent.v1.SetBlobArgs - */ -export type SetBlobArgs = Message<"agent.v1.SetBlobArgs"> & { - /** - * @generated from field: bytes blob_id = 1; - */ - blobId: Uint8Array; - /** - * @generated from field: bytes blob_data = 2; - */ - blobData: Uint8Array; -}; -/** - * Describes the message agent.v1.SetBlobArgs. - * Use `create(SetBlobArgsSchema)` to create a new message. - */ -export declare const SetBlobArgsSchema: GenMessage; -/** - * @generated from message agent.v1.SetBlobResult - */ -export type SetBlobResult = Message<"agent.v1.SetBlobResult"> & { - /** - * @generated from field: optional agent.v1.Error error = 1; - */ - error?: Error; -}; -/** - * Describes the message agent.v1.SetBlobResult. - * Use `create(SetBlobResultSchema)` to create a new message. - */ -export declare const SetBlobResultSchema: GenMessage; -/** - * @generated from message agent.v1.KvServerMessage - */ -export type KvServerMessage = Message<"agent.v1.KvServerMessage"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; - /** - * Span context for distributed tracing - * - * @generated from field: optional agent.v1.SpanContext span_context = 4; - */ - spanContext?: SpanContext; - /** - * @generated from oneof agent.v1.KvServerMessage.message - */ - message: { - /** - * @generated from field: agent.v1.GetBlobArgs get_blob_args = 2; - */ - value: GetBlobArgs; - case: "getBlobArgs"; - } | { - /** - * @generated from field: agent.v1.SetBlobArgs set_blob_args = 3; - */ - value: SetBlobArgs; - case: "setBlobArgs"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.KvServerMessage. - * Use `create(KvServerMessageSchema)` to create a new message. - */ -export declare const KvServerMessageSchema: GenMessage; -/** - * @generated from message agent.v1.KvClientMessage - */ -export type KvClientMessage = Message<"agent.v1.KvClientMessage"> & { - /** - * @generated from field: uint32 id = 1; - */ - id: number; - /** - * @generated from oneof agent.v1.KvClientMessage.message - */ - message: { - /** - * @generated from field: agent.v1.GetBlobResult get_blob_result = 2; - */ - value: GetBlobResult; - case: "getBlobResult"; - } | { - /** - * @generated from field: agent.v1.SetBlobResult set_blob_result = 3; - */ - value: SetBlobResult; - case: "setBlobResult"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.KvClientMessage. - * Use `create(KvClientMessageSchema)` to create a new message. - */ -export declare const KvClientMessageSchema: GenMessage; -/** - * @generated from message agent.v1.LsArgs - */ -export type LsArgs = Message<"agent.v1.LsArgs"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: repeated string ignore = 2; - */ - ignore: string[]; - /** - * @generated from field: string tool_call_id = 3; - */ - toolCallId: string; - /** - * @generated from field: optional agent.v1.SandboxPolicy sandbox_policy = 4; - */ - sandboxPolicy?: SandboxPolicy; - /** - * defaults to 5000ms - * - * @generated from field: optional uint32 timeout_ms = 5; - */ - timeoutMs?: number; -}; -/** - * Describes the message agent.v1.LsArgs. - * Use `create(LsArgsSchema)` to create a new message. - */ -export declare const LsArgsSchema: GenMessage; -/** - * @generated from message agent.v1.LsResult - */ -export type LsResult = Message<"agent.v1.LsResult"> & { - /** - * @generated from oneof agent.v1.LsResult.result - */ - result: { - /** - * @generated from field: agent.v1.LsSuccess success = 1; - */ - value: LsSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.LsError error = 2; - */ - value: LsError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.LsRejected rejected = 3; - */ - value: LsRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.LsTimeout timeout = 4; - */ - value: LsTimeout; - case: "timeout"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.LsResult. - * Use `create(LsResultSchema)` to create a new message. - */ -export declare const LsResultSchema: GenMessage; -/** - * @generated from message agent.v1.LsSuccess - */ -export type LsSuccess = Message<"agent.v1.LsSuccess"> & { - /** - * @generated from field: agent.v1.LsDirectoryTreeNode directory_tree_root = 1; - */ - directoryTreeRoot?: LsDirectoryTreeNode; -}; -/** - * Describes the message agent.v1.LsSuccess. - * Use `create(LsSuccessSchema)` to create a new message. - */ -export declare const LsSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.LsDirectoryTreeNode - */ -export type LsDirectoryTreeNode = Message<"agent.v1.LsDirectoryTreeNode"> & { - /** - * @generated from field: string abs_path = 1; - */ - absPath: string; - /** - * @generated from field: repeated agent.v1.LsDirectoryTreeNode children_dirs = 2; - */ - childrenDirs: LsDirectoryTreeNode[]; - /** - * @generated from field: repeated agent.v1.LsDirectoryTreeNode_File children_files = 3; - */ - childrenFiles: LsDirectoryTreeNode_File[]; - /** - * Proto doesn't allow repeated fields to be optional, so in case of empty children arrays, this fields indicates if it happens: `true` - because directory really doesn't have any children `false` - because we stopped traversal before getting to its children - * - * @generated from field: bool children_were_processed = 4; - */ - childrenWereProcessed: boolean; - /** - * Count of extensions in the full sub-tree - * - * @generated from field: map full_subtree_extension_counts = 5; - */ - fullSubtreeExtensionCounts: { - [key: string]: number; - }; - /** - * @generated from field: int32 num_files = 6; - */ - numFiles: number; -}; -/** - * Describes the message agent.v1.LsDirectoryTreeNode. - * Use `create(LsDirectoryTreeNodeSchema)` to create a new message. - */ -export declare const LsDirectoryTreeNodeSchema: GenMessage; -/** - * @generated from message agent.v1.LsDirectoryTreeNode_File - */ -export type LsDirectoryTreeNode_File = Message<"agent.v1.LsDirectoryTreeNode_File"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: optional agent.v1.TerminalMetadata terminal_metadata = 2; - */ - terminalMetadata?: TerminalMetadata; -}; -/** - * Describes the message agent.v1.LsDirectoryTreeNode_File. - * Use `create(LsDirectoryTreeNode_FileSchema)` to create a new message. - */ -export declare const LsDirectoryTreeNode_FileSchema: GenMessage; -/** - * @generated from message agent.v1.LsError - */ -export type LsError = Message<"agent.v1.LsError"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string error = 2; - */ - error: string; -}; -/** - * Describes the message agent.v1.LsError. - * Use `create(LsErrorSchema)` to create a new message. - */ -export declare const LsErrorSchema: GenMessage; -/** - * @generated from message agent.v1.LsRejected - */ -export type LsRejected = Message<"agent.v1.LsRejected"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string reason = 2; - */ - reason: string; -}; -/** - * Describes the message agent.v1.LsRejected. - * Use `create(LsRejectedSchema)` to create a new message. - */ -export declare const LsRejectedSchema: GenMessage; -/** - * Returned when ls operation timed out. Contains partial results gathered before timeout. - * - * @generated from message agent.v1.LsTimeout - */ -export type LsTimeout = Message<"agent.v1.LsTimeout"> & { - /** - * @generated from field: agent.v1.LsDirectoryTreeNode directory_tree_root = 1; - */ - directoryTreeRoot?: LsDirectoryTreeNode; -}; -/** - * Describes the message agent.v1.LsTimeout. - * Use `create(LsTimeoutSchema)` to create a new message. - */ -export declare const LsTimeoutSchema: GenMessage; -/** - * @generated from message agent.v1.TerminalMetadata - */ -export type TerminalMetadata = Message<"agent.v1.TerminalMetadata"> & { - /** - * @generated from field: optional string cwd = 1; - */ - cwd?: string; - /** - * @generated from field: repeated agent.v1.TerminalMetadata_Command last_commands = 2; - */ - lastCommands: TerminalMetadata_Command[]; - /** - * @generated from field: optional int64 last_modified_ms = 3; - */ - lastModifiedMs?: bigint; - /** - * @generated from field: optional agent.v1.TerminalMetadata_Command current_command = 4; - */ - currentCommand?: TerminalMetadata_Command; -}; -/** - * Describes the message agent.v1.TerminalMetadata. - * Use `create(TerminalMetadataSchema)` to create a new message. - */ -export declare const TerminalMetadataSchema: GenMessage; -/** - * @generated from message agent.v1.TerminalMetadata_Command - */ -export type TerminalMetadata_Command = Message<"agent.v1.TerminalMetadata_Command"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: optional int32 exit_code = 2; - */ - exitCode?: number; - /** - * @generated from field: optional int64 timestamp_ms = 3; - */ - timestampMs?: bigint; - /** - * @generated from field: optional int64 duration_ms = 4; - */ - durationMs?: bigint; -}; -/** - * Describes the message agent.v1.TerminalMetadata_Command. - * Use `create(TerminalMetadata_CommandSchema)` to create a new message. - */ -export declare const TerminalMetadata_CommandSchema: GenMessage; -/** - * @generated from message agent.v1.LsToolCall - */ -export type LsToolCall = Message<"agent.v1.LsToolCall"> & { - /** - * @generated from field: agent.v1.LsArgs args = 1; - */ - args?: LsArgs; - /** - * @generated from field: agent.v1.LsResult result = 2; - */ - result?: LsResult; -}; -/** - * Describes the message agent.v1.LsToolCall. - * Use `create(LsToolCallSchema)` to create a new message. - */ -export declare const LsToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.McpArgs - */ -export type McpArgs = Message<"agent.v1.McpArgs"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: map args = 2; - */ - args: { - [key: string]: Uint8Array; - }; - /** - * @generated from field: string tool_call_id = 3; - */ - toolCallId: string; - /** - * @generated from field: string provider_identifier = 4; - */ - providerIdentifier: string; - /** - * @generated from field: string tool_name = 5; - */ - toolName: string; -}; -/** - * Describes the message agent.v1.McpArgs. - * Use `create(McpArgsSchema)` to create a new message. - */ -export declare const McpArgsSchema: GenMessage; -/** - * @generated from message agent.v1.McpResult - */ -export type McpResult = Message<"agent.v1.McpResult"> & { - /** - * @generated from oneof agent.v1.McpResult.result - */ - result: { - /** - * @generated from field: agent.v1.McpSuccess success = 1; - */ - value: McpSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.McpError error = 2; - */ - value: McpError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.McpRejected rejected = 3; - */ - value: McpRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.McpPermissionDenied permission_denied = 4; - */ - value: McpPermissionDenied; - case: "permissionDenied"; - } | { - /** - * @generated from field: agent.v1.McpToolNotFound tool_not_found = 5; - */ - value: McpToolNotFound; - case: "toolNotFound"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.McpResult. - * Use `create(McpResultSchema)` to create a new message. - */ -export declare const McpResultSchema: GenMessage; -/** - * @generated from message agent.v1.McpToolNotFound - */ -export type McpToolNotFound = Message<"agent.v1.McpToolNotFound"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: repeated string available_tools = 2; - */ - availableTools: string[]; -}; -/** - * Describes the message agent.v1.McpToolNotFound. - * Use `create(McpToolNotFoundSchema)` to create a new message. - */ -export declare const McpToolNotFoundSchema: GenMessage; -/** - * Text content item - * - * @generated from message agent.v1.McpTextContent - */ -export type McpTextContent = Message<"agent.v1.McpTextContent"> & { - /** - * @generated from field: string text = 1; - */ - text: string; - /** - * Optional file location for large outputs - * - * @generated from field: optional agent.v1.OutputLocation output_location = 2; - */ - outputLocation?: OutputLocation; -}; -/** - * Describes the message agent.v1.McpTextContent. - * Use `create(McpTextContentSchema)` to create a new message. - */ -export declare const McpTextContentSchema: GenMessage; -/** - * Image content item - * - * @generated from message agent.v1.McpImageContent - */ -export type McpImageContent = Message<"agent.v1.McpImageContent"> & { - /** - * Raw bytes of the image. In JSON, this will be base64-encoded. - * - * @generated from field: bytes data = 1; - */ - data: Uint8Array; - /** - * Optional MIME type, e.g. "image/png" - * - * @generated from field: string mime_type = 2; - */ - mimeType: string; -}; -/** - * Describes the message agent.v1.McpImageContent. - * Use `create(McpImageContentSchema)` to create a new message. - */ -export declare const McpImageContentSchema: GenMessage; -/** - * A single tool result content item: either text or image - * - * @generated from message agent.v1.McpToolResultContentItem - */ -export type McpToolResultContentItem = Message<"agent.v1.McpToolResultContentItem"> & { - /** - * @generated from oneof agent.v1.McpToolResultContentItem.content - */ - content: { - /** - * @generated from field: agent.v1.McpTextContent text = 1; - */ - value: McpTextContent; - case: "text"; - } | { - /** - * @generated from field: agent.v1.McpImageContent image = 2; - */ - value: McpImageContent; - case: "image"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.McpToolResultContentItem. - * Use `create(McpToolResultContentItemSchema)` to create a new message. - */ -export declare const McpToolResultContentItemSchema: GenMessage; -/** - * Equivalent to the requested McpToolResult TypeScript type - * - * @generated from message agent.v1.McpSuccess - */ -export type McpSuccess = Message<"agent.v1.McpSuccess"> & { - /** - * @generated from field: repeated agent.v1.McpToolResultContentItem content = 1; - */ - content: McpToolResultContentItem[]; - /** - * @generated from field: bool is_error = 2; - */ - isError: boolean; -}; -/** - * Describes the message agent.v1.McpSuccess. - * Use `create(McpSuccessSchema)` to create a new message. - */ -export declare const McpSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.McpError - */ -export type McpError = Message<"agent.v1.McpError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.McpError. - * Use `create(McpErrorSchema)` to create a new message. - */ -export declare const McpErrorSchema: GenMessage; -/** - * @generated from message agent.v1.McpRejected - */ -export type McpRejected = Message<"agent.v1.McpRejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; - /** - * @generated from field: bool is_readonly = 2; - */ - isReadonly: boolean; -}; -/** - * Describes the message agent.v1.McpRejected. - * Use `create(McpRejectedSchema)` to create a new message. - */ -export declare const McpRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.McpPermissionDenied - */ -export type McpPermissionDenied = Message<"agent.v1.McpPermissionDenied"> & { - /** - * @generated from field: string error = 1; - */ - error: string; - /** - * @generated from field: bool is_readonly = 2; - */ - isReadonly: boolean; -}; -/** - * Describes the message agent.v1.McpPermissionDenied. - * Use `create(McpPermissionDeniedSchema)` to create a new message. - */ -export declare const McpPermissionDeniedSchema: GenMessage; -/** - * List MCP resources exec args - * - * @generated from message agent.v1.ListMcpResourcesExecArgs - */ -export type ListMcpResourcesExecArgs = Message<"agent.v1.ListMcpResourcesExecArgs"> & { - /** - * Optional server name to filter resources by - * - * @generated from field: optional string server = 1; - */ - server?: string; -}; -/** - * Describes the message agent.v1.ListMcpResourcesExecArgs. - * Use `create(ListMcpResourcesExecArgsSchema)` to create a new message. - */ -export declare const ListMcpResourcesExecArgsSchema: GenMessage; -/** - * List MCP resources exec result - * - * @generated from message agent.v1.ListMcpResourcesExecResult - */ -export type ListMcpResourcesExecResult = Message<"agent.v1.ListMcpResourcesExecResult"> & { - /** - * @generated from oneof agent.v1.ListMcpResourcesExecResult.result - */ - result: { - /** - * @generated from field: agent.v1.ListMcpResourcesSuccess success = 1; - */ - value: ListMcpResourcesSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ListMcpResourcesError error = 2; - */ - value: ListMcpResourcesError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.ListMcpResourcesRejected rejected = 3; - */ - value: ListMcpResourcesRejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ListMcpResourcesExecResult. - * Use `create(ListMcpResourcesExecResultSchema)` to create a new message. - */ -export declare const ListMcpResourcesExecResultSchema: GenMessage; -/** - * @generated from message agent.v1.ListMcpResourcesExecResult_McpResource - */ -export type ListMcpResourcesExecResult_McpResource = Message<"agent.v1.ListMcpResourcesExecResult_McpResource"> & { - /** - * @generated from field: string uri = 1; - */ - uri: string; - /** - * @generated from field: optional string name = 2; - */ - name?: string; - /** - * @generated from field: optional string description = 3; - */ - description?: string; - /** - * @generated from field: optional string mime_type = 4; - */ - mimeType?: string; - /** - * Server name that provides this resource - * - * @generated from field: string server = 5; - */ - server: string; - /** - * Additional metadata - * - * @generated from field: map annotations = 6; - */ - annotations: { - [key: string]: string; - }; -}; -/** - * Describes the message agent.v1.ListMcpResourcesExecResult_McpResource. - * Use `create(ListMcpResourcesExecResult_McpResourceSchema)` to create a new message. - */ -export declare const ListMcpResourcesExecResult_McpResourceSchema: GenMessage; -/** - * @generated from message agent.v1.ListMcpResourcesSuccess - */ -export type ListMcpResourcesSuccess = Message<"agent.v1.ListMcpResourcesSuccess"> & { - /** - * @generated from field: repeated agent.v1.ListMcpResourcesExecResult_McpResource resources = 1; - */ - resources: ListMcpResourcesExecResult_McpResource[]; -}; -/** - * Describes the message agent.v1.ListMcpResourcesSuccess. - * Use `create(ListMcpResourcesSuccessSchema)` to create a new message. - */ -export declare const ListMcpResourcesSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ListMcpResourcesError - */ -export type ListMcpResourcesError = Message<"agent.v1.ListMcpResourcesError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.ListMcpResourcesError. - * Use `create(ListMcpResourcesErrorSchema)` to create a new message. - */ -export declare const ListMcpResourcesErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ListMcpResourcesRejected - */ -export type ListMcpResourcesRejected = Message<"agent.v1.ListMcpResourcesRejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.ListMcpResourcesRejected. - * Use `create(ListMcpResourcesRejectedSchema)` to create a new message. - */ -export declare const ListMcpResourcesRejectedSchema: GenMessage; -/** - * Read MCP resource exec args - * - * @generated from message agent.v1.ReadMcpResourceExecArgs - */ -export type ReadMcpResourceExecArgs = Message<"agent.v1.ReadMcpResourceExecArgs"> & { - /** - * Required server name - * - * @generated from field: string server = 1; - */ - server: string; - /** - * Required resource URI - * - * @generated from field: string uri = 2; - */ - uri: string; - /** - * Optional: when set, the resource will be downloaded to this path relative to the workspace, and the content will not be returned to the model. - * - * @generated from field: optional string download_path = 3; - */ - downloadPath?: string; -}; -/** - * Describes the message agent.v1.ReadMcpResourceExecArgs. - * Use `create(ReadMcpResourceExecArgsSchema)` to create a new message. - */ -export declare const ReadMcpResourceExecArgsSchema: GenMessage; -/** - * Read MCP resource exec result - * - * @generated from message agent.v1.ReadMcpResourceExecResult - */ -export type ReadMcpResourceExecResult = Message<"agent.v1.ReadMcpResourceExecResult"> & { - /** - * @generated from oneof agent.v1.ReadMcpResourceExecResult.result - */ - result: { - /** - * @generated from field: agent.v1.ReadMcpResourceSuccess success = 1; - */ - value: ReadMcpResourceSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ReadMcpResourceError error = 2; - */ - value: ReadMcpResourceError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.ReadMcpResourceRejected rejected = 3; - */ - value: ReadMcpResourceRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.ReadMcpResourceNotFound not_found = 4; - */ - value: ReadMcpResourceNotFound; - case: "notFound"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReadMcpResourceExecResult. - * Use `create(ReadMcpResourceExecResultSchema)` to create a new message. - */ -export declare const ReadMcpResourceExecResultSchema: GenMessage; -/** - * @generated from message agent.v1.ReadMcpResourceSuccess - */ -export type ReadMcpResourceSuccess = Message<"agent.v1.ReadMcpResourceSuccess"> & { - /** - * @generated from field: string uri = 1; - */ - uri: string; - /** - * @generated from field: optional string name = 2; - */ - name?: string; - /** - * @generated from field: optional string description = 3; - */ - description?: string; - /** - * @generated from field: optional string mime_type = 4; - */ - mimeType?: string; - /** - * Additional metadata - * - * @generated from field: map annotations = 7; - */ - annotations: { - [key: string]: string; - }; - /** - * If set, resource was downloaded to this path - * - * @generated from field: optional string download_path = 8; - */ - downloadPath?: string; - /** - * @generated from oneof agent.v1.ReadMcpResourceSuccess.content - */ - content: { - /** - * @generated from field: string text = 5; - */ - value: string; - case: "text"; - } | { - /** - * @generated from field: bytes blob = 6; - */ - value: Uint8Array; - case: "blob"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReadMcpResourceSuccess. - * Use `create(ReadMcpResourceSuccessSchema)` to create a new message. - */ -export declare const ReadMcpResourceSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ReadMcpResourceError - */ -export type ReadMcpResourceError = Message<"agent.v1.ReadMcpResourceError"> & { - /** - * @generated from field: string uri = 1; - */ - uri: string; - /** - * @generated from field: string error = 2; - */ - error: string; -}; -/** - * Describes the message agent.v1.ReadMcpResourceError. - * Use `create(ReadMcpResourceErrorSchema)` to create a new message. - */ -export declare const ReadMcpResourceErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ReadMcpResourceRejected - */ -export type ReadMcpResourceRejected = Message<"agent.v1.ReadMcpResourceRejected"> & { - /** - * @generated from field: string uri = 1; - */ - uri: string; - /** - * @generated from field: string reason = 2; - */ - reason: string; -}; -/** - * Describes the message agent.v1.ReadMcpResourceRejected. - * Use `create(ReadMcpResourceRejectedSchema)` to create a new message. - */ -export declare const ReadMcpResourceRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.ReadMcpResourceNotFound - */ -export type ReadMcpResourceNotFound = Message<"agent.v1.ReadMcpResourceNotFound"> & { - /** - * @generated from field: string uri = 1; - */ - uri: string; -}; -/** - * Describes the message agent.v1.ReadMcpResourceNotFound. - * Use `create(ReadMcpResourceNotFoundSchema)` to create a new message. - */ -export declare const ReadMcpResourceNotFoundSchema: GenMessage; -/** - * @generated from message agent.v1.McpToolDefinition - */ -export type McpToolDefinition = Message<"agent.v1.McpToolDefinition"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: string provider_identifier = 4; - */ - providerIdentifier: string; - /** - * @generated from field: string tool_name = 5; - */ - toolName: string; - /** - * @generated from field: string description = 2; - */ - description: string; - /** - * @generated from field: bytes input_schema = 3; - */ - inputSchema: Uint8Array; -}; -/** - * Describes the message agent.v1.McpToolDefinition. - * Use `create(McpToolDefinitionSchema)` to create a new message. - */ -export declare const McpToolDefinitionSchema: GenMessage; -/** - * @generated from message agent.v1.McpTools - */ -export type McpTools = Message<"agent.v1.McpTools"> & { - /** - * @generated from field: repeated agent.v1.McpToolDefinition mcp_tools = 1; - */ - mcpTools: McpToolDefinition[]; -}; -/** - * Describes the message agent.v1.McpTools. - * Use `create(McpToolsSchema)` to create a new message. - */ -export declare const McpToolsSchema: GenMessage; -/** - * Represents MCP-provided instructions from a specific server - * - * @generated from message agent.v1.McpInstructions - */ -export type McpInstructions = Message<"agent.v1.McpInstructions"> & { - /** - * @generated from field: string server_name = 1; - */ - serverName: string; - /** - * @generated from field: string instructions = 2; - */ - instructions: string; -}; -/** - * Describes the message agent.v1.McpInstructions. - * Use `create(McpInstructionsSchema)` to create a new message. - */ -export declare const McpInstructionsSchema: GenMessage; -/** - * @generated from message agent.v1.McpDescriptor - */ -export type McpDescriptor = Message<"agent.v1.McpDescriptor"> & { - /** - * Display name of the MCP server associated with this folder. - * - * @generated from field: string server_name = 1; - */ - serverName: string; - /** - * @generated from field: string server_identifier = 2; - */ - serverIdentifier: string; - /** - * Absolute folder path where MCP tool descriptor JSON files are stored. - * - * @generated from field: optional string folder_path = 3; - */ - folderPath?: string; - /** - * @generated from field: optional string server_use_instructions = 4; - */ - serverUseInstructions?: string; - /** - * @generated from field: repeated agent.v1.McpToolDescriptor tools = 5; - */ - tools: McpToolDescriptor[]; -}; -/** - * Describes the message agent.v1.McpDescriptor. - * Use `create(McpDescriptorSchema)` to create a new message. - */ -export declare const McpDescriptorSchema: GenMessage; -/** - * @generated from message agent.v1.McpToolDescriptor - */ -export type McpToolDescriptor = Message<"agent.v1.McpToolDescriptor"> & { - /** - * @generated from field: string tool_name = 1; - */ - toolName: string; - /** - * @generated from field: optional string definition_path = 2; - */ - definitionPath?: string; -}; -/** - * Describes the message agent.v1.McpToolDescriptor. - * Use `create(McpToolDescriptorSchema)` to create a new message. - */ -export declare const McpToolDescriptorSchema: GenMessage; -/** - * @generated from message agent.v1.McpFileSystemOptions - */ -export type McpFileSystemOptions = Message<"agent.v1.McpFileSystemOptions"> & { - /** - * @generated from field: bool enabled = 1; - */ - enabled: boolean; - /** - * @generated from field: string workspace_project_dir = 2; - */ - workspaceProjectDir: string; - /** - * @generated from field: repeated agent.v1.McpDescriptor mcp_descriptors = 3; - */ - mcpDescriptors: McpDescriptor[]; -}; -/** - * Describes the message agent.v1.McpFileSystemOptions. - * Use `create(McpFileSystemOptionsSchema)` to create a new message. - */ -export declare const McpFileSystemOptionsSchema: GenMessage; -/** - * @generated from message agent.v1.ReadArgs - */ -export type ReadArgs = Message<"agent.v1.ReadArgs"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.ReadArgs. - * Use `create(ReadArgsSchema)` to create a new message. - */ -export declare const ReadArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ReadResult - */ -export type ReadResult = Message<"agent.v1.ReadResult"> & { - /** - * @generated from oneof agent.v1.ReadResult.result - */ - result: { - /** - * @generated from field: agent.v1.ReadSuccess success = 1; - */ - value: ReadSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ReadError error = 2; - */ - value: ReadError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.ReadRejected rejected = 3; - */ - value: ReadRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.ReadFileNotFound file_not_found = 4; - */ - value: ReadFileNotFound; - case: "fileNotFound"; - } | { - /** - * @generated from field: agent.v1.ReadPermissionDenied permission_denied = 5; - */ - value: ReadPermissionDenied; - case: "permissionDenied"; - } | { - /** - * @generated from field: agent.v1.ReadInvalidFile invalid_file = 6; - */ - value: ReadInvalidFile; - case: "invalidFile"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReadResult. - * Use `create(ReadResultSchema)` to create a new message. - */ -export declare const ReadResultSchema: GenMessage; -/** - * @generated from message agent.v1.ReadSuccess - */ -export type ReadSuccess = Message<"agent.v1.ReadSuccess"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: int32 total_lines = 3; - */ - totalLines: number; - /** - * @generated from field: int64 file_size = 4; - */ - fileSize: bigint; - /** - * true if the content was truncated due to size limits - * - * @generated from field: bool truncated = 6; - */ - truncated: boolean; - /** - * Returns blob ID if the output was stored in the blob store. If provided, the output is stored separately from the rest of the tool result, and since it's already in the blob store, it need not be sent back to the client -- reducing bandwidth. - * - * @generated from field: optional bytes output_blob_id = 7; - */ - outputBlobId?: Uint8Array; - /** - * @generated from oneof agent.v1.ReadSuccess.output - */ - output: { - /** - * @generated from field: string content = 2; - */ - value: string; - case: "content"; - } | { - /** - * @generated from field: bytes data = 5; - */ - value: Uint8Array; - case: "data"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReadSuccess. - * Use `create(ReadSuccessSchema)` to create a new message. - */ -export declare const ReadSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ReadError - */ -export type ReadError = Message<"agent.v1.ReadError"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string error = 2; - */ - error: string; -}; -/** - * Describes the message agent.v1.ReadError. - * Use `create(ReadErrorSchema)` to create a new message. - */ -export declare const ReadErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ReadRejected - */ -export type ReadRejected = Message<"agent.v1.ReadRejected"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string reason = 2; - */ - reason: string; -}; -/** - * Describes the message agent.v1.ReadRejected. - * Use `create(ReadRejectedSchema)` to create a new message. - */ -export declare const ReadRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.ReadFileNotFound - */ -export type ReadFileNotFound = Message<"agent.v1.ReadFileNotFound"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.ReadFileNotFound. - * Use `create(ReadFileNotFoundSchema)` to create a new message. - */ -export declare const ReadFileNotFoundSchema: GenMessage; -/** - * @generated from message agent.v1.ReadPermissionDenied - */ -export type ReadPermissionDenied = Message<"agent.v1.ReadPermissionDenied"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.ReadPermissionDenied. - * Use `create(ReadPermissionDeniedSchema)` to create a new message. - */ -export declare const ReadPermissionDeniedSchema: GenMessage; -/** - * @generated from message agent.v1.ReadInvalidFile - */ -export type ReadInvalidFile = Message<"agent.v1.ReadInvalidFile"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * e.g., "Path is a directory, not a file" - * - * @generated from field: string reason = 2; - */ - reason: string; -}; -/** - * Describes the message agent.v1.ReadInvalidFile. - * Use `create(ReadInvalidFileSchema)` to create a new message. - */ -export declare const ReadInvalidFileSchema: GenMessage; -/** - * @generated from message agent.v1.ReadToolCall - */ -export type ReadToolCall = Message<"agent.v1.ReadToolCall"> & { - /** - * @generated from field: agent.v1.ReadToolArgs args = 1; - */ - args?: ReadToolArgs; - /** - * @generated from field: agent.v1.ReadToolResult result = 2; - */ - result?: ReadToolResult; -}; -/** - * Describes the message agent.v1.ReadToolCall. - * Use `create(ReadToolCallSchema)` to create a new message. - */ -export declare const ReadToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ReadToolArgs - */ -export type ReadToolArgs = Message<"agent.v1.ReadToolArgs"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: optional int32 offset = 2; - */ - offset?: number; - /** - * @generated from field: optional int32 limit = 3; - */ - limit?: number; -}; -/** - * Describes the message agent.v1.ReadToolArgs. - * Use `create(ReadToolArgsSchema)` to create a new message. - */ -export declare const ReadToolArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ReadToolResult - */ -export type ReadToolResult = Message<"agent.v1.ReadToolResult"> & { - /** - * @generated from oneof agent.v1.ReadToolResult.result - */ - result: { - /** - * @generated from field: agent.v1.ReadToolSuccess success = 1; - */ - value: ReadToolSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ReadToolError error = 2; - */ - value: ReadToolError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReadToolResult. - * Use `create(ReadToolResultSchema)` to create a new message. - */ -export declare const ReadToolResultSchema: GenMessage; -/** - * @generated from message agent.v1.ReadRange - */ -export type ReadRange = Message<"agent.v1.ReadRange"> & { - /** - * @generated from field: uint32 start_line = 1; - */ - startLine: number; - /** - * @generated from field: uint32 end_line = 2; - */ - endLine: number; -}; -/** - * Describes the message agent.v1.ReadRange. - * Use `create(ReadRangeSchema)` to create a new message. - */ -export declare const ReadRangeSchema: GenMessage; -/** - * @generated from message agent.v1.ReadToolSuccess - */ -export type ReadToolSuccess = Message<"agent.v1.ReadToolSuccess"> & { - /** - * @generated from field: bool is_empty = 2; - */ - isEmpty: boolean; - /** - * @generated from field: bool exceeded_limit = 3; - */ - exceededLimit: boolean; - /** - * @generated from field: uint32 total_lines = 4; - */ - totalLines: number; - /** - * @generated from field: uint32 file_size = 5; - */ - fileSize: number; - /** - * @generated from field: string path = 7; - */ - path: string; - /** - * @generated from field: optional agent.v1.ReadRange read_range = 8; - */ - readRange?: ReadRange; - /** - * @generated from oneof agent.v1.ReadToolSuccess.output - */ - output: { - /** - * @generated from field: string content = 1; - */ - value: string; - case: "content"; - } | { - /** - * @generated from field: bytes data = 6; - */ - value: Uint8Array; - case: "data"; - } | { - /** - * @generated from field: bytes data_blob_id = 9; - */ - value: Uint8Array; - case: "dataBlobId"; - } | { - /** - * @generated from field: bytes content_blob_id = 10; - */ - value: Uint8Array; - case: "contentBlobId"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReadToolSuccess. - * Use `create(ReadToolSuccessSchema)` to create a new message. - */ -export declare const ReadToolSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ReadToolError - */ -export type ReadToolError = Message<"agent.v1.ReadToolError"> & { - /** - * @generated from field: string error_message = 1; - */ - errorMessage: string; -}; -/** - * Describes the message agent.v1.ReadToolError. - * Use `create(ReadToolErrorSchema)` to create a new message. - */ -export declare const ReadToolErrorSchema: GenMessage; -/** - * @generated from message agent.v1.RecordScreenArgs - */ -export type RecordScreenArgs = Message<"agent.v1.RecordScreenArgs"> & { - /** - * @generated from field: int32 mode = 1; - */ - mode: number; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; - /** - * Custom filename for SAVE_RECORDING mode - * - * @generated from field: optional string save_as_filename = 3; - */ - saveAsFilename?: string; -}; -/** - * Describes the message agent.v1.RecordScreenArgs. - * Use `create(RecordScreenArgsSchema)` to create a new message. - */ -export declare const RecordScreenArgsSchema: GenMessage; -/** - * @generated from message agent.v1.RecordScreenResult - */ -export type RecordScreenResult = Message<"agent.v1.RecordScreenResult"> & { - /** - * @generated from oneof agent.v1.RecordScreenResult.result - */ - result: { - /** - * @generated from field: agent.v1.RecordScreenStartSuccess start_success = 1; - */ - value: RecordScreenStartSuccess; - case: "startSuccess"; - } | { - /** - * @generated from field: agent.v1.RecordScreenSaveSuccess save_success = 2; - */ - value: RecordScreenSaveSuccess; - case: "saveSuccess"; - } | { - /** - * @generated from field: agent.v1.RecordScreenDiscardSuccess discard_success = 3; - */ - value: RecordScreenDiscardSuccess; - case: "discardSuccess"; - } | { - /** - * @generated from field: agent.v1.RecordScreenFailure failure = 4; - */ - value: RecordScreenFailure; - case: "failure"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.RecordScreenResult. - * Use `create(RecordScreenResultSchema)` to create a new message. - */ -export declare const RecordScreenResultSchema: GenMessage; -/** - * @generated from message agent.v1.RecordScreenStartSuccess - */ -export type RecordScreenStartSuccess = Message<"agent.v1.RecordScreenStartSuccess"> & { - /** - * True if a prior recording was cancelled, false otherwise - * - * @generated from field: bool was_prior_recording_cancelled = 1; - */ - wasPriorRecordingCancelled: boolean; - /** - * True if save_as_filename arg was passed to start tool and ignored - * - * @generated from field: bool was_save_as_filename_ignored = 2; - */ - wasSaveAsFilenameIgnored: boolean; -}; -/** - * Describes the message agent.v1.RecordScreenStartSuccess. - * Use `create(RecordScreenStartSuccessSchema)` to create a new message. - */ -export declare const RecordScreenStartSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.RecordScreenSaveSuccess - */ -export type RecordScreenSaveSuccess = Message<"agent.v1.RecordScreenSaveSuccess"> & { - /** - * Path to the saved recording file - * - * @generated from field: string path = 1; - */ - path: string; - /** - * Duration of the recording in milliseconds - * - * @generated from field: int64 recording_duration_ms = 2; - */ - recordingDurationMs: bigint; - /** - * Set if save_as_filename was invalid and default path was used instead - * - * @generated from field: optional int32 requested_file_path_rejected_reason = 3; - */ - requestedFilePathRejectedReason?: number; -}; -/** - * Describes the message agent.v1.RecordScreenSaveSuccess. - * Use `create(RecordScreenSaveSuccessSchema)` to create a new message. - */ -export declare const RecordScreenSaveSuccessSchema: GenMessage; -/** - * Empty message - recording discarded successfully - * - * @generated from message agent.v1.RecordScreenDiscardSuccess - */ -export type RecordScreenDiscardSuccess = Message<"agent.v1.RecordScreenDiscardSuccess"> & {}; -/** - * Describes the message agent.v1.RecordScreenDiscardSuccess. - * Use `create(RecordScreenDiscardSuccessSchema)` to create a new message. - */ -export declare const RecordScreenDiscardSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.RecordScreenFailure - */ -export type RecordScreenFailure = Message<"agent.v1.RecordScreenFailure"> & { - /** - * Error message - * - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.RecordScreenFailure. - * Use `create(RecordScreenFailureSchema)` to create a new message. - */ -export declare const RecordScreenFailureSchema: GenMessage; -/** - * @generated from message agent.v1.CursorPackagePrompt - */ -export type CursorPackagePrompt = Message<"agent.v1.CursorPackagePrompt"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: string file_path = 2; - */ - filePath: string; -}; -/** - * Describes the message agent.v1.CursorPackagePrompt. - * Use `create(CursorPackagePromptSchema)` to create a new message. - */ -export declare const CursorPackagePromptSchema: GenMessage; -/** - * @generated from message agent.v1.CursorPackage - */ -export type CursorPackage = Message<"agent.v1.CursorPackage"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: string description = 2; - */ - description: string; - /** - * @generated from field: string folder_path = 3; - */ - folderPath: string; - /** - * @generated from field: bool enabled = 4; - */ - enabled: boolean; - /** - * @generated from field: optional string parse_error = 5; - */ - parseError?: string; - /** - * @generated from field: repeated agent.v1.CursorPackagePrompt prompts = 6; - */ - prompts: CursorPackagePrompt[]; - /** - * @generated from field: string readme_file_path = 7; - */ - readmeFilePath: string; - /** - * @generated from field: int32 package_type = 8; - */ - packageType: number; -}; -/** - * Describes the message agent.v1.CursorPackage. - * Use `create(CursorPackageSchema)` to create a new message. - */ -export declare const CursorPackageSchema: GenMessage; -/** - * TODO: you should be able to override / configure this list in your .vscode settings not exactly sure what that should look like... but i guess you should be able to specify an override URL because we use URLs for identifying repos and maybe you should be able to specify additional buckets too... like in the jane street case: i guess jane street should have some default buckets - * - * @generated from message agent.v1.RepositoryIndexingInfo - */ -export type RepositoryIndexingInfo = Message<"agent.v1.RepositoryIndexingInfo"> & { - /** - * the relative path in the current workspace this is useful for locating the repo and identifying what repo a given file is in this should be unique for different repositories (I think) - * - * @generated from field: string relative_workspace_path = 1; - */ - relativeWorkspacePath: string; - /** - * a git repo may have multiple remotes at the server we choose the remote (either origin, or the one we have embedded, or something else) invariant: len(remote_urls) == len(remote_names) - * - * @generated from field: repeated string remote_urls = 2; - */ - remoteUrls: string[]; - /** - * @generated from field: repeated string remote_names = 3; - */ - remoteNames: string[]; - /** - * @generated from field: string repo_name = 4; - */ - repoName: string; - /** - * @generated from field: string repo_owner = 5; - */ - repoOwner: string; - /** - * @generated from field: bool is_tracked = 6; - */ - isTracked: boolean; - /** - * If this is local - * - * @generated from field: bool is_local = 7; - */ - isLocal: boolean; - /** - * the orthogonal transform seed if sent from the client! if the client sends up the transform seed then we use that for the orthogonal transform instead of the value stored in the database - * - * @generated from field: optional double orthogonal_transform_seed = 8; - */ - orthogonalTransformSeed?: number; - /** - * The encrypted workspace uri for the repository. - * - * @generated from field: string workspace_uri = 9; - */ - workspaceUri: string; - /** - * The encryption key for partial paths - * - * @generated from field: string path_encryption_key = 10; - */ - pathEncryptionKey: string; -}; -/** - * Describes the message agent.v1.RepositoryIndexingInfo. - * Use `create(RepositoryIndexingInfoSchema)` to create a new message. - */ -export declare const RepositoryIndexingInfoSchema: GenMessage; -/** - * @generated from message agent.v1.RequestContextArgs - */ -export type RequestContextArgs = Message<"agent.v1.RequestContextArgs"> & { - /** - * @generated from field: optional string notes_session_id = 2; - */ - notesSessionId?: string; - /** - * @generated from field: optional string workspace_id = 3; - */ - workspaceId?: string; -}; -/** - * Describes the message agent.v1.RequestContextArgs. - * Use `create(RequestContextArgsSchema)` to create a new message. - */ -export declare const RequestContextArgsSchema: GenMessage; -/** - * @generated from message agent.v1.RequestContextResult - */ -export type RequestContextResult = Message<"agent.v1.RequestContextResult"> & { - /** - * @generated from oneof agent.v1.RequestContextResult.result - */ - result: { - /** - * @generated from field: agent.v1.RequestContextSuccess success = 1; - */ - value: RequestContextSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.RequestContextError error = 2; - */ - value: RequestContextError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.RequestContextRejected rejected = 3; - */ - value: RequestContextRejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.RequestContextResult. - * Use `create(RequestContextResultSchema)` to create a new message. - */ -export declare const RequestContextResultSchema: GenMessage; -/** - * @generated from message agent.v1.RequestContextSuccess - */ -export type RequestContextSuccess = Message<"agent.v1.RequestContextSuccess"> & { - /** - * @generated from field: agent.v1.RequestContext request_context = 1; - */ - requestContext?: RequestContext; -}; -/** - * Describes the message agent.v1.RequestContextSuccess. - * Use `create(RequestContextSuccessSchema)` to create a new message. - */ -export declare const RequestContextSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.RequestContextError - */ -export type RequestContextError = Message<"agent.v1.RequestContextError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.RequestContextError. - * Use `create(RequestContextErrorSchema)` to create a new message. - */ -export declare const RequestContextErrorSchema: GenMessage; -/** - * @generated from message agent.v1.RequestContextRejected - */ -export type RequestContextRejected = Message<"agent.v1.RequestContextRejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.RequestContextRejected. - * Use `create(RequestContextRejectedSchema)` to create a new message. - */ -export declare const RequestContextRejectedSchema: GenMessage; -/** - * same as SelectedImage, but with the data field is the full image data - * - * @generated from message agent.v1.ImageProto - */ -export type ImageProto = Message<"agent.v1.ImageProto"> & { - /** - * @generated from field: bytes data = 1; - */ - data: Uint8Array; - /** - * @generated from field: string uuid = 2; - */ - uuid: string; - /** - * @generated from field: string path = 3; - */ - path: string; - /** - * @generated from field: agent.v1.ImageProto_Dimension dimension = 4; - */ - dimension?: ImageProto_Dimension; - /** - * @generated from field: optional string task_specific_description = 6; - */ - taskSpecificDescription?: string; - /** - * @generated from field: string mime_type = 7; - */ - mimeType: string; -}; -/** - * Describes the message agent.v1.ImageProto. - * Use `create(ImageProtoSchema)` to create a new message. - */ -export declare const ImageProtoSchema: GenMessage; -/** - * @generated from message agent.v1.ImageProto_Dimension - */ -export type ImageProto_Dimension = Message<"agent.v1.ImageProto_Dimension"> & { - /** - * @generated from field: int32 width = 1; - */ - width: number; - /** - * @generated from field: int32 height = 2; - */ - height: number; -}; -/** - * Describes the message agent.v1.ImageProto_Dimension. - * Use `create(ImageProto_DimensionSchema)` to create a new message. - */ -export declare const ImageProto_DimensionSchema: GenMessage; -/** - * Git repository information for a workspace - * - * @generated from message agent.v1.GitRepoInfo - */ -export type GitRepoInfo = Message<"agent.v1.GitRepoInfo"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string status = 2; - */ - status: string; - /** - * @generated from field: string branch_name = 3; - */ - branchName: string; - /** - * @generated from field: optional string remote_url = 4; - */ - remoteUrl?: string; -}; -/** - * Describes the message agent.v1.GitRepoInfo. - * Use `create(GitRepoInfoSchema)` to create a new message. - */ -export declare const GitRepoInfoSchema: GenMessage; -/** - * Environment details for system prompt/context - * - * @generated from message agent.v1.RequestContextEnv - */ -export type RequestContextEnv = Message<"agent.v1.RequestContextEnv"> & { - /** - * @generated from field: string os_version = 1; - */ - osVersion: string; - /** - * @generated from field: repeated string workspace_paths = 2; - */ - workspacePaths: string[]; - /** - * @generated from field: string shell = 3; - */ - shell: string; - /** - * @generated from field: bool sandbox_enabled = 5; - */ - sandboxEnabled: boolean; - /** - * @generated from field: string terminals_folder = 7; - */ - terminalsFolder: string; - /** - * @generated from field: string agent_shared_notes_folder = 8; - */ - agentSharedNotesFolder: string; - /** - * @generated from field: string agent_conversation_notes_folder = 9; - */ - agentConversationNotesFolder: string; - /** - * @generated from field: string time_zone = 10; - */ - timeZone: string; - /** - * Project-specific folder for storing artifacts, computed client-side as ~/.cursor/projects/{slug}/ - * - * @generated from field: string project_folder = 11; - */ - projectFolder: string; - /** - * Folder where agent conversation transcripts are stored - * - * @generated from field: string agent_transcripts_folder = 12; - */ - agentTranscriptsFolder: string; -}; -/** - * Describes the message agent.v1.RequestContextEnv. - * Use `create(RequestContextEnvSchema)` to create a new message. - */ -export declare const RequestContextEnvSchema: GenMessage; -/** - * @generated from message agent.v1.DebugModeConfig - */ -export type DebugModeConfig = Message<"agent.v1.DebugModeConfig"> & { - /** - * @generated from field: string log_path = 1; - */ - logPath: string; - /** - * @generated from field: string server_endpoint = 2; - */ - serverEndpoint: string; -}; -/** - * Describes the message agent.v1.DebugModeConfig. - * Use `create(DebugModeConfigSchema)` to create a new message. - */ -export declare const DebugModeConfigSchema: GenMessage; -/** - * @generated from message agent.v1.SkillDescriptor - */ -export type SkillDescriptor = Message<"agent.v1.SkillDescriptor"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: string description = 2; - */ - description: string; - /** - * @generated from field: string folder_path = 3; - */ - folderPath: string; - /** - * @generated from field: bool enabled = 4; - */ - enabled: boolean; - /** - * @generated from field: optional string parse_error = 5; - */ - parseError?: string; - /** - * @generated from field: string readme_file_path = 6; - */ - readmeFilePath: string; - /** - * @generated from field: int32 package_type = 7; - */ - packageType: number; -}; -/** - * Describes the message agent.v1.SkillDescriptor. - * Use `create(SkillDescriptorSchema)` to create a new message. - */ -export declare const SkillDescriptorSchema: GenMessage; -/** - * @generated from message agent.v1.SkillOptions - */ -export type SkillOptions = Message<"agent.v1.SkillOptions"> & { - /** - * @generated from field: repeated agent.v1.SkillDescriptor skill_descriptors = 1; - */ - skillDescriptors: SkillDescriptor[]; -}; -/** - * Describes the message agent.v1.SkillOptions. - * Use `create(SkillOptionsSchema)` to create a new message. - */ -export declare const SkillOptionsSchema: GenMessage; -/** - * @generated from message agent.v1.RequestContext - */ -export type RequestContext = Message<"agent.v1.RequestContext"> & { - /** - * All rules, categorized by the embedded type - * - * @generated from field: repeated agent.v1.CursorRule rules = 2; - */ - rules: CursorRule[]; - /** - * @generated from field: agent.v1.RequestContextEnv env = 4; - */ - env?: RequestContextEnv; - /** - * @generated from field: repeated agent.v1.RepositoryIndexingInfo repository_info = 6; - */ - repositoryInfo: RepositoryIndexingInfo[]; - /** - * @generated from field: repeated agent.v1.McpToolDefinition tools = 7; - */ - tools: McpToolDefinition[]; - /** - * @generated from field: optional string conversation_notes_listing = 8; - */ - conversationNotesListing?: string; - /** - * @generated from field: optional string shared_notes_listing = 9; - */ - sharedNotesListing?: string; - /** - * @generated from field: repeated agent.v1.GitRepoInfo git_repos = 11; - */ - gitRepos: GitRepoInfo[]; - /** - * @generated from field: repeated agent.v1.LsDirectoryTreeNode project_layouts = 13; - */ - projectLayouts: LsDirectoryTreeNode[]; - /** - * @generated from field: repeated agent.v1.McpInstructions mcp_instructions = 14; - */ - mcpInstructions: McpInstructions[]; - /** - * @generated from field: optional agent.v1.DebugModeConfig debug_mode_config = 15; - */ - debugModeConfig?: DebugModeConfig; - /** - * @generated from field: optional string cloud_rule = 16; - */ - cloudRule?: string; - /** - * @generated from field: optional bool web_search_enabled = 17; - */ - webSearchEnabled?: boolean; - /** - * @generated from field: optional agent.v1.SkillOptions skill_options = 18; - */ - skillOptions?: SkillOptions; - /** - * @generated from field: optional bool repository_info_should_query_prod = 19; - */ - repositoryInfoShouldQueryProd?: boolean; - /** - * @generated from field: map file_contents = 20; - */ - fileContents: { - [key: string]: string; - }; - /** - * Content of the user-intent/index.md file summarizing past conversations - * - * @generated from field: optional string user_intent_summary = 21; - */ - userIntentSummary?: string; - /** - * Local custom subagent definitions loaded from workspace configuration - * - * @generated from field: repeated agent.v1.CustomSubagent custom_subagents = 22; - */ - customSubagents: CustomSubagent[]; - /** - * MCP file system options for agent MCP tool descriptor access - * - * @generated from field: optional agent.v1.McpFileSystemOptions mcp_file_system_options = 23; - */ - mcpFileSystemOptions?: McpFileSystemOptions; -}; -/** - * Describes the message agent.v1.RequestContext. - * Use `create(RequestContextSchema)` to create a new message. - */ -export declare const RequestContextSchema: GenMessage; -/** - * @generated from message agent.v1.SandboxPolicy - */ -export type SandboxPolicy = Message<"agent.v1.SandboxPolicy"> & { - /** - * @generated from field: int32 type = 1; - */ - type: number; - /** - * @generated from field: optional bool network_access = 2; - */ - networkAccess?: boolean; - /** - * @generated from field: repeated string additional_readwrite_paths = 3; - */ - additionalReadwritePaths: string[]; - /** - * @generated from field: repeated string additional_readonly_paths = 4; - */ - additionalReadonlyPaths: string[]; - /** - * @generated from field: optional string debug_output_dir = 5; - */ - debugOutputDir?: string; - /** - * @generated from field: optional bool block_git_writes = 6; - */ - blockGitWrites?: boolean; - /** - * If true, excludes default tmp paths (/tmp/, /private/tmp/, /var/folders/) from the sandbox writable paths. Useful for testing readonly behavior. - * - * @generated from field: optional bool disable_tmp_write = 7; - */ - disableTmpWrite?: boolean; -}; -/** - * Describes the message agent.v1.SandboxPolicy. - * Use `create(SandboxPolicySchema)` to create a new message. - */ -export declare const SandboxPolicySchema: GenMessage; -/** - * @generated from message agent.v1.SelectedImage - */ -export type SelectedImage = Message<"agent.v1.SelectedImage"> & { - /** - * @generated from field: string uuid = 2; - */ - uuid: string; - /** - * @generated from field: string path = 3; - */ - path: string; - /** - * @generated from field: agent.v1.SelectedImage_Dimension dimension = 4; - */ - dimension?: SelectedImage_Dimension; - /** - * @generated from field: string mime_type = 7; - */ - mimeType: string; - /** - * @generated from oneof agent.v1.SelectedImage.data_or_blob_id - */ - dataOrBlobId: { - /** - * @generated from field: bytes blob_id = 1; - */ - value: Uint8Array; - case: "blobId"; - } | { - /** - * @generated from field: bytes data = 8; - */ - value: Uint8Array; - case: "data"; - } | { - /** - * @generated from field: agent.v1.SelectedImage_BlobIdWithData blob_id_with_data = 9; - */ - value: SelectedImage_BlobIdWithData; - case: "blobIdWithData"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.SelectedImage. - * Use `create(SelectedImageSchema)` to create a new message. - */ -export declare const SelectedImageSchema: GenMessage; -/** - * Contains both blob_id and data together, for when the client has both and wants to populate the server-side cache without re-uploading - * - * @generated from message agent.v1.SelectedImage_BlobIdWithData - */ -export type SelectedImage_BlobIdWithData = Message<"agent.v1.SelectedImage_BlobIdWithData"> & { - /** - * @generated from field: bytes blob_id = 1; - */ - blobId: Uint8Array; - /** - * @generated from field: bytes data = 2; - */ - data: Uint8Array; -}; -/** - * Describes the message agent.v1.SelectedImage_BlobIdWithData. - * Use `create(SelectedImage_BlobIdWithDataSchema)` to create a new message. - */ -export declare const SelectedImage_BlobIdWithDataSchema: GenMessage; -/** - * @generated from message agent.v1.SelectedImage_Dimension - */ -export type SelectedImage_Dimension = Message<"agent.v1.SelectedImage_Dimension"> & { - /** - * @generated from field: int32 width = 1; - */ - width: number; - /** - * @generated from field: int32 height = 2; - */ - height: number; -}; -/** - * Describes the message agent.v1.SelectedImage_Dimension. - * Use `create(SelectedImage_DimensionSchema)` to create a new message. - */ -export declare const SelectedImage_DimensionSchema: GenMessage; -/** - * Extra context entry that can be stored inline or as a blob reference - * - * @generated from message agent.v1.ExtraContextEntry - */ -export type ExtraContextEntry = Message<"agent.v1.ExtraContextEntry"> & { - /** - * @generated from oneof agent.v1.ExtraContextEntry.data_or_blob_id - */ - dataOrBlobId: { - /** - * @generated from field: string data = 1; - */ - value: string; - case: "data"; - } | { - /** - * @generated from field: bytes blob_id = 2; - */ - value: Uint8Array; - case: "blobId"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExtraContextEntry. - * Use `create(ExtraContextEntrySchema)` to create a new message. - */ -export declare const ExtraContextEntrySchema: GenMessage; -/** - * A selected file from the UI - * - * @generated from message agent.v1.SelectedFile - */ -export type SelectedFile = Message<"agent.v1.SelectedFile"> & { - /** - * @generated from field: string content = 1; - */ - content: string; - /** - * This is the full path - * - * @generated from field: string path = 2; - */ - path: string; - /** - * @generated from field: optional string relative_path = 3; - */ - relativePath?: string; -}; -/** - * Describes the message agent.v1.SelectedFile. - * Use `create(SelectedFileSchema)` to create a new message. - */ -export declare const SelectedFileSchema: GenMessage; -/** - * A selected code selection from the UI - * - * @generated from message agent.v1.SelectedCodeSelection - */ -export type SelectedCodeSelection = Message<"agent.v1.SelectedCodeSelection"> & { - /** - * @generated from field: string content = 1; - */ - content: string; - /** - * This is the full path - * - * @generated from field: string path = 2; - */ - path: string; - /** - * @generated from field: optional string relative_path = 3; - */ - relativePath?: string; - /** - * @generated from field: agent.v1.Range range = 4; - */ - range?: Range; -}; -/** - * Describes the message agent.v1.SelectedCodeSelection. - * Use `create(SelectedCodeSelectionSchema)` to create a new message. - */ -export declare const SelectedCodeSelectionSchema: GenMessage; -/** - * A selected terminal from the UI - * - * @generated from message agent.v1.SelectedTerminal - */ -export type SelectedTerminal = Message<"agent.v1.SelectedTerminal"> & { - /** - * @generated from field: string content = 1; - */ - content: string; - /** - * @generated from field: optional string title = 2; - */ - title?: string; - /** - * @generated from field: optional string path = 3; - */ - path?: string; -}; -/** - * Describes the message agent.v1.SelectedTerminal. - * Use `create(SelectedTerminalSchema)` to create a new message. - */ -export declare const SelectedTerminalSchema: GenMessage; -/** - * A selected terminal selection from the UI - * - * @generated from message agent.v1.SelectedTerminalSelection - */ -export type SelectedTerminalSelection = Message<"agent.v1.SelectedTerminalSelection"> & { - /** - * @generated from field: string content = 1; - */ - content: string; - /** - * @generated from field: optional string title = 2; - */ - title?: string; - /** - * @generated from field: optional string path = 3; - */ - path?: string; - /** - * @generated from field: agent.v1.Range range = 4; - */ - range?: Range; -}; -/** - * Describes the message agent.v1.SelectedTerminalSelection. - * Use `create(SelectedTerminalSelectionSchema)` to create a new message. - */ -export declare const SelectedTerminalSelectionSchema: GenMessage; -/** - * A selected folder from the UI - * - * @generated from message agent.v1.SelectedFolder - */ -export type SelectedFolder = Message<"agent.v1.SelectedFolder"> & { - /** - * This is the full path - * - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: optional string relative_path = 2; - */ - relativePath?: string; - /** - * @generated from field: agent.v1.LsDirectoryTreeNode directory_tree = 3; - */ - directoryTree?: LsDirectoryTreeNode; -}; -/** - * Describes the message agent.v1.SelectedFolder. - * Use `create(SelectedFolderSchema)` to create a new message. - */ -export declare const SelectedFolderSchema: GenMessage; -/** - * An external link manually attached by the user - * - * @generated from message agent.v1.SelectedExternalLink - */ -export type SelectedExternalLink = Message<"agent.v1.SelectedExternalLink"> & { - /** - * @generated from field: string url = 1; - */ - url: string; - /** - * @generated from field: string uuid = 2; - */ - uuid: string; - /** - * For local PDF files Base64-encoded PDF content - * - * @generated from field: optional string pdf_content = 3; - */ - pdfContent?: string; - /** - * @generated from field: optional bool is_pdf = 4; - */ - isPdf?: boolean; - /** - * @generated from field: optional string filename = 5; - */ - filename?: string; -}; -/** - * Describes the message agent.v1.SelectedExternalLink. - * Use `create(SelectedExternalLinkSchema)` to create a new message. - */ -export declare const SelectedExternalLinkSchema: GenMessage; -/** - * A cursor rule manually attached by the user - * - * @generated from message agent.v1.SelectedCursorRule - */ -export type SelectedCursorRule = Message<"agent.v1.SelectedCursorRule"> & { - /** - * @generated from field: agent.v1.CursorRule rule = 1; - */ - rule?: CursorRule; -}; -/** - * Describes the message agent.v1.SelectedCursorRule. - * Use `create(SelectedCursorRuleSchema)` to create a new message. - */ -export declare const SelectedCursorRuleSchema: GenMessage; -/** - * Git diff (uncommitted changes in working tree) - * - * @generated from message agent.v1.SelectedGitDiff - */ -export type SelectedGitDiff = Message<"agent.v1.SelectedGitDiff"> & { - /** - * Raw git diff output - * - * @generated from field: string content = 1; - */ - content: string; -}; -/** - * Describes the message agent.v1.SelectedGitDiff. - * Use `create(SelectedGitDiffSchema)` to create a new message. - */ -export declare const SelectedGitDiffSchema: GenMessage; -/** - * Git diff from branch to main - * - * @generated from message agent.v1.SelectedGitDiffFromBranchToMain - */ -export type SelectedGitDiffFromBranchToMain = Message<"agent.v1.SelectedGitDiffFromBranchToMain"> & { - /** - * Raw git diff output - * - * @generated from field: string content = 1; - */ - content: string; -}; -/** - * Describes the message agent.v1.SelectedGitDiffFromBranchToMain. - * Use `create(SelectedGitDiffFromBranchToMainSchema)` to create a new message. - */ -export declare const SelectedGitDiffFromBranchToMainSchema: GenMessage; -/** - * A git commit manually attached by the user - * - * @generated from message agent.v1.SelectedGitCommit - */ -export type SelectedGitCommit = Message<"agent.v1.SelectedGitCommit"> & { - /** - * @generated from field: string sha = 1; - */ - sha: string; - /** - * @generated from field: string message = 2; - */ - message: string; - /** - * @generated from field: optional string description = 3; - */ - description?: string; - /** - * Raw git diff output for this commit - * - * @generated from field: string diff = 4; - */ - diff: string; -}; -/** - * Describes the message agent.v1.SelectedGitCommit. - * Use `create(SelectedGitCommitSchema)` to create a new message. - */ -export declare const SelectedGitCommitSchema: GenMessage; -/** - * A pull request manually attached by the user via @mention Uses the same folder structure as ViewedPullRequest for consistency - * - * @generated from message agent.v1.SelectedPullRequest - */ -export type SelectedPullRequest = Message<"agent.v1.SelectedPullRequest"> & { - /** - * @generated from field: int32 number = 1; - */ - number: number; - /** - * @generated from field: string url = 2; - */ - url: string; - /** - * @generated from field: optional string title = 3; - */ - title?: string; - /** - * Path to the folder containing PR details (diffs, metadata, etc.) - * - * @generated from field: string folder_path = 4; - */ - folderPath: string; - /** - * Summary JSON containing file list and diff sizes (contents of summary.json) - * - * @generated from field: optional string summary_json = 5; - */ - summaryJson?: string; - /** - * PR description/body - * - * @generated from field: optional string description = 6; - */ - description?: string; - /** - * If set, other fields are empty and data should be fetched from the blob - * - * @generated from field: optional bytes blob_id = 7; - */ - blobId?: Uint8Array; -}; -/** - * Describes the message agent.v1.SelectedPullRequest. - * Use `create(SelectedPullRequestSchema)` to create a new message. - */ -export declare const SelectedPullRequestSchema: GenMessage; -/** - * A selection from a pull request diff (for files that may not exist on disk) - * - * @generated from message agent.v1.SelectedGitPRDiffSelection - */ -export type SelectedGitPRDiffSelection = Message<"agent.v1.SelectedGitPRDiffSelection"> & { - /** - * Full URL to the pull request - * - * @generated from field: string pr_url = 1; - */ - prUrl: string; - /** - * Path to the file within the PR - * - * @generated from field: string file_path = 2; - */ - filePath: string; - /** - * Start line in the diff - * - * @generated from field: int32 start_line = 3; - */ - startLine: number; - /** - * End line in the diff - * - * @generated from field: int32 end_line = 4; - */ - endLine: number; - /** - * The diff content for this file (or selection) - * - * @generated from field: optional string diff_content = 5; - */ - diffContent?: string; - /** - * If set, other fields are empty and data should be fetched from the blob - * - * @generated from field: optional bytes blob_id = 6; - */ - blobId?: Uint8Array; -}; -/** - * Describes the message agent.v1.SelectedGitPRDiffSelection. - * Use `create(SelectedGitPRDiffSelectionSchema)` to create a new message. - */ -export declare const SelectedGitPRDiffSelectionSchema: GenMessage; -/** - * A cursor command manually attached by the user - * - * @generated from message agent.v1.SelectedCursorCommand - */ -export type SelectedCursorCommand = Message<"agent.v1.SelectedCursorCommand"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: string content = 2; - */ - content: string; -}; -/** - * Describes the message agent.v1.SelectedCursorCommand. - * Use `create(SelectedCursorCommandSchema)` to create a new message. - */ -export declare const SelectedCursorCommandSchema: GenMessage; -/** - * A documentation manually attached by the user - * - * @generated from message agent.v1.SelectedDocumentation - */ -export type SelectedDocumentation = Message<"agent.v1.SelectedDocumentation"> & { - /** - * @generated from field: string doc_id = 1; - */ - docId: string; - /** - * @generated from field: string name = 2; - */ - name: string; -}; -/** - * Describes the message agent.v1.SelectedDocumentation. - * Use `create(SelectedDocumentationSchema)` to create a new message. - */ -export declare const SelectedDocumentationSchema: GenMessage; -/** - * A past chat manually attached by the user (transcript file) - * - * @generated from message agent.v1.SelectedPastChat - */ -export type SelectedPastChat = Message<"agent.v1.SelectedPastChat"> & { - /** - * @generated from field: string agent_id = 1; - */ - agentId: string; - /** - * @generated from field: string name = 2; - */ - name: string; -}; -/** - * Describes the message agent.v1.SelectedPastChat. - * Use `create(SelectedPastChatSchema)` to create a new message. - */ -export declare const SelectedPastChatSchema: GenMessage; -/** - * A call frame from a stack trace - * - * @generated from message agent.v1.CallFrame - */ -export type CallFrame = Message<"agent.v1.CallFrame"> & { - /** - * @generated from field: optional string function_name = 1; - */ - functionName?: string; - /** - * @generated from field: optional string url = 2; - */ - url?: string; - /** - * @generated from field: optional int32 line_number = 3; - */ - lineNumber?: number; - /** - * @generated from field: optional int32 column_number = 4; - */ - columnNumber?: number; -}; -/** - * Describes the message agent.v1.CallFrame. - * Use `create(CallFrameSchema)` to create a new message. - */ -export declare const CallFrameSchema: GenMessage; -/** - * A stack trace - * - * @generated from message agent.v1.StackTrace - */ -export type StackTrace = Message<"agent.v1.StackTrace"> & { - /** - * @generated from field: repeated agent.v1.CallFrame call_frames = 1; - */ - callFrames: CallFrame[]; - /** - * @generated from field: optional string raw_stack_trace = 2; - */ - rawStackTrace?: string; -}; -/** - * Describes the message agent.v1.StackTrace. - * Use `create(StackTraceSchema)` to create a new message. - */ -export declare const StackTraceSchema: GenMessage; -/** - * A console log entry from the runtime - * - * @generated from message agent.v1.SelectedConsoleLog - */ -export type SelectedConsoleLog = Message<"agent.v1.SelectedConsoleLog"> & { - /** - * @generated from field: string message = 1; - */ - message: string; - /** - * * Unix timestamp in milliseconds when this log entry was created - * - * @generated from field: double timestamp = 2; - */ - timestamp: number; - /** - * @generated from field: string level = 3; - */ - level: string; - /** - * @generated from field: string client_name = 4; - */ - clientName: string; - /** - * @generated from field: string session_id = 5; - */ - sessionId: string; - /** - * @generated from field: optional agent.v1.StackTrace stack_trace = 6; - */ - stackTrace?: StackTrace; - /** - * @generated from field: optional string object_data_json = 7; - */ - objectDataJson?: string; -}; -/** - * Describes the message agent.v1.SelectedConsoleLog. - * Use `create(SelectedConsoleLogSchema)` to create a new message. - */ -export declare const SelectedConsoleLogSchema: GenMessage; -/** - * A UI element picked by the user from the runtime - * - * @generated from message agent.v1.SelectedUIElement - */ -export type SelectedUIElement = Message<"agent.v1.SelectedUIElement"> & { - /** - * @generated from field: string element = 1; - */ - element: string; - /** - * @generated from field: string xpath = 2; - */ - xpath: string; - /** - * @generated from field: string text_content = 3; - */ - textContent: string; - /** - * @generated from field: string extra = 4; - */ - extra: string; - /** - * @generated from field: optional string component = 5; - */ - component?: string; - /** - * @generated from field: optional string component_props_json = 6; - */ - componentPropsJson?: string; -}; -/** - * Describes the message agent.v1.SelectedUIElement. - * Use `create(SelectedUIElementSchema)` to create a new message. - */ -export declare const SelectedUIElementSchema: GenMessage; -/** - * A subagent selected by the user from the slash menu - * - * @generated from message agent.v1.SelectedSubagent - */ -export type SelectedSubagent = Message<"agent.v1.SelectedSubagent"> & { - /** - * @generated from field: string name = 1; - */ - name: string; -}; -/** - * Describes the message agent.v1.SelectedSubagent. - * Use `create(SelectedSubagentSchema)` to create a new message. - */ -export declare const SelectedSubagentSchema: GenMessage; -/** - * Container for selected context from the UI - * - * @generated from message agent.v1.SelectedContext - */ -export type SelectedContext = Message<"agent.v1.SelectedContext"> & { - /** - * @generated from field: repeated agent.v1.SelectedImage selected_images = 1; - */ - selectedImages: SelectedImage[]; - /** - * @generated from field: optional agent.v1.InvocationContext invocation_context = 2; - */ - invocationContext?: InvocationContext; - /** - * Temporary hack for IDE-based context (@filename, @Diff, etc.) in background agents only. TODO: remove once proper IDE context format is implemented. - * - * @generated from field: repeated string extra_context = 3; - */ - extraContext: string[]; - /** - * @generated from field: repeated agent.v1.ExtraContextEntry extra_context_entries = 16; - */ - extraContextEntries: ExtraContextEntry[]; - /** - * New context types - * - * @generated from field: repeated agent.v1.SelectedFile files = 4; - */ - files: SelectedFile[]; - /** - * @generated from field: repeated agent.v1.SelectedCodeSelection code_selections = 5; - */ - codeSelections: SelectedCodeSelection[]; - /** - * @generated from field: repeated agent.v1.SelectedTerminal terminals = 6; - */ - terminals: SelectedTerminal[]; - /** - * @generated from field: repeated agent.v1.SelectedTerminalSelection terminal_selections = 7; - */ - terminalSelections: SelectedTerminalSelection[]; - /** - * @generated from field: repeated agent.v1.SelectedFolder folders = 8; - */ - folders: SelectedFolder[]; - /** - * @generated from field: repeated agent.v1.SelectedExternalLink external_links = 9; - */ - externalLinks: SelectedExternalLink[]; - /** - * @generated from field: repeated agent.v1.SelectedCursorRule cursor_rules = 10; - */ - cursorRules: SelectedCursorRule[]; - /** - * @generated from field: optional agent.v1.SelectedGitDiff git_diff = 18; - */ - gitDiff?: SelectedGitDiff; - /** - * @generated from field: optional agent.v1.SelectedGitDiffFromBranchToMain git_diff_from_branch_to_main = 11; - */ - gitDiffFromBranchToMain?: SelectedGitDiffFromBranchToMain; - /** - * @generated from field: repeated agent.v1.SelectedCursorCommand cursor_commands = 12; - */ - cursorCommands: SelectedCursorCommand[]; - /** - * @generated from field: repeated agent.v1.SelectedDocumentation documentations = 13; - */ - documentations: SelectedDocumentation[]; - /** - * @generated from field: repeated agent.v1.SelectedUIElement ui_elements = 14; - */ - uiElements: SelectedUIElement[]; - /** - * @generated from field: repeated agent.v1.SelectedConsoleLog console_logs = 15; - */ - consoleLogs: SelectedConsoleLog[]; - /** - * @generated from field: repeated agent.v1.SelectedGitCommit git_commits = 17; - */ - gitCommits: SelectedGitCommit[]; - /** - * @generated from field: repeated agent.v1.SelectedPastChat past_chats = 19; - */ - pastChats: SelectedPastChat[]; - /** - * @generated from field: repeated agent.v1.SelectedGitPRDiffSelection git_pr_diff_selections = 20; - */ - gitPrDiffSelections: SelectedGitPRDiffSelection[]; - /** - * @generated from field: repeated agent.v1.SelectedPullRequest selected_pull_requests = 21; - */ - selectedPullRequests: SelectedPullRequest[]; - /** - * @generated from field: repeated agent.v1.SelectedSubagent selected_subagents = 22; - */ - selectedSubagents: SelectedSubagent[]; -}; -/** - * Describes the message agent.v1.SelectedContext. - * Use `create(SelectedContextSchema)` to create a new message. - */ -export declare const SelectedContextSchema: GenMessage; -/** - * InvocationContext represents the context from the external app/integration that triggered this agent request. - * - * @generated from message agent.v1.InvocationContext - */ -export type InvocationContext = Message<"agent.v1.InvocationContext"> & { - /** - * @generated from oneof agent.v1.InvocationContext.data - */ - data: { - /** - * @generated from field: agent.v1.InvocationContext_SlackThread slack_thread = 1; - */ - value: InvocationContext_SlackThread; - case: "slackThread"; - } | { - /** - * @generated from field: agent.v1.InvocationContext_GithubPR github_pr = 2; - */ - value: InvocationContext_GithubPR; - case: "githubPr"; - } | { - /** - * @generated from field: agent.v1.InvocationContext_IdeState ide_state = 3; - */ - value: InvocationContext_IdeState; - case: "ideState"; - } | { - /** - * @generated from field: bytes blob_id = 10; - */ - value: Uint8Array; - case: "blobId"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.InvocationContext. - * Use `create(InvocationContextSchema)` to create a new message. - */ -export declare const InvocationContextSchema: GenMessage; -/** - * @generated from message agent.v1.InvocationContext_SlackThread - */ -export type InvocationContext_SlackThread = Message<"agent.v1.InvocationContext_SlackThread"> & { - /** - * @generated from field: string thread = 1; - */ - thread: string; - /** - * @generated from field: optional string channel_name = 2; - */ - channelName?: string; - /** - * @generated from field: optional string channel_purpose = 3; - */ - channelPurpose?: string; - /** - * @generated from field: optional string channel_topic = 4; - */ - channelTopic?: string; -}; -/** - * Describes the message agent.v1.InvocationContext_SlackThread. - * Use `create(InvocationContext_SlackThreadSchema)` to create a new message. - */ -export declare const InvocationContext_SlackThreadSchema: GenMessage; -/** - * @generated from message agent.v1.InvocationContext_GithubPR - */ -export type InvocationContext_GithubPR = Message<"agent.v1.InvocationContext_GithubPR"> & { - /** - * @generated from field: string title = 1; - */ - title: string; - /** - * @generated from field: string description = 2; - */ - description: string; - /** - * @generated from field: string comments = 3; - */ - comments: string; - /** - * @generated from field: optional string ci_failures = 4; - */ - ciFailures?: string; -}; -/** - * Describes the message agent.v1.InvocationContext_GithubPR. - * Use `create(InvocationContext_GithubPRSchema)` to create a new message. - */ -export declare const InvocationContext_GithubPRSchema: GenMessage; -/** - * @generated from message agent.v1.InvocationContext_IdeState - */ -export type InvocationContext_IdeState = Message<"agent.v1.InvocationContext_IdeState"> & { - /** - * @generated from field: repeated agent.v1.InvocationContext_IdeState_File visible_files = 1; - */ - visibleFiles: InvocationContext_IdeState_File[]; - /** - * @generated from field: repeated agent.v1.InvocationContext_IdeState_File recently_viewed_files = 2; - */ - recentlyViewedFiles: InvocationContext_IdeState_File[]; - /** - * PRs currently being viewed in the review editor (if any) - * - * @generated from field: repeated agent.v1.InvocationContext_IdeState_ViewedPullRequest currently_viewed_prs = 3; - */ - currentlyViewedPrs: InvocationContext_IdeState_ViewedPullRequest[]; -}; -/** - * Describes the message agent.v1.InvocationContext_IdeState. - * Use `create(InvocationContext_IdeStateSchema)` to create a new message. - */ -export declare const InvocationContext_IdeStateSchema: GenMessage; -/** - * @generated from message agent.v1.InvocationContext_IdeState_File - */ -export type InvocationContext_IdeState_File = Message<"agent.v1.InvocationContext_IdeState_File"> & { - /** - * This is the full path - * - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: optional string relative_path = 2; - */ - relativePath?: string; - /** - * Present if file is currently focused - * - * @generated from field: optional agent.v1.InvocationContext_IdeState_File_CursorPosition cursor_position = 3; - */ - cursorPosition?: InvocationContext_IdeState_File_CursorPosition; - /** - * @generated from field: int32 total_lines = 4; - */ - totalLines: number; - /** - * Present for terminal files - * - * @generated from field: optional string active_command = 5; - */ - activeCommand?: string; -}; -/** - * Describes the message agent.v1.InvocationContext_IdeState_File. - * Use `create(InvocationContext_IdeState_FileSchema)` to create a new message. - */ -export declare const InvocationContext_IdeState_FileSchema: GenMessage; -/** - * @generated from message agent.v1.InvocationContext_IdeState_File_CursorPosition - */ -export type InvocationContext_IdeState_File_CursorPosition = Message<"agent.v1.InvocationContext_IdeState_File_CursorPosition"> & { - /** - * @generated from field: int32 line = 1; - */ - line: number; - /** - * @generated from field: string text = 2; - */ - text: string; -}; -/** - * Describes the message agent.v1.InvocationContext_IdeState_File_CursorPosition. - * Use `create(InvocationContext_IdeState_File_CursorPositionSchema)` to create a new message. - */ -export declare const InvocationContext_IdeState_File_CursorPositionSchema: GenMessage; -/** - * Information about a PR currently being viewed in a review editor - * - * @generated from message agent.v1.InvocationContext_IdeState_ViewedPullRequest - */ -export type InvocationContext_IdeState_ViewedPullRequest = Message<"agent.v1.InvocationContext_IdeState_ViewedPullRequest"> & { - /** - * @generated from field: int32 number = 1; - */ - number: number; - /** - * @generated from field: string url = 2; - */ - url: string; - /** - * @generated from field: optional string title = 3; - */ - title?: string; - /** - * Path to the folder containing PR details (diffs, metadata, etc.) - * - * @generated from field: optional string folder_path = 4; - */ - folderPath?: string; - /** - * Summary JSON containing file list and diff sizes (contents of summary.json) - * - * @generated from field: optional string summary_json = 5; - */ - summaryJson?: string; - /** - * PR description/body - * - * @generated from field: optional string description = 6; - */ - description?: string; -}; -/** - * Describes the message agent.v1.InvocationContext_IdeState_ViewedPullRequest. - * Use `create(InvocationContext_IdeState_ViewedPullRequestSchema)` to create a new message. - */ -export declare const InvocationContext_IdeState_ViewedPullRequestSchema: GenMessage; -/** - * @generated from message agent.v1.SetupVmEnvironmentArgs - */ -export type SetupVmEnvironmentArgs = Message<"agent.v1.SetupVmEnvironmentArgs"> & { - /** - * Command to install runtime dependencies (e.g., "npm install") - * - * @generated from field: string install_command = 2; - */ - installCommand: string; - /** - * @generated from field: string start_command = 3; - */ - startCommand: string; -}; -/** - * Describes the message agent.v1.SetupVmEnvironmentArgs. - * Use `create(SetupVmEnvironmentArgsSchema)` to create a new message. - */ -export declare const SetupVmEnvironmentArgsSchema: GenMessage; -/** - * Result of VM environment setup operations - * - * @generated from message agent.v1.SetupVmEnvironmentResult - */ -export type SetupVmEnvironmentResult = Message<"agent.v1.SetupVmEnvironmentResult"> & { - /** - * @generated from oneof agent.v1.SetupVmEnvironmentResult.result - */ - result: { - /** - * @generated from field: agent.v1.SetupVmEnvironmentSuccess success = 1; - */ - value: SetupVmEnvironmentSuccess; - case: "success"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.SetupVmEnvironmentResult. - * Use `create(SetupVmEnvironmentResultSchema)` to create a new message. - */ -export declare const SetupVmEnvironmentResultSchema: GenMessage; -/** - * Successful VM environment setup result - * - * @generated from message agent.v1.SetupVmEnvironmentSuccess - */ -export type SetupVmEnvironmentSuccess = Message<"agent.v1.SetupVmEnvironmentSuccess"> & {}; -/** - * Describes the message agent.v1.SetupVmEnvironmentSuccess. - * Use `create(SetupVmEnvironmentSuccessSchema)` to create a new message. - */ -export declare const SetupVmEnvironmentSuccessSchema: GenMessage; -/** - * Tool call structure for SetupVmEnvironment - * - * @generated from message agent.v1.SetupVmEnvironmentToolCall - */ -export type SetupVmEnvironmentToolCall = Message<"agent.v1.SetupVmEnvironmentToolCall"> & { - /** - * Arguments for the tool call - * - * @generated from field: agent.v1.SetupVmEnvironmentArgs args = 1; - */ - args?: SetupVmEnvironmentArgs; - /** - * Result of the tool call (populated after execution) - * - * @generated from field: agent.v1.SetupVmEnvironmentResult result = 2; - */ - result?: SetupVmEnvironmentResult; -}; -/** - * Describes the message agent.v1.SetupVmEnvironmentToolCall. - * Use `create(SetupVmEnvironmentToolCallSchema)` to create a new message. - */ -export declare const SetupVmEnvironmentToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ShellCommandParsingResult - */ -export type ShellCommandParsingResult = Message<"agent.v1.ShellCommandParsingResult"> & { - /** - * @generated from field: bool parsing_failed = 1; - */ - parsingFailed: boolean; - /** - * @generated from field: repeated agent.v1.ShellCommandParsingResult_ExecutableCommand executable_commands = 2; - */ - executableCommands: ShellCommandParsingResult_ExecutableCommand[]; - /** - * @generated from field: bool has_redirects = 3; - */ - hasRedirects: boolean; - /** - * @generated from field: bool has_command_substitution = 4; - */ - hasCommandSubstitution: boolean; -}; -/** - * Describes the message agent.v1.ShellCommandParsingResult. - * Use `create(ShellCommandParsingResultSchema)` to create a new message. - */ -export declare const ShellCommandParsingResultSchema: GenMessage; -/** - * @generated from message agent.v1.ShellCommandParsingResult_ExecutableCommandArg - */ -export type ShellCommandParsingResult_ExecutableCommandArg = Message<"agent.v1.ShellCommandParsingResult_ExecutableCommandArg"> & { - /** - * @generated from field: string type = 1; - */ - type: string; - /** - * @generated from field: string value = 2; - */ - value: string; -}; -/** - * Describes the message agent.v1.ShellCommandParsingResult_ExecutableCommandArg. - * Use `create(ShellCommandParsingResult_ExecutableCommandArgSchema)` to create a new message. - */ -export declare const ShellCommandParsingResult_ExecutableCommandArgSchema: GenMessage; -/** - * @generated from message agent.v1.ShellCommandParsingResult_ExecutableCommand - */ -export type ShellCommandParsingResult_ExecutableCommand = Message<"agent.v1.ShellCommandParsingResult_ExecutableCommand"> & { - /** - * @generated from field: string name = 1; - */ - name: string; - /** - * @generated from field: repeated agent.v1.ShellCommandParsingResult_ExecutableCommandArg args = 2; - */ - args: ShellCommandParsingResult_ExecutableCommandArg[]; - /** - * @generated from field: string full_text = 3; - */ - fullText: string; -}; -/** - * Describes the message agent.v1.ShellCommandParsingResult_ExecutableCommand. - * Use `create(ShellCommandParsingResult_ExecutableCommandSchema)` to create a new message. - */ -export declare const ShellCommandParsingResult_ExecutableCommandSchema: GenMessage; -/** - * @generated from message agent.v1.ShellArgs - */ -export type ShellArgs = Message<"agent.v1.ShellArgs"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: int32 timeout = 3; - */ - timeout: number; - /** - * @generated from field: string tool_call_id = 4; - */ - toolCallId: string; - /** - * @generated from field: repeated string simple_commands = 5; - */ - simpleCommands: string[]; - /** - * @generated from field: bool has_input_redirect = 6; - */ - hasInputRedirect: boolean; - /** - * @generated from field: bool has_output_redirect = 7; - */ - hasOutputRedirect: boolean; - /** - * Deprecated: use parsing_result instead @deprecated simpleCommands = []; Deprecated: use parsing_result instead @deprecated hasInputRedirect = false; Deprecated: use parsing_result instead @deprecated hasOutputRedirect = false; - * - * @generated from field: agent.v1.ShellCommandParsingResult parsing_result = 8; - */ - parsingResult?: ShellCommandParsingResult; - /** - * @generated from field: optional agent.v1.SandboxPolicy requested_sandbox_policy = 9; - */ - requestedSandboxPolicy?: SandboxPolicy; - /** - * If output size exceeds this threshold (in bytes), write to file instead of inline. If unset or 0, always use inline output. - * - * @generated from field: optional uint64 file_output_threshold_bytes = 10; - */ - fileOutputThresholdBytes?: bigint; - /** - * @generated from field: bool is_background = 11; - */ - isBackground: boolean; - /** - * @generated from field: bool skip_approval = 12; - */ - skipApproval: boolean; - /** - * @generated from field: int32 timeout_behavior = 13; - */ - timeoutBehavior: number; - /** - * Hard timeout: kill the command after this many ms, even if running in background - * - * @generated from field: optional int32 hard_timeout = 14; - */ - hardTimeout?: number; -}; -/** - * Describes the message agent.v1.ShellArgs. - * Use `create(ShellArgsSchema)` to create a new message. - */ -export declare const ShellArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ShellResult - */ -export type ShellResult = Message<"agent.v1.ShellResult"> & { - /** - * @generated from field: optional agent.v1.SandboxPolicy sandbox_policy = 101; - */ - sandboxPolicy?: SandboxPolicy; - /** - * Rendering is affected by this flag, pass forward from args. - * - * @generated from field: optional bool is_background = 102; - */ - isBackground?: boolean; - /** - * Rendering is affected by this flag, pass forward from args. - * - * @generated from field: optional string terminals_folder = 103; - */ - terminalsFolder?: string; - /** - * Process ID, used for backgrounded shells. - * - * @generated from field: optional uint32 pid = 104; - */ - pid?: number; - /** - * @generated from oneof agent.v1.ShellResult.result - */ - result: { - /** - * @generated from field: agent.v1.ShellSuccess success = 1; - */ - value: ShellSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ShellFailure failure = 2; - */ - value: ShellFailure; - case: "failure"; - } | { - /** - * @generated from field: agent.v1.ShellTimeout timeout = 3; - */ - value: ShellTimeout; - case: "timeout"; - } | { - /** - * @generated from field: agent.v1.ShellRejected rejected = 4; - */ - value: ShellRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.ShellSpawnError spawn_error = 5; - */ - value: ShellSpawnError; - case: "spawnError"; - } | { - /** - * @generated from field: agent.v1.ShellPermissionDenied permission_denied = 7; - */ - value: ShellPermissionDenied; - case: "permissionDenied"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ShellResult. - * Use `create(ShellResultSchema)` to create a new message. - */ -export declare const ShellResultSchema: GenMessage; -/** - * @generated from message agent.v1.ShellStreamStdout - */ -export type ShellStreamStdout = Message<"agent.v1.ShellStreamStdout"> & { - /** - * @generated from field: string data = 1; - */ - data: string; -}; -/** - * Describes the message agent.v1.ShellStreamStdout. - * Use `create(ShellStreamStdoutSchema)` to create a new message. - */ -export declare const ShellStreamStdoutSchema: GenMessage; -/** - * @generated from message agent.v1.ShellStreamStderr - */ -export type ShellStreamStderr = Message<"agent.v1.ShellStreamStderr"> & { - /** - * @generated from field: string data = 1; - */ - data: string; -}; -/** - * Describes the message agent.v1.ShellStreamStderr. - * Use `create(ShellStreamStderrSchema)` to create a new message. - */ -export declare const ShellStreamStderrSchema: GenMessage; -/** - * @generated from message agent.v1.ShellStreamExit - */ -export type ShellStreamExit = Message<"agent.v1.ShellStreamExit"> & { - /** - * @generated from field: uint32 code = 1; - */ - code: number; - /** - * @generated from field: string cwd = 2; - */ - cwd: string; - /** - * @generated from field: optional agent.v1.OutputLocation output_location = 3; - */ - outputLocation?: OutputLocation; - /** - * @generated from field: bool aborted = 4; - */ - aborted: boolean; - /** - * If aborted is true, this field indicates the reason for the abort - * - * @generated from field: optional int32 abort_reason = 5; - */ - abortReason?: number; -}; -/** - * Describes the message agent.v1.ShellStreamExit. - * Use `create(ShellStreamExitSchema)` to create a new message. - */ -export declare const ShellStreamExitSchema: GenMessage; -/** - * @generated from message agent.v1.ShellStreamStart - */ -export type ShellStreamStart = Message<"agent.v1.ShellStreamStart"> & { - /** - * @generated from field: optional agent.v1.SandboxPolicy sandbox_policy = 1; - */ - sandboxPolicy?: SandboxPolicy; -}; -/** - * Describes the message agent.v1.ShellStreamStart. - * Use `create(ShellStreamStartSchema)` to create a new message. - */ -export declare const ShellStreamStartSchema: GenMessage; -/** - * @generated from message agent.v1.ShellStreamBackgrounded - */ -export type ShellStreamBackgrounded = Message<"agent.v1.ShellStreamBackgrounded"> & { - /** - * @generated from field: uint32 shell_id = 1; - */ - shellId: number; - /** - * @generated from field: string command = 2; - */ - command: string; - /** - * @generated from field: string working_directory = 3; - */ - workingDirectory: string; - /** - * @generated from field: optional uint32 pid = 4; - */ - pid?: number; - /** - * The ms_to_wait value that was used for backgrounding, for display purposes - * - * @generated from field: optional int32 ms_to_wait = 5; - */ - msToWait?: number; -}; -/** - * Describes the message agent.v1.ShellStreamBackgrounded. - * Use `create(ShellStreamBackgroundedSchema)` to create a new message. - */ -export declare const ShellStreamBackgroundedSchema: GenMessage; -/** - * @generated from message agent.v1.ShellStream - */ -export type ShellStream = Message<"agent.v1.ShellStream"> & { - /** - * @generated from oneof agent.v1.ShellStream.event - */ - event: { - /** - * @generated from field: agent.v1.ShellStreamStdout stdout = 1; - */ - value: ShellStreamStdout; - case: "stdout"; - } | { - /** - * @generated from field: agent.v1.ShellStreamStderr stderr = 2; - */ - value: ShellStreamStderr; - case: "stderr"; - } | { - /** - * @generated from field: agent.v1.ShellStreamExit exit = 3; - */ - value: ShellStreamExit; - case: "exit"; - } | { - /** - * @generated from field: agent.v1.ShellStreamStart start = 4; - */ - value: ShellStreamStart; - case: "start"; - } | { - /** - * @generated from field: agent.v1.ShellRejected rejected = 5; - */ - value: ShellRejected; - case: "rejected"; - } | { - /** - * @generated from field: agent.v1.ShellPermissionDenied permission_denied = 6; - */ - value: ShellPermissionDenied; - case: "permissionDenied"; - } | { - /** - * @generated from field: agent.v1.ShellStreamBackgrounded backgrounded = 7; - */ - value: ShellStreamBackgrounded; - case: "backgrounded"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ShellStream. - * Use `create(ShellStreamSchema)` to create a new message. - */ -export declare const ShellStreamSchema: GenMessage; -/** - * @generated from message agent.v1.OutputLocation - */ -export type OutputLocation = Message<"agent.v1.OutputLocation"> & { - /** - * Absolute path to the output file - * - * @generated from field: string file_path = 1; - */ - filePath: string; - /** - * Size of the output in bytes - * - * @generated from field: int64 size_bytes = 2; - */ - sizeBytes: bigint; - /** - * Number of lines in the output - * - * @generated from field: int64 line_count = 3; - */ - lineCount: bigint; -}; -/** - * Describes the message agent.v1.OutputLocation. - * Use `create(OutputLocationSchema)` to create a new message. - */ -export declare const OutputLocationSchema: GenMessage; -/** - * @generated from message agent.v1.ShellSuccess - */ -export type ShellSuccess = Message<"agent.v1.ShellSuccess"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: int32 exit_code = 3; - */ - exitCode: number; - /** - * @generated from field: string signal = 4; - */ - signal: string; - /** - * Inline stdout - populated when write_output_to_file is false, empty when true - * - * @generated from field: string stdout = 5; - */ - stdout: string; - /** - * Inline stderr - populated when write_output_to_file is false, empty when true - * - * @generated from field: string stderr = 6; - */ - stderr: string; - /** - * @generated from field: int32 execution_time = 7; - */ - executionTime: number; - /** - * File-based output - populated when write_output_to_file is true (chronologically merged stdout+stderr) - * - * @generated from field: optional agent.v1.OutputLocation output_location = 8; - */ - outputLocation?: OutputLocation; - /** - * Used by background shell executor - * - * @generated from field: optional uint32 shell_id = 9; - */ - shellId?: number; - /** - * @generated from field: optional string interleaved_output = 10; - */ - interleavedOutput?: string; - /** - * Process ID, used for backgrounded shells - * - * @generated from field: optional uint32 pid = 11; - */ - pid?: number; - /** - * The ms_to_wait value used for backgrounding (for display in result) - * - * @generated from field: optional int32 ms_to_wait = 12; - */ - msToWait?: number; -}; -/** - * Describes the message agent.v1.ShellSuccess. - * Use `create(ShellSuccessSchema)` to create a new message. - */ -export declare const ShellSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ShellFailure - */ -export type ShellFailure = Message<"agent.v1.ShellFailure"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: int32 exit_code = 3; - */ - exitCode: number; - /** - * @generated from field: string signal = 4; - */ - signal: string; - /** - * Inline stdout - populated when write_output_to_file is false, empty when true - * - * @generated from field: string stdout = 5; - */ - stdout: string; - /** - * Inline stderr - populated when write_output_to_file is false, empty when true - * - * @generated from field: string stderr = 6; - */ - stderr: string; - /** - * @generated from field: int32 execution_time = 7; - */ - executionTime: number; - /** - * File-based output - populated when write_output_to_file is true (chronologically merged stdout+stderr) - * - * @generated from field: optional agent.v1.OutputLocation output_location = 8; - */ - outputLocation?: OutputLocation; - /** - * @generated from field: optional string interleaved_output = 9; - */ - interleavedOutput?: string; - /** - * If the command was aborted, this indicates the reason - * - * @generated from field: optional int32 abort_reason = 10; - */ - abortReason?: number; - /** - * Whether the command was aborted (by user or timeout) - * - * @generated from field: bool aborted = 11; - */ - aborted: boolean; -}; -/** - * Describes the message agent.v1.ShellFailure. - * Use `create(ShellFailureSchema)` to create a new message. - */ -export declare const ShellFailureSchema: GenMessage; -/** - * @generated from message agent.v1.ShellTimeout - */ -export type ShellTimeout = Message<"agent.v1.ShellTimeout"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: int32 timeout_ms = 3; - */ - timeoutMs: number; -}; -/** - * Describes the message agent.v1.ShellTimeout. - * Use `create(ShellTimeoutSchema)` to create a new message. - */ -export declare const ShellTimeoutSchema: GenMessage; -/** - * @generated from message agent.v1.ShellRejected - */ -export type ShellRejected = Message<"agent.v1.ShellRejected"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: string reason = 3; - */ - reason: string; - /** - * @generated from field: bool is_readonly = 4; - */ - isReadonly: boolean; -}; -/** - * Describes the message agent.v1.ShellRejected. - * Use `create(ShellRejectedSchema)` to create a new message. - */ -export declare const ShellRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.ShellPermissionDenied - */ -export type ShellPermissionDenied = Message<"agent.v1.ShellPermissionDenied"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: string error = 3; - */ - error: string; - /** - * @generated from field: bool is_readonly = 4; - */ - isReadonly: boolean; -}; -/** - * Describes the message agent.v1.ShellPermissionDenied. - * Use `create(ShellPermissionDeniedSchema)` to create a new message. - */ -export declare const ShellPermissionDeniedSchema: GenMessage; -/** - * @generated from message agent.v1.ShellSpawnError - */ -export type ShellSpawnError = Message<"agent.v1.ShellSpawnError"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: string working_directory = 2; - */ - workingDirectory: string; - /** - * @generated from field: string error = 3; - */ - error: string; -}; -/** - * Describes the message agent.v1.ShellSpawnError. - * Use `create(ShellSpawnErrorSchema)` to create a new message. - */ -export declare const ShellSpawnErrorSchema: GenMessage; -/** - * @generated from message agent.v1.ShellPartialResult - */ -export type ShellPartialResult = Message<"agent.v1.ShellPartialResult"> & { - /** - * @generated from field: string stdout_delta = 1; - */ - stdoutDelta: string; - /** - * @generated from field: string stderr_delta = 2; - */ - stderrDelta: string; -}; -/** - * Describes the message agent.v1.ShellPartialResult. - * Use `create(ShellPartialResultSchema)` to create a new message. - */ -export declare const ShellPartialResultSchema: GenMessage; -/** - * @generated from message agent.v1.ShellToolCall - */ -export type ShellToolCall = Message<"agent.v1.ShellToolCall"> & { - /** - * @generated from field: agent.v1.ShellArgs args = 1; - */ - args?: ShellArgs; - /** - * @generated from field: agent.v1.ShellResult result = 2; - */ - result?: ShellResult; -}; -/** - * Describes the message agent.v1.ShellToolCall. - * Use `create(ShellToolCallSchema)` to create a new message. - */ -export declare const ShellToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ShellToolCallStdoutDelta - */ -export type ShellToolCallStdoutDelta = Message<"agent.v1.ShellToolCallStdoutDelta"> & { - /** - * @generated from field: string content = 1; - */ - content: string; -}; -/** - * Describes the message agent.v1.ShellToolCallStdoutDelta. - * Use `create(ShellToolCallStdoutDeltaSchema)` to create a new message. - */ -export declare const ShellToolCallStdoutDeltaSchema: GenMessage; -/** - * @generated from message agent.v1.ShellToolCallStderrDelta - */ -export type ShellToolCallStderrDelta = Message<"agent.v1.ShellToolCallStderrDelta"> & { - /** - * @generated from field: string content = 1; - */ - content: string; -}; -/** - * Describes the message agent.v1.ShellToolCallStderrDelta. - * Use `create(ShellToolCallStderrDeltaSchema)` to create a new message. - */ -export declare const ShellToolCallStderrDeltaSchema: GenMessage; -/** - * @generated from message agent.v1.ShellToolCallDelta - */ -export type ShellToolCallDelta = Message<"agent.v1.ShellToolCallDelta"> & { - /** - * @generated from oneof agent.v1.ShellToolCallDelta.delta - */ - delta: { - /** - * @generated from field: agent.v1.ShellToolCallStdoutDelta stdout = 1; - */ - value: ShellToolCallStdoutDelta; - case: "stdout"; - } | { - /** - * @generated from field: agent.v1.ShellToolCallStderrDelta stderr = 2; - */ - value: ShellToolCallStderrDelta; - case: "stderr"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ShellToolCallDelta. - * Use `create(ShellToolCallDeltaSchema)` to create a new message. - */ -export declare const ShellToolCallDeltaSchema: GenMessage; -/** - * @generated from message agent.v1.SubagentType - */ -export type SubagentType = Message<"agent.v1.SubagentType"> & { - /** - * @generated from oneof agent.v1.SubagentType.type - */ - type: { - /** - * @generated from field: agent.v1.SubagentTypeUnspecified unspecified = 1; - */ - value: SubagentTypeUnspecified; - case: "unspecified"; - } | { - /** - * @generated from field: agent.v1.SubagentTypeComputerUse computer_use = 2; - */ - value: SubagentTypeComputerUse; - case: "computerUse"; - } | { - /** - * @generated from field: agent.v1.SubagentTypeCustom custom = 3; - */ - value: SubagentTypeCustom; - case: "custom"; - } | { - /** - * @generated from field: agent.v1.SubagentTypeExplore explore = 4; - */ - value: SubagentTypeExplore; - case: "explore"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.SubagentType. - * Use `create(SubagentTypeSchema)` to create a new message. - */ -export declare const SubagentTypeSchema: GenMessage; -/** - * Empty message for unspecified subagent type - * - * @generated from message agent.v1.SubagentTypeUnspecified - */ -export type SubagentTypeUnspecified = Message<"agent.v1.SubagentTypeUnspecified"> & {}; -/** - * Describes the message agent.v1.SubagentTypeUnspecified. - * Use `create(SubagentTypeUnspecifiedSchema)` to create a new message. - */ -export declare const SubagentTypeUnspecifiedSchema: GenMessage; -/** - * Empty message for computer use subagent type - * - * @generated from message agent.v1.SubagentTypeComputerUse - */ -export type SubagentTypeComputerUse = Message<"agent.v1.SubagentTypeComputerUse"> & {}; -/** - * Describes the message agent.v1.SubagentTypeComputerUse. - * Use `create(SubagentTypeComputerUseSchema)` to create a new message. - */ -export declare const SubagentTypeComputerUseSchema: GenMessage; -/** - * Empty message for explore subagent type (read-only codebase exploration) - * - * @generated from message agent.v1.SubagentTypeExplore - */ -export type SubagentTypeExplore = Message<"agent.v1.SubagentTypeExplore"> & {}; -/** - * Describes the message agent.v1.SubagentTypeExplore. - * Use `create(SubagentTypeExploreSchema)` to create a new message. - */ -export declare const SubagentTypeExploreSchema: GenMessage; -/** - * Custom subagent type with a name field - * - * @generated from message agent.v1.SubagentTypeCustom - */ -export type SubagentTypeCustom = Message<"agent.v1.SubagentTypeCustom"> & { - /** - * unique identifier of the custom subagent - * - * @generated from field: string name = 1; - */ - name: string; -}; -/** - * Describes the message agent.v1.SubagentTypeCustom. - * Use `create(SubagentTypeCustomSchema)` to create a new message. - */ -export declare const SubagentTypeCustomSchema: GenMessage; -/** - * Custom subagent definition loaded from local workspace configuration. - * - * @generated from message agent.v1.CustomSubagent - */ -export type CustomSubagent = Message<"agent.v1.CustomSubagent"> & { - /** - * absolute path to the markdown definition file - * - * @generated from field: string full_path = 1; - */ - fullPath: string; - /** - * unique identifier of the subagent - * - * @generated from field: string name = 2; - */ - name: string; - /** - * short summary of the agent's specialization - * - * @generated from field: string description = 3; - */ - description: string; - /** - * list of tool names the subagent can access - * - * @generated from field: repeated string tools = 4; - */ - tools: string[]; - /** - * preferred model (or "inherit" to use parent's model) - * - * @generated from field: string model = 5; - */ - model: string; - /** - * full prompt contents from the markdown file - * - * @generated from field: string prompt = 6; - */ - prompt: string; - /** - * default permission mode for subagent execution - * - * @generated from field: int32 permission_mode = 7; - */ - permissionMode: number; -}; -/** - * Describes the message agent.v1.CustomSubagent. - * Use `create(CustomSubagentSchema)` to create a new message. - */ -export declare const CustomSubagentSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeArgs - */ -export type SwitchModeArgs = Message<"agent.v1.SwitchModeArgs"> & { - /** - * The unified mode id to switch to (agent/chat/plan/spec/debug/triage) - * - * @generated from field: string target_mode_id = 1; - */ - targetModeId: string; - /** - * Optional explanation for why the mode switch is requested - * - * @generated from field: optional string explanation = 2; - */ - explanation?: string; - /** - * @generated from field: string tool_call_id = 3; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.SwitchModeArgs. - * Use `create(SwitchModeArgsSchema)` to create a new message. - */ -export declare const SwitchModeArgsSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeResult - */ -export type SwitchModeResult = Message<"agent.v1.SwitchModeResult"> & { - /** - * @generated from oneof agent.v1.SwitchModeResult.result - */ - result: { - /** - * @generated from field: agent.v1.SwitchModeSuccess success = 1; - */ - value: SwitchModeSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.SwitchModeError error = 2; - */ - value: SwitchModeError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.SwitchModeRejected rejected = 3; - */ - value: SwitchModeRejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.SwitchModeResult. - * Use `create(SwitchModeResultSchema)` to create a new message. - */ -export declare const SwitchModeResultSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeSuccess - */ -export type SwitchModeSuccess = Message<"agent.v1.SwitchModeSuccess"> & { - /** - * The mode we switched from - * - * @generated from field: string from_mode_id = 1; - */ - fromModeId: string; - /** - * The mode we switched to - * - * @generated from field: string to_mode_id = 2; - */ - toModeId: string; -}; -/** - * Describes the message agent.v1.SwitchModeSuccess. - * Use `create(SwitchModeSuccessSchema)` to create a new message. - */ -export declare const SwitchModeSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeError - */ -export type SwitchModeError = Message<"agent.v1.SwitchModeError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.SwitchModeError. - * Use `create(SwitchModeErrorSchema)` to create a new message. - */ -export declare const SwitchModeErrorSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeRejected - */ -export type SwitchModeRejected = Message<"agent.v1.SwitchModeRejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.SwitchModeRejected. - * Use `create(SwitchModeRejectedSchema)` to create a new message. - */ -export declare const SwitchModeRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeToolCall - */ -export type SwitchModeToolCall = Message<"agent.v1.SwitchModeToolCall"> & { - /** - * @generated from field: agent.v1.SwitchModeArgs args = 1; - */ - args?: SwitchModeArgs; - /** - * @generated from field: agent.v1.SwitchModeResult result = 2; - */ - result?: SwitchModeResult; -}; -/** - * Describes the message agent.v1.SwitchModeToolCall. - * Use `create(SwitchModeToolCallSchema)` to create a new message. - */ -export declare const SwitchModeToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeRequestQuery - */ -export type SwitchModeRequestQuery = Message<"agent.v1.SwitchModeRequestQuery"> & { - /** - * @generated from field: agent.v1.SwitchModeArgs args = 1; - */ - args?: SwitchModeArgs; -}; -/** - * Describes the message agent.v1.SwitchModeRequestQuery. - * Use `create(SwitchModeRequestQuerySchema)` to create a new message. - */ -export declare const SwitchModeRequestQuerySchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeRequestResponse - */ -export type SwitchModeRequestResponse = Message<"agent.v1.SwitchModeRequestResponse"> & { - /** - * @generated from oneof agent.v1.SwitchModeRequestResponse.result - */ - result: { - /** - * @generated from field: agent.v1.SwitchModeRequestResponse_Approved approved = 1; - */ - value: SwitchModeRequestResponse_Approved; - case: "approved"; - } | { - /** - * @generated from field: agent.v1.SwitchModeRequestResponse_Rejected rejected = 2; - */ - value: SwitchModeRequestResponse_Rejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.SwitchModeRequestResponse. - * Use `create(SwitchModeRequestResponseSchema)` to create a new message. - */ -export declare const SwitchModeRequestResponseSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeRequestResponse_Approved - */ -export type SwitchModeRequestResponse_Approved = Message<"agent.v1.SwitchModeRequestResponse_Approved"> & {}; -/** - * Describes the message agent.v1.SwitchModeRequestResponse_Approved. - * Use `create(SwitchModeRequestResponse_ApprovedSchema)` to create a new message. - */ -export declare const SwitchModeRequestResponse_ApprovedSchema: GenMessage; -/** - * @generated from message agent.v1.SwitchModeRequestResponse_Rejected - */ -export type SwitchModeRequestResponse_Rejected = Message<"agent.v1.SwitchModeRequestResponse_Rejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.SwitchModeRequestResponse_Rejected. - * Use `create(SwitchModeRequestResponse_RejectedSchema)` to create a new message. - */ -export declare const SwitchModeRequestResponse_RejectedSchema: GenMessage; -/** - * @generated from message agent.v1.TodoItem - */ -export type TodoItem = Message<"agent.v1.TodoItem"> & { - /** - * @generated from field: string id = 1; - */ - id: string; - /** - * @generated from field: string content = 2; - */ - content: string; - /** - * @generated from field: int32 status = 3; - */ - status: number; - /** - * @generated from field: int64 created_at = 4; - */ - createdAt: bigint; - /** - * @generated from field: int64 updated_at = 5; - */ - updatedAt: bigint; - /** - * IDs of other TODOs this depends on - * - * @generated from field: repeated string dependencies = 6; - */ - dependencies: string[]; -}; -/** - * Describes the message agent.v1.TodoItem. - * Use `create(TodoItemSchema)` to create a new message. - */ -export declare const TodoItemSchema: GenMessage; -/** - * UpdateTodos tool call - * - * @generated from message agent.v1.UpdateTodosToolCall - */ -export type UpdateTodosToolCall = Message<"agent.v1.UpdateTodosToolCall"> & { - /** - * @generated from field: agent.v1.UpdateTodosArgs args = 1; - */ - args?: UpdateTodosArgs; - /** - * @generated from field: agent.v1.UpdateTodosResult result = 2; - */ - result?: UpdateTodosResult; -}; -/** - * Describes the message agent.v1.UpdateTodosToolCall. - * Use `create(UpdateTodosToolCallSchema)` to create a new message. - */ -export declare const UpdateTodosToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.UpdateTodosArgs - */ -export type UpdateTodosArgs = Message<"agent.v1.UpdateTodosArgs"> & { - /** - * @generated from field: repeated agent.v1.TodoItem todos = 1; - */ - todos: TodoItem[]; - /** - * @generated from field: bool merge = 2; - */ - merge: boolean; -}; -/** - * Describes the message agent.v1.UpdateTodosArgs. - * Use `create(UpdateTodosArgsSchema)` to create a new message. - */ -export declare const UpdateTodosArgsSchema: GenMessage; -/** - * @generated from message agent.v1.UpdateTodosResult - */ -export type UpdateTodosResult = Message<"agent.v1.UpdateTodosResult"> & { - /** - * @generated from oneof agent.v1.UpdateTodosResult.result - */ - result: { - /** - * @generated from field: agent.v1.UpdateTodosSuccess success = 1; - */ - value: UpdateTodosSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.UpdateTodosError error = 2; - */ - value: UpdateTodosError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.UpdateTodosResult. - * Use `create(UpdateTodosResultSchema)` to create a new message. - */ -export declare const UpdateTodosResultSchema: GenMessage; -/** - * @generated from message agent.v1.UpdateTodosSuccess - */ -export type UpdateTodosSuccess = Message<"agent.v1.UpdateTodosSuccess"> & { - /** - * @generated from field: repeated agent.v1.TodoItem todos = 1; - */ - todos: TodoItem[]; - /** - * @generated from field: int32 total_count = 2; - */ - totalCount: number; - /** - * Whether this was a merge operation (needed for conditional rendering) - * - * @generated from field: bool was_merge = 3; - */ - wasMerge: boolean; -}; -/** - * Describes the message agent.v1.UpdateTodosSuccess. - * Use `create(UpdateTodosSuccessSchema)` to create a new message. - */ -export declare const UpdateTodosSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.UpdateTodosError - */ -export type UpdateTodosError = Message<"agent.v1.UpdateTodosError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.UpdateTodosError. - * Use `create(UpdateTodosErrorSchema)` to create a new message. - */ -export declare const UpdateTodosErrorSchema: GenMessage; -/** - * ReadTodos tool call - * - * @generated from message agent.v1.ReadTodosToolCall - */ -export type ReadTodosToolCall = Message<"agent.v1.ReadTodosToolCall"> & { - /** - * @generated from field: agent.v1.ReadTodosArgs args = 1; - */ - args?: ReadTodosArgs; - /** - * @generated from field: agent.v1.ReadTodosResult result = 2; - */ - result?: ReadTodosResult; -}; -/** - * Describes the message agent.v1.ReadTodosToolCall. - * Use `create(ReadTodosToolCallSchema)` to create a new message. - */ -export declare const ReadTodosToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.ReadTodosArgs - */ -export type ReadTodosArgs = Message<"agent.v1.ReadTodosArgs"> & { - /** - * Optional: filter by status - * - * @generated from field: repeated int32 status_filter = 1; - */ - statusFilter: number[]; - /** - * Optional: filter by IDs - * - * @generated from field: repeated string id_filter = 2; - */ - idFilter: string[]; -}; -/** - * Describes the message agent.v1.ReadTodosArgs. - * Use `create(ReadTodosArgsSchema)` to create a new message. - */ -export declare const ReadTodosArgsSchema: GenMessage; -/** - * @generated from message agent.v1.ReadTodosResult - */ -export type ReadTodosResult = Message<"agent.v1.ReadTodosResult"> & { - /** - * @generated from oneof agent.v1.ReadTodosResult.result - */ - result: { - /** - * @generated from field: agent.v1.ReadTodosSuccess success = 1; - */ - value: ReadTodosSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.ReadTodosError error = 2; - */ - value: ReadTodosError; - case: "error"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ReadTodosResult. - * Use `create(ReadTodosResultSchema)` to create a new message. - */ -export declare const ReadTodosResultSchema: GenMessage; -/** - * @generated from message agent.v1.ReadTodosSuccess - */ -export type ReadTodosSuccess = Message<"agent.v1.ReadTodosSuccess"> & { - /** - * @generated from field: repeated agent.v1.TodoItem todos = 1; - */ - todos: TodoItem[]; - /** - * @generated from field: int32 total_count = 2; - */ - totalCount: number; -}; -/** - * Describes the message agent.v1.ReadTodosSuccess. - * Use `create(ReadTodosSuccessSchema)` to create a new message. - */ -export declare const ReadTodosSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.ReadTodosError - */ -export type ReadTodosError = Message<"agent.v1.ReadTodosError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.ReadTodosError. - * Use `create(ReadTodosErrorSchema)` to create a new message. - */ -export declare const ReadTodosErrorSchema: GenMessage; -/** - * @generated from message agent.v1.Range - */ -export type Range = Message<"agent.v1.Range"> & { - /** - * @generated from field: agent.v1.Position start = 1; - */ - start?: Position; - /** - * @generated from field: agent.v1.Position end = 2; - */ - end?: Position; -}; -/** - * Describes the message agent.v1.Range. - * Use `create(RangeSchema)` to create a new message. - */ -export declare const RangeSchema: GenMessage; -/** - * @generated from message agent.v1.Position - */ -export type Position = Message<"agent.v1.Position"> & { - /** - * @generated from field: uint32 line = 1; - */ - line: number; - /** - * @generated from field: uint32 column = 2; - */ - column: number; -}; -/** - * Describes the message agent.v1.Position. - * Use `create(PositionSchema)` to create a new message. - */ -export declare const PositionSchema: GenMessage; -/** - * @generated from message agent.v1.Error - */ -export type Error = Message<"agent.v1.Error"> & { - /** - * @generated from field: string message = 1; - */ - message: string; -}; -/** - * Describes the message agent.v1.Error. - * Use `create(ErrorSchema)` to create a new message. - */ -export declare const ErrorSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchArgs - */ -export type WebSearchArgs = Message<"agent.v1.WebSearchArgs"> & { - /** - * @generated from field: string search_term = 1; - */ - searchTerm: string; - /** - * @generated from field: string tool_call_id = 2; - */ - toolCallId: string; -}; -/** - * Describes the message agent.v1.WebSearchArgs. - * Use `create(WebSearchArgsSchema)` to create a new message. - */ -export declare const WebSearchArgsSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchResult - */ -export type WebSearchResult = Message<"agent.v1.WebSearchResult"> & { - /** - * @generated from oneof agent.v1.WebSearchResult.result - */ - result: { - /** - * @generated from field: agent.v1.WebSearchSuccess success = 1; - */ - value: WebSearchSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.WebSearchError error = 2; - */ - value: WebSearchError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.WebSearchRejected rejected = 3; - */ - value: WebSearchRejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.WebSearchResult. - * Use `create(WebSearchResultSchema)` to create a new message. - */ -export declare const WebSearchResultSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchSuccess - */ -export type WebSearchSuccess = Message<"agent.v1.WebSearchSuccess"> & { - /** - * @generated from field: repeated agent.v1.WebSearchReference references = 1; - */ - references: WebSearchReference[]; -}; -/** - * Describes the message agent.v1.WebSearchSuccess. - * Use `create(WebSearchSuccessSchema)` to create a new message. - */ -export declare const WebSearchSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchError - */ -export type WebSearchError = Message<"agent.v1.WebSearchError"> & { - /** - * @generated from field: string error = 1; - */ - error: string; -}; -/** - * Describes the message agent.v1.WebSearchError. - * Use `create(WebSearchErrorSchema)` to create a new message. - */ -export declare const WebSearchErrorSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchRejected - */ -export type WebSearchRejected = Message<"agent.v1.WebSearchRejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.WebSearchRejected. - * Use `create(WebSearchRejectedSchema)` to create a new message. - */ -export declare const WebSearchRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchReference - */ -export type WebSearchReference = Message<"agent.v1.WebSearchReference"> & { - /** - * @generated from field: string title = 1; - */ - title: string; - /** - * @generated from field: string url = 2; - */ - url: string; - /** - * @generated from field: string chunk = 3; - */ - chunk: string; -}; -/** - * Describes the message agent.v1.WebSearchReference. - * Use `create(WebSearchReferenceSchema)` to create a new message. - */ -export declare const WebSearchReferenceSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchToolCall - */ -export type WebSearchToolCall = Message<"agent.v1.WebSearchToolCall"> & { - /** - * @generated from field: agent.v1.WebSearchArgs args = 1; - */ - args?: WebSearchArgs; - /** - * @generated from field: agent.v1.WebSearchResult result = 2; - */ - result?: WebSearchResult; -}; -/** - * Describes the message agent.v1.WebSearchToolCall. - * Use `create(WebSearchToolCallSchema)` to create a new message. - */ -export declare const WebSearchToolCallSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchRequestQuery - */ -export type WebSearchRequestQuery = Message<"agent.v1.WebSearchRequestQuery"> & { - /** - * @generated from field: agent.v1.WebSearchArgs args = 1; - */ - args?: WebSearchArgs; -}; -/** - * Describes the message agent.v1.WebSearchRequestQuery. - * Use `create(WebSearchRequestQuerySchema)` to create a new message. - */ -export declare const WebSearchRequestQuerySchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchRequestResponse - */ -export type WebSearchRequestResponse = Message<"agent.v1.WebSearchRequestResponse"> & { - /** - * @generated from oneof agent.v1.WebSearchRequestResponse.result - */ - result: { - /** - * @generated from field: agent.v1.WebSearchRequestResponse_Approved approved = 1; - */ - value: WebSearchRequestResponse_Approved; - case: "approved"; - } | { - /** - * @generated from field: agent.v1.WebSearchRequestResponse_Rejected rejected = 2; - */ - value: WebSearchRequestResponse_Rejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.WebSearchRequestResponse. - * Use `create(WebSearchRequestResponseSchema)` to create a new message. - */ -export declare const WebSearchRequestResponseSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchRequestResponse_Approved - */ -export type WebSearchRequestResponse_Approved = Message<"agent.v1.WebSearchRequestResponse_Approved"> & {}; -/** - * Describes the message agent.v1.WebSearchRequestResponse_Approved. - * Use `create(WebSearchRequestResponse_ApprovedSchema)` to create a new message. - */ -export declare const WebSearchRequestResponse_ApprovedSchema: GenMessage; -/** - * @generated from message agent.v1.WebSearchRequestResponse_Rejected - */ -export type WebSearchRequestResponse_Rejected = Message<"agent.v1.WebSearchRequestResponse_Rejected"> & { - /** - * @generated from field: string reason = 1; - */ - reason: string; -}; -/** - * Describes the message agent.v1.WebSearchRequestResponse_Rejected. - * Use `create(WebSearchRequestResponse_RejectedSchema)` to create a new message. - */ -export declare const WebSearchRequestResponse_RejectedSchema: GenMessage; -/** - * @generated from message agent.v1.WriteArgs - */ -export type WriteArgs = Message<"agent.v1.WriteArgs"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string file_text = 2; - */ - fileText: string; - /** - * @generated from field: string tool_call_id = 3; - */ - toolCallId: string; - /** - * @generated from field: bool return_file_content_after_write = 4; - */ - returnFileContentAfterWrite: boolean; - /** - * Raw binary data to write. When set, file_text is ignored and the bytes are written directly without any text processing (e.g., line ending normalization). - * - * @generated from field: bytes file_bytes = 5; - */ - fileBytes: Uint8Array; -}; -/** - * Describes the message agent.v1.WriteArgs. - * Use `create(WriteArgsSchema)` to create a new message. - */ -export declare const WriteArgsSchema: GenMessage; -/** - * @generated from message agent.v1.WriteResult - */ -export type WriteResult = Message<"agent.v1.WriteResult"> & { - /** - * @generated from oneof agent.v1.WriteResult.result - */ - result: { - /** - * @generated from field: agent.v1.WriteSuccess success = 1; - */ - value: WriteSuccess; - case: "success"; - } | { - /** - * @generated from field: agent.v1.WritePermissionDenied permission_denied = 3; - */ - value: WritePermissionDenied; - case: "permissionDenied"; - } | { - /** - * @generated from field: agent.v1.WriteNoSpace no_space = 4; - */ - value: WriteNoSpace; - case: "noSpace"; - } | { - /** - * @generated from field: agent.v1.WriteError error = 5; - */ - value: WriteError; - case: "error"; - } | { - /** - * @generated from field: agent.v1.WriteRejected rejected = 6; - */ - value: WriteRejected; - case: "rejected"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.WriteResult. - * Use `create(WriteResultSchema)` to create a new message. - */ -export declare const WriteResultSchema: GenMessage; -/** - * @generated from message agent.v1.WriteSuccess - */ -export type WriteSuccess = Message<"agent.v1.WriteSuccess"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: int32 lines_created = 2; - */ - linesCreated: number; - /** - * @generated from field: int32 file_size = 3; - */ - fileSize: number; - /** - * @generated from field: optional string file_content_after_write = 4; - */ - fileContentAfterWrite?: string; -}; -/** - * Describes the message agent.v1.WriteSuccess. - * Use `create(WriteSuccessSchema)` to create a new message. - */ -export declare const WriteSuccessSchema: GenMessage; -/** - * @generated from message agent.v1.WritePermissionDenied - */ -export type WritePermissionDenied = Message<"agent.v1.WritePermissionDenied"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string directory = 2; - */ - directory: string; - /** - * "create_directory" or "create_file" - * - * @generated from field: string operation = 3; - */ - operation: string; - /** - * @generated from field: string error = 4; - */ - error: string; - /** - * @generated from field: bool is_readonly = 5; - */ - isReadonly: boolean; -}; -/** - * Describes the message agent.v1.WritePermissionDenied. - * Use `create(WritePermissionDeniedSchema)` to create a new message. - */ -export declare const WritePermissionDeniedSchema: GenMessage; -/** - * @generated from message agent.v1.WriteNoSpace - */ -export type WriteNoSpace = Message<"agent.v1.WriteNoSpace"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.WriteNoSpace. - * Use `create(WriteNoSpaceSchema)` to create a new message. - */ -export declare const WriteNoSpaceSchema: GenMessage; -/** - * @generated from message agent.v1.WriteError - */ -export type WriteError = Message<"agent.v1.WriteError"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string error = 2; - */ - error: string; -}; -/** - * Describes the message agent.v1.WriteError. - * Use `create(WriteErrorSchema)` to create a new message. - */ -export declare const WriteErrorSchema: GenMessage; -/** - * @generated from message agent.v1.WriteRejected - */ -export type WriteRejected = Message<"agent.v1.WriteRejected"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string reason = 2; - */ - reason: string; -}; -/** - * Describes the message agent.v1.WriteRejected. - * Use `create(WriteRejectedSchema)` to create a new message. - */ -export declare const WriteRejectedSchema: GenMessage; -/** - * @generated from message agent.v1.BootstrapStatsigRequest - */ -export type BootstrapStatsigRequest = Message<"agent.v1.BootstrapStatsigRequest"> & { - /** - * When true, the server should evaluate gates as if dev/internal status is ignored. This is used by clients to simulate a prod user experience. - * - * @generated from field: optional bool ignore_dev_status = 1; - */ - ignoreDevStatus?: boolean; - /** - * @generated from field: optional int32 operating_system = 2; - */ - operatingSystem?: number; -}; -/** - * Describes the message agent.v1.BootstrapStatsigRequest. - * Use `create(BootstrapStatsigRequestSchema)` to create a new message. - */ -export declare const BootstrapStatsigRequestSchema: GenMessage; -/** - * @generated from message agent.v1.PingResponse - */ -export type PingResponse = Message<"agent.v1.PingResponse"> & {}; -/** - * Describes the message agent.v1.PingResponse. - * Use `create(PingResponseSchema)` to create a new message. - */ -export declare const PingResponseSchema: GenMessage; -/** - * @generated from message agent.v1.ExecRequest - */ -export type ExecRequest = Message<"agent.v1.ExecRequest"> & { - /** - * @generated from field: string command = 1; - */ - command: string; - /** - * @generated from field: optional string cwd = 2; - */ - cwd?: string; - /** - * @generated from field: repeated string args = 3; - */ - args: string[]; - /** - * @generated from field: map environment = 4; - */ - environment: { - [key: string]: string; - }; -}; -/** - * Describes the message agent.v1.ExecRequest. - * Use `create(ExecRequestSchema)` to create a new message. - */ -export declare const ExecRequestSchema: GenMessage; -/** - * @generated from message agent.v1.ExecResponse - */ -export type ExecResponse = Message<"agent.v1.ExecResponse"> & { - /** - * @generated from oneof agent.v1.ExecResponse.event - */ - event: { - /** - * @generated from field: agent.v1.StdoutEvent stdout_event = 1; - */ - value: StdoutEvent; - case: "stdoutEvent"; - } | { - /** - * @generated from field: agent.v1.StderrEvent stderr_event = 2; - */ - value: StderrEvent; - case: "stderrEvent"; - } | { - /** - * @generated from field: agent.v1.ExitEvent exit_event = 3; - */ - value: ExitEvent; - case: "exitEvent"; - } | { - case: undefined; - value?: undefined; - }; -}; -/** - * Describes the message agent.v1.ExecResponse. - * Use `create(ExecResponseSchema)` to create a new message. - */ -export declare const ExecResponseSchema: GenMessage; -/** - * @generated from message agent.v1.StdoutEvent - */ -export type StdoutEvent = Message<"agent.v1.StdoutEvent"> & { - /** - * @generated from field: string data = 1; - */ - data: string; -}; -/** - * Describes the message agent.v1.StdoutEvent. - * Use `create(StdoutEventSchema)` to create a new message. - */ -export declare const StdoutEventSchema: GenMessage; -/** - * @generated from message agent.v1.StderrEvent - */ -export type StderrEvent = Message<"agent.v1.StderrEvent"> & { - /** - * @generated from field: string data = 1; - */ - data: string; -}; -/** - * Describes the message agent.v1.StderrEvent. - * Use `create(StderrEventSchema)` to create a new message. - */ -export declare const StderrEventSchema: GenMessage; -/** - * @generated from message agent.v1.ExitEvent - */ -export type ExitEvent = Message<"agent.v1.ExitEvent"> & { - /** - * @generated from field: int32 exit_code = 1; - */ - exitCode: number; -}; -/** - * Describes the message agent.v1.ExitEvent. - * Use `create(ExitEventSchema)` to create a new message. - */ -export declare const ExitEventSchema: GenMessage; -/** - * @generated from message agent.v1.ReadTextFileRequest - */ -export type ReadTextFileRequest = Message<"agent.v1.ReadTextFileRequest"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.ReadTextFileRequest. - * Use `create(ReadTextFileRequestSchema)` to create a new message. - */ -export declare const ReadTextFileRequestSchema: GenMessage; -/** - * @generated from message agent.v1.ReadTextFileResponse - */ -export type ReadTextFileResponse = Message<"agent.v1.ReadTextFileResponse"> & { - /** - * @generated from field: string content = 1; - */ - content: string; -}; -/** - * Describes the message agent.v1.ReadTextFileResponse. - * Use `create(ReadTextFileResponseSchema)` to create a new message. - */ -export declare const ReadTextFileResponseSchema: GenMessage; -/** - * @generated from message agent.v1.WriteTextFileRequest - */ -export type WriteTextFileRequest = Message<"agent.v1.WriteTextFileRequest"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: string content = 2; - */ - content: string; -}; -/** - * Describes the message agent.v1.WriteTextFileRequest. - * Use `create(WriteTextFileRequestSchema)` to create a new message. - */ -export declare const WriteTextFileRequestSchema: GenMessage; -/** - * Empty response - success is implied by RPC completion - * - * @generated from message agent.v1.WriteTextFileResponse - */ -export type WriteTextFileResponse = Message<"agent.v1.WriteTextFileResponse"> & {}; -/** - * Describes the message agent.v1.WriteTextFileResponse. - * Use `create(WriteTextFileResponseSchema)` to create a new message. - */ -export declare const WriteTextFileResponseSchema: GenMessage; -/** - * @generated from message agent.v1.ReadBinaryFileRequest - */ -export type ReadBinaryFileRequest = Message<"agent.v1.ReadBinaryFileRequest"> & { - /** - * @generated from field: string path = 1; - */ - path: string; -}; -/** - * Describes the message agent.v1.ReadBinaryFileRequest. - * Use `create(ReadBinaryFileRequestSchema)` to create a new message. - */ -export declare const ReadBinaryFileRequestSchema: GenMessage; -/** - * @generated from message agent.v1.ReadBinaryFileResponse - */ -export type ReadBinaryFileResponse = Message<"agent.v1.ReadBinaryFileResponse"> & { - /** - * @generated from field: bytes content = 1; - */ - content: Uint8Array; -}; -/** - * Describes the message agent.v1.ReadBinaryFileResponse. - * Use `create(ReadBinaryFileResponseSchema)` to create a new message. - */ -export declare const ReadBinaryFileResponseSchema: GenMessage; -/** - * @generated from message agent.v1.WriteBinaryFileRequest - */ -export type WriteBinaryFileRequest = Message<"agent.v1.WriteBinaryFileRequest"> & { - /** - * @generated from field: string path = 1; - */ - path: string; - /** - * @generated from field: bytes content = 2; - */ - content: Uint8Array; -}; -/** - * Describes the message agent.v1.WriteBinaryFileRequest. - * Use `create(WriteBinaryFileRequestSchema)` to create a new message. - */ -export declare const WriteBinaryFileRequestSchema: GenMessage; -/** - * Empty response - success is implied by RPC completion - * - * @generated from message agent.v1.WriteBinaryFileResponse - */ -export type WriteBinaryFileResponse = Message<"agent.v1.WriteBinaryFileResponse"> & {}; -/** - * Describes the message agent.v1.WriteBinaryFileResponse. - * Use `create(WriteBinaryFileResponseSchema)` to create a new message. - */ -export declare const WriteBinaryFileResponseSchema: GenMessage; -/** - * @generated from message agent.v1.GetWorkspaceChangesHashRequest - */ -export type GetWorkspaceChangesHashRequest = Message<"agent.v1.GetWorkspaceChangesHashRequest"> & { - /** - * @generated from field: string root_path = 1; - */ - rootPath: string; - /** - * @generated from field: string base_ref = 2; - */ - baseRef: string; -}; -/** - * Describes the message agent.v1.GetWorkspaceChangesHashRequest. - * Use `create(GetWorkspaceChangesHashRequestSchema)` to create a new message. - */ -export declare const GetWorkspaceChangesHashRequestSchema: GenMessage; -/** - * @generated from message agent.v1.GetWorkspaceChangesHashResponse - */ -export type GetWorkspaceChangesHashResponse = Message<"agent.v1.GetWorkspaceChangesHashResponse"> & { - /** - * @generated from field: string hash = 1; - */ - hash: string; -}; -/** - * Describes the message agent.v1.GetWorkspaceChangesHashResponse. - * Use `create(GetWorkspaceChangesHashResponseSchema)` to create a new message. - */ -export declare const GetWorkspaceChangesHashResponseSchema: GenMessage; -/** - * @generated from message agent.v1.RefreshGithubAccessTokenRequest - */ -export type RefreshGithubAccessTokenRequest = Message<"agent.v1.RefreshGithubAccessTokenRequest"> & { - /** - * @generated from field: string github_access_token = 1; - */ - githubAccessToken: string; - /** - * e.g., "github.com", "gitlab.com", "gitlab.example.com" - * - * @generated from field: string hostname = 2; - */ - hostname: string; -}; -/** - * Describes the message agent.v1.RefreshGithubAccessTokenRequest. - * Use `create(RefreshGithubAccessTokenRequestSchema)` to create a new message. - */ -export declare const RefreshGithubAccessTokenRequestSchema: GenMessage; -/** - * Empty response - success is implied by RPC completion - * - * @generated from message agent.v1.RefreshGithubAccessTokenResponse - */ -export type RefreshGithubAccessTokenResponse = Message<"agent.v1.RefreshGithubAccessTokenResponse"> & {}; -/** - * Describes the message agent.v1.RefreshGithubAccessTokenResponse. - * Use `create(RefreshGithubAccessTokenResponseSchema)` to create a new message. - */ -export declare const RefreshGithubAccessTokenResponseSchema: GenMessage; -/** - * @generated from message agent.v1.WarmRemoteAccessServerRequest - */ -export type WarmRemoteAccessServerRequest = Message<"agent.v1.WarmRemoteAccessServerRequest"> & { - /** - * @generated from field: string commit = 1; - */ - commit: string; - /** - * @generated from field: int32 port = 2; - */ - port: number; - /** - * @generated from field: string connection_token = 3; - */ - connectionToken: string; -}; -/** - * Describes the message agent.v1.WarmRemoteAccessServerRequest. - * Use `create(WarmRemoteAccessServerRequestSchema)` to create a new message. - */ -export declare const WarmRemoteAccessServerRequestSchema: GenMessage; -/** - * Empty response - success is implied by RPC completion - * - * @generated from message agent.v1.WarmRemoteAccessServerResponse - */ -export type WarmRemoteAccessServerResponse = Message<"agent.v1.WarmRemoteAccessServerResponse"> & {}; -/** - * Describes the message agent.v1.WarmRemoteAccessServerResponse. - * Use `create(WarmRemoteAccessServerResponseSchema)` to create a new message. - */ -export declare const WarmRemoteAccessServerResponseSchema: GenMessage; -/** - * @generated from message agent.v1.ListArtifactsRequest - */ -export type ListArtifactsRequest = Message<"agent.v1.ListArtifactsRequest"> & {}; -/** - * Describes the message agent.v1.ListArtifactsRequest. - * Use `create(ListArtifactsRequestSchema)` to create a new message. - */ -export declare const ListArtifactsRequestSchema: GenMessage; -/** - * @generated from message agent.v1.ArtifactUploadMetadata - */ -export type ArtifactUploadMetadata = Message<"agent.v1.ArtifactUploadMetadata"> & { - /** - * @generated from field: string absolute_path = 1; - */ - absolutePath: string; - /** - * @generated from field: uint64 size_bytes = 2; - */ - sizeBytes: bigint; - /** - * @generated from field: int64 updated_at_unix_ms = 3; - */ - updatedAtUnixMs: bigint; - /** - * @generated from field: int32 status = 4; - */ - status: number; - /** - * @generated from field: uint64 bytes_uploaded = 5; - */ - bytesUploaded: bigint; - /** - * @generated from field: string last_error = 6; - */ - lastError: string; - /** - * @generated from field: uint32 upload_attempts = 7; - */ - uploadAttempts: number; - /** - * @generated from field: int64 last_started_at_unix_ms = 8; - */ - lastStartedAtUnixMs: bigint; - /** - * @generated from field: int64 last_finished_at_unix_ms = 9; - */ - lastFinishedAtUnixMs: bigint; - /** - * @generated from field: string upload_id = 10; - */ - uploadId: string; -}; -/** - * Describes the message agent.v1.ArtifactUploadMetadata. - * Use `create(ArtifactUploadMetadataSchema)` to create a new message. - */ -export declare const ArtifactUploadMetadataSchema: GenMessage; -/** - * @generated from message agent.v1.ListArtifactsResponse - */ -export type ListArtifactsResponse = Message<"agent.v1.ListArtifactsResponse"> & { - /** - * @generated from field: repeated agent.v1.ArtifactUploadMetadata artifacts = 1; - */ - artifacts: ArtifactUploadMetadata[]; -}; -/** - * Describes the message agent.v1.ListArtifactsResponse. - * Use `create(ListArtifactsResponseSchema)` to create a new message. - */ -export declare const ListArtifactsResponseSchema: GenMessage; -/** - * @generated from message agent.v1.UploadArtifactsRequest - */ -export type UploadArtifactsRequest = Message<"agent.v1.UploadArtifactsRequest"> & { - /** - * @generated from field: repeated agent.v1.ArtifactUploadInstruction uploads = 1; - */ - uploads: ArtifactUploadInstruction[]; -}; -/** - * Describes the message agent.v1.UploadArtifactsRequest. - * Use `create(UploadArtifactsRequestSchema)` to create a new message. - */ -export declare const UploadArtifactsRequestSchema: GenMessage; -/** - * @generated from message agent.v1.ArtifactUploadInstruction - */ -export type ArtifactUploadInstruction = Message<"agent.v1.ArtifactUploadInstruction"> & { - /** - * @generated from field: string absolute_path = 1; - */ - absolutePath: string; - /** - * @generated from field: string upload_url = 2; - */ - uploadUrl: string; - /** - * @generated from field: string method = 3; - */ - method: string; - /** - * @generated from field: map headers = 4; - */ - headers: { - [key: string]: string; - }; - /** - * @generated from field: optional string content_type = 5; - */ - contentType?: string; - /** - * @generated from field: optional string slack_upload_url = 6; - */ - slackUploadUrl?: string; - /** - * @generated from field: optional string slack_file_id = 7; - */ - slackFileId?: string; -}; -/** - * Describes the message agent.v1.ArtifactUploadInstruction. - * Use `create(ArtifactUploadInstructionSchema)` to create a new message. - */ -export declare const ArtifactUploadInstructionSchema: GenMessage; -/** - * @generated from message agent.v1.ArtifactUploadDispatchResult - */ -export type ArtifactUploadDispatchResult = Message<"agent.v1.ArtifactUploadDispatchResult"> & { - /** - * @generated from field: string absolute_path = 1; - */ - absolutePath: string; - /** - * @generated from field: int32 status = 2; - */ - status: number; - /** - * @generated from field: string message = 3; - */ - message: string; - /** - * @generated from field: optional string slack_file_id = 4; - */ - slackFileId?: string; -}; -/** - * Describes the message agent.v1.ArtifactUploadDispatchResult. - * Use `create(ArtifactUploadDispatchResultSchema)` to create a new message. - */ -export declare const ArtifactUploadDispatchResultSchema: GenMessage; -/** - * @generated from message agent.v1.UploadArtifactsResponse - */ -export type UploadArtifactsResponse = Message<"agent.v1.UploadArtifactsResponse"> & { - /** - * @generated from field: repeated agent.v1.ArtifactUploadDispatchResult results = 1; - */ - results: ArtifactUploadDispatchResult[]; -}; -/** - * Describes the message agent.v1.UploadArtifactsResponse. - * Use `create(UploadArtifactsResponseSchema)` to create a new message. - */ -export declare const UploadArtifactsResponseSchema: GenMessage; -/** - * @generated from message agent.v1.GetMcpRefreshTokensRequest - */ -export type GetMcpRefreshTokensRequest = Message<"agent.v1.GetMcpRefreshTokensRequest"> & {}; -/** - * Describes the message agent.v1.GetMcpRefreshTokensRequest. - * Use `create(GetMcpRefreshTokensRequestSchema)` to create a new message. - */ -export declare const GetMcpRefreshTokensRequestSchema: GenMessage; -/** - * @generated from message agent.v1.GetMcpRefreshTokensResponse - */ -export type GetMcpRefreshTokensResponse = Message<"agent.v1.GetMcpRefreshTokensResponse"> & { - /** - * Map from server URL to refresh token - * - * @generated from field: map refresh_tokens = 1; - */ - refreshTokens: { - [key: string]: string; - }; -}; -/** - * Describes the message agent.v1.GetMcpRefreshTokensResponse. - * Use `create(GetMcpRefreshTokensResponseSchema)` to create a new message. - */ -export declare const GetMcpRefreshTokensResponseSchema: GenMessage; -/** - * @generated from message agent.v1.UpdateEnvironmentVariablesRequest - */ -export type UpdateEnvironmentVariablesRequest = Message<"agent.v1.UpdateEnvironmentVariablesRequest"> & { - /** - * Environment variables to manage (plaintext values). - * - * @generated from field: map env = 1; - */ - env: { - [key: string]: string; - }; - /** - * If true, unset previously-managed keys that are not present in `env`. - * - * @generated from field: bool replace = 2; - */ - replace: boolean; -}; -/** - * Describes the message agent.v1.UpdateEnvironmentVariablesRequest. - * Use `create(UpdateEnvironmentVariablesRequestSchema)` to create a new message. - */ -export declare const UpdateEnvironmentVariablesRequestSchema: GenMessage; -/** - * @generated from message agent.v1.UpdateEnvironmentVariablesResponse - */ -export type UpdateEnvironmentVariablesResponse = Message<"agent.v1.UpdateEnvironmentVariablesResponse"> & { - /** - * @generated from field: uint32 applied = 1; - */ - applied: number; - /** - * @generated from field: uint32 removed = 2; - */ - removed: number; -}; -/** - * Describes the message agent.v1.UpdateEnvironmentVariablesResponse. - * Use `create(UpdateEnvironmentVariablesResponseSchema)` to create a new message. - */ -export declare const UpdateEnvironmentVariablesResponseSchema: GenMessage; -/** - * Check if an error is caused by the client disconnecting (e.g., due to timeout or abort). This includes errors like ERR_STREAM_DESTROYED which occur when the HTTP response stream is closed by the client while the server is still writing to it. function isClientDisconnectError(error) { if (!(error instanceof Error)) { return false; const code = error.code; return (code === "ERR_STREAM_DESTROYED" || code === "ERR_STREAM_PREMATURE_CLOSE" || code === "ECONNRESET" || code === "EPIPE"); ;// ../proto/dist/generated/aiserver/v1/mcp_pb.js // @ts-nocheck - * - * @generated from message agent.v1.McpOAuthStoredData - */ -export type McpOAuthStoredData = Message<"agent.v1.McpOAuthStoredData"> & { - /** - * @generated from field: string refresh_token = 1; - */ - refreshToken: string; - /** - * @generated from field: string client_id = 2; - */ - clientId: string; - /** - * @generated from field: optional string client_secret = 3; - */ - clientSecret?: string; - /** - * @generated from field: repeated string redirect_uris = 4; - */ - redirectUris: string[]; -}; -/** - * Describes the message agent.v1.McpOAuthStoredData. - * Use `create(McpOAuthStoredDataSchema)` to create a new message. - */ -export declare const McpOAuthStoredDataSchema: GenMessage; -/** - * @generated from message agent.v1.Frame - */ -export type Frame = Message<"agent.v1.Frame"> & { - /** - * Correlation ID - * - * @generated from field: string id = 1; - */ - id: string; - /** - * RPC method (e.g., "/agent.v1.ControlService/Ping") - * - * @generated from field: string method = 2; - */ - method: string; - /** - * Serialized payload - * - * @generated from field: bytes data = 3; - */ - data: Uint8Array; - /** - * @generated from field: int32 kind = 4; - */ - kind: number; - /** - * Error message (kind == ERROR) - * - * @generated from field: string error = 5; - */ - error: string; -}; -/** - * Describes the message agent.v1.Frame. - * Use `create(FrameSchema)` to create a new message. - */ -export declare const FrameSchema: GenMessage; -/** - * var Frame_Kind; (function (Frame_Kind) { Frame_Kind[Frame_Kind["UNSPECIFIED"] = 0] = "UNSPECIFIED"; Frame_Kind[Frame_Kind["REQUEST"] = 1] = "REQUEST"; Frame_Kind[Frame_Kind["RESPONSE"] = 2] = "RESPONSE"; Frame_Kind[Frame_Kind["ERROR"] = 3] = "ERROR"; })(Frame_Kind || (Frame_Kind = {})); // Retrieve enum metadata with: proto3.getEnumType(Frame_Kind) proto3/* int32 *\/.C.util.setEnumType(Frame_Kind, "agent.v1.Frame.Kind", [ { no: 0, name: "KIND_UNSPECIFIED" }, { no: 1, name: "KIND_REQUEST" }, { no: 2, name: "KIND_RESPONSE" }, { no: 3, name: "KIND_ERROR" }, ]); - * - * @generated from message agent.v1.Empty - */ -export type Empty = Message<"agent.v1.Empty"> & {}; -/** - * Describes the message agent.v1.Empty. - * Use `create(EmptySchema)` to create a new message. - */ -export declare const EmptySchema: GenMessage; -/** - * @generated from message agent.v1.BidiRequestId - */ -export type BidiRequestId = Message<"agent.v1.BidiRequestId"> & { - /** - * @generated from field: string request_id = 1; - */ - requestId: string; -}; -/** - * Describes the message agent.v1.BidiRequestId. - * Use `create(BidiRequestIdSchema)` to create a new message. - */ -export declare const BidiRequestIdSchema: GenMessage; -/** - * @generated from enum agent.v1.AppliedAgentChange_ChangeType - */ -export declare enum AppliedAgentChange_ChangeType { - /** - * @generated from enum value: CHANGE_TYPE_UNSPECIFIED = 0; - */ - CHANGE_TYPE_UNSPECIFIED = 0, - /** - * @generated from enum value: CHANGE_TYPE_CREATED = 1; - */ - CHANGE_TYPE_CREATED = 1, - /** - * @generated from enum value: CHANGE_TYPE_MODIFIED = 2; - */ - CHANGE_TYPE_MODIFIED = 2, - /** - * @generated from enum value: CHANGE_TYPE_DELETED = 3; - */ - CHANGE_TYPE_DELETED = 3 -} -/** - * Describes the enum agent.v1.AppliedAgentChange_ChangeType. - */ -export declare const AppliedAgentChange_ChangeTypeSchema: GenEnum; -/** - * @generated from enum agent.v1.MouseButton - */ -export declare enum MouseButton { - /** - * @generated from enum value: MOUSE_BUTTON_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: MOUSE_BUTTON_LEFT = 1; - */ - LEFT = 1, - /** - * @generated from enum value: MOUSE_BUTTON_RIGHT = 2; - */ - RIGHT = 2, - /** - * @generated from enum value: MOUSE_BUTTON_MIDDLE = 3; - */ - MIDDLE = 3, - /** - * @generated from enum value: MOUSE_BUTTON_BACK = 4; - */ - BACK = 4, - /** - * @generated from enum value: MOUSE_BUTTON_FORWARD = 5; - */ - FORWARD = 5 -} -/** - * Describes the enum agent.v1.MouseButton. - */ -export declare const MouseButtonSchema: GenEnum; -/** - * @generated from enum agent.v1.ScrollDirection - */ -export declare enum ScrollDirection { - /** - * @generated from enum value: SCROLL_DIRECTION_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: SCROLL_DIRECTION_UP = 1; - */ - UP = 1, - /** - * @generated from enum value: SCROLL_DIRECTION_DOWN = 2; - */ - DOWN = 2, - /** - * @generated from enum value: SCROLL_DIRECTION_LEFT = 3; - */ - LEFT = 3, - /** - * @generated from enum value: SCROLL_DIRECTION_RIGHT = 4; - */ - RIGHT = 4 -} -/** - * Describes the enum agent.v1.ScrollDirection. - */ -export declare const ScrollDirectionSchema: GenEnum; -/** - * @generated from enum agent.v1.CursorRuleSource - */ -export declare enum CursorRuleSource { - /** - * @generated from enum value: CURSOR_RULE_SOURCE_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: CURSOR_RULE_SOURCE_TEAM = 1; - */ - TEAM = 1, - /** - * @generated from enum value: CURSOR_RULE_SOURCE_USER = 2; - */ - USER = 2 -} -/** - * Describes the enum agent.v1.CursorRuleSource. - */ -export declare const CursorRuleSourceSchema: GenEnum; -/** - * @generated from enum agent.v1.DiagnosticSeverity - */ -export declare enum DiagnosticSeverity { - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_ERROR = 1; - */ - ERROR = 1, - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_WARNING = 2; - */ - WARNING = 2, - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_INFORMATION = 3; - */ - INFORMATION = 3, - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_HINT = 4; - */ - HINT = 4 -} -/** - * Describes the enum agent.v1.DiagnosticSeverity. - */ -export declare const DiagnosticSeveritySchema: GenEnum; -/** - * @generated from enum agent.v1.RecordingMode - */ -export declare enum RecordingMode { - /** - * @generated from enum value: RECORDING_MODE_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: RECORDING_MODE_START_RECORDING = 1; - */ - START_RECORDING = 1, - /** - * @generated from enum value: RECORDING_MODE_SAVE_RECORDING = 2; - */ - SAVE_RECORDING = 2, - /** - * @generated from enum value: RECORDING_MODE_DISCARD_RECORDING = 3; - */ - DISCARD_RECORDING = 3 -} -/** - * Describes the enum agent.v1.RecordingMode. - */ -export declare const RecordingModeSchema: GenEnum; -/** - * @generated from enum agent.v1.RequestedFilePathRejectedReason - */ -export declare enum RequestedFilePathRejectedReason { - /** - * @generated from enum value: REQUESTED_FILE_PATH_REJECTED_REASON_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: REQUESTED_FILE_PATH_REJECTED_REASON_SLASHES_NOT_ALLOWED = 1; - */ - SLASHES_NOT_ALLOWED = 1 -} -/** - * Describes the enum agent.v1.RequestedFilePathRejectedReason. - */ -export declare const RequestedFilePathRejectedReasonSchema: GenEnum; -/** - * @generated from enum agent.v1.PackageType - */ -export declare enum PackageType { - /** - * @generated from enum value: PACKAGE_TYPE_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: PACKAGE_TYPE_CURSOR_PROJECT = 1; - */ - CURSOR_PROJECT = 1, - /** - * @generated from enum value: PACKAGE_TYPE_CURSOR_PERSONAL = 2; - */ - CURSOR_PERSONAL = 2, - /** - * @generated from enum value: PACKAGE_TYPE_CLAUDE_SKILL = 3; - */ - CLAUDE_SKILL = 3, - /** - * @generated from enum value: PACKAGE_TYPE_CLAUDE_PLUGIN = 4; - */ - CLAUDE_PLUGIN = 4 -} -/** - * Describes the enum agent.v1.PackageType. - */ -export declare const PackageTypeSchema: GenEnum; -/** - * @generated from enum agent.v1.SandboxPolicy_Type - */ -export declare enum SandboxPolicy_Type { - /** - * @generated from enum value: TYPE_UNSPECIFIED = 0; - */ - TYPE_UNSPECIFIED = 0, - /** - * @generated from enum value: TYPE_INSECURE_NONE = 1; - */ - TYPE_INSECURE_NONE = 1, - /** - * @generated from enum value: TYPE_WORKSPACE_READWRITE = 2; - */ - TYPE_WORKSPACE_READWRITE = 2, - /** - * @generated from enum value: TYPE_WORKSPACE_READONLY = 3; - */ - TYPE_WORKSPACE_READONLY = 3 -} -/** - * Describes the enum agent.v1.SandboxPolicy_Type. - */ -export declare const SandboxPolicy_TypeSchema: GenEnum; -/** - * @generated from enum agent.v1.TimeoutBehavior - */ -export declare enum TimeoutBehavior { - /** - * @generated from enum value: TIMEOUT_BEHAVIOR_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: TIMEOUT_BEHAVIOR_CANCEL = 1; - */ - CANCEL = 1, - /** - * @generated from enum value: TIMEOUT_BEHAVIOR_BACKGROUND = 2; - */ - BACKGROUND = 2 -} -/** - * Describes the enum agent.v1.TimeoutBehavior. - */ -export declare const TimeoutBehaviorSchema: GenEnum; -/** - * @generated from enum agent.v1.ShellAbortReason - */ -export declare enum ShellAbortReason { - /** - * @generated from enum value: SHELL_ABORT_REASON_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: SHELL_ABORT_REASON_USER_ABORT = 1; - */ - USER_ABORT = 1, - /** - * @generated from enum value: SHELL_ABORT_REASON_TIMEOUT = 2; - */ - TIMEOUT = 2 -} -/** - * Describes the enum agent.v1.ShellAbortReason. - */ -export declare const ShellAbortReasonSchema: GenEnum; -/** - * @generated from enum agent.v1.CustomSubagentPermissionMode - */ -export declare enum CustomSubagentPermissionMode { - /** - * @generated from enum value: CUSTOM_SUBAGENT_PERMISSION_MODE_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: CUSTOM_SUBAGENT_PERMISSION_MODE_DEFAULT = 1; - */ - DEFAULT = 1, - /** - * @generated from enum value: CUSTOM_SUBAGENT_PERMISSION_MODE_READONLY = 2; - */ - READONLY = 2 -} -/** - * Describes the enum agent.v1.CustomSubagentPermissionMode. - */ -export declare const CustomSubagentPermissionModeSchema: GenEnum; -/** - * @generated from enum agent.v1.TodoStatus - */ -export declare enum TodoStatus { - /** - * @generated from enum value: TODO_STATUS_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: TODO_STATUS_PENDING = 1; - */ - PENDING = 1, - /** - * @generated from enum value: TODO_STATUS_IN_PROGRESS = 2; - */ - IN_PROGRESS = 2, - /** - * @generated from enum value: TODO_STATUS_COMPLETED = 3; - */ - COMPLETED = 3, - /** - * @generated from enum value: TODO_STATUS_CANCELLED = 4; - */ - CANCELLED = 4 -} -/** - * Describes the enum agent.v1.TodoStatus. - */ -export declare const TodoStatusSchema: GenEnum; -/** - * @generated from enum agent.v1.ClientOS - */ -export declare enum ClientOS { - /** - * @generated from enum value: CLIENT_OS_UNSPECIFIED = 0; - */ - CLIENT_OS_UNSPECIFIED = 0, - /** - * @generated from enum value: CLIENT_OS_WINDOWS = 1; - */ - CLIENT_OS_WINDOWS = 1, - /** - * @generated from enum value: CLIENT_OS_MACOS = 2; - */ - CLIENT_OS_MACOS = 2, - /** - * @generated from enum value: CLIENT_OS_LINUX = 3; - */ - CLIENT_OS_LINUX = 3 -} -/** - * Describes the enum agent.v1.ClientOS. - */ -export declare const ClientOSSchema: GenEnum; -/** - * @generated from enum agent.v1.ArtifactUploadDispatchStatus - */ -export declare enum ArtifactUploadDispatchStatus { - /** - * @generated from enum value: ARTIFACT_UPLOAD_DISPATCH_STATUS_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: ARTIFACT_UPLOAD_DISPATCH_STATUS_ACCEPTED = 1; - */ - ACCEPTED = 1, - /** - * @generated from enum value: ARTIFACT_UPLOAD_DISPATCH_STATUS_REJECTED = 2; - */ - REJECTED = 2, - /** - * @generated from enum value: ARTIFACT_UPLOAD_DISPATCH_STATUS_SKIPPED_ALREADY_IN_PROGRESS = 3; - */ - SKIPPED_ALREADY_IN_PROGRESS = 3 -} -/** - * Describes the enum agent.v1.ArtifactUploadDispatchStatus. - */ -export declare const ArtifactUploadDispatchStatusSchema: GenEnum; -/** - * @generated from enum agent.v1.Frame_Kind - */ -export declare enum Frame_Kind { - /** - * @generated from enum value: KIND_UNSPECIFIED = 0; - */ - KIND_UNSPECIFIED = 0, - /** - * @generated from enum value: KIND_REQUEST = 1; - */ - KIND_REQUEST = 1, - /** - * @generated from enum value: KIND_RESPONSE = 2; - */ - KIND_RESPONSE = 2, - /** - * @generated from enum value: KIND_ERROR = 3; - */ - KIND_ERROR = 3 -} -/** - * Describes the enum agent.v1.Frame_Kind. - */ -export declare const Frame_KindSchema: GenEnum; -/** - * @generated from enum agent.v1.BugbotDeeplinkEventKind - */ -export declare enum BugbotDeeplinkEventKind { - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_CLICKED = 1; - */ - CLICKED = 1, - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_HANDLED_DIALOG_SHOWN = 2; - */ - HANDLED_DIALOG_SHOWN = 2, - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_HANDLED_CHAT_CREATED = 3; - */ - HANDLED_CHAT_CREATED = 3, - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_ERROR = 4; - */ - ERROR = 4, - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_HANDLED_FIX_IN_WEB = 5; - */ - HANDLED_FIX_IN_WEB = 5 -} -/** - * Describes the enum agent.v1.BugbotDeeplinkEventKind. - */ -export declare const BugbotDeeplinkEventKindSchema: GenEnum; -/** - * Agent Service with bidirectional streaming - * - * @generated from service agent.v1.AgentService - */ -export declare const AgentService: GenService<{ - /** - * @generated from rpc agent.v1.AgentService.Run - */ - run: { - methodKind: "unary"; - input: typeof AgentClientMessageSchema; - output: typeof AgentServerMessageSchema; - }; - /** - * @generated from rpc agent.v1.AgentService.RunSSE - */ - runSSE: { - methodKind: "unary"; - input: typeof BidiRequestIdSchema; - output: typeof AgentServerMessageSchema; - }; - /** - * Generate a very short, succinct agent name from the provided user message. - * - * @generated from rpc agent.v1.AgentService.NameAgent - */ - nameAgent: { - methodKind: "unary"; - input: typeof NameAgentRequestSchema; - output: typeof NameAgentResponseSchema; - }; - /** - * @generated from rpc agent.v1.AgentService.GetUsableModels - */ - getUsableModels: { - methodKind: "unary"; - input: typeof GetUsableModelsRequestSchema; - output: typeof GetUsableModelsResponseSchema; - }; - /** - * @generated from rpc agent.v1.AgentService.GetDefaultModelForCli - */ - getDefaultModelForCli: { - methodKind: "unary"; - input: typeof GetDefaultModelForCliRequestSchema; - output: typeof GetDefaultModelForCliResponseSchema; - }; - /** - * Internal endpoint: returns all allowed model intents for devs - * - * @generated from rpc agent.v1.AgentService.GetAllowedModelIntents - */ - getAllowedModelIntents: { - methodKind: "unary"; - input: typeof GetAllowedModelIntentsRequestSchema; - output: typeof GetAllowedModelIntentsResponseSchema; - }; -}>; -/** - * @generated from service agent.v1.ControlService - */ -export declare const ControlService: GenService<{ - /** - * Spawn - * File read / write - * - * @generated from rpc agent.v1.ControlService.ReadTextFile - */ - readTextFile: { - methodKind: "unary"; - input: typeof ReadTextFileRequestSchema; - output: typeof ReadTextFileResponseSchema; - }; - /** - * @generated from rpc agent.v1.ControlService.WriteTextFile - */ - writeTextFile: { - methodKind: "unary"; - input: typeof WriteTextFileRequestSchema; - output: typeof WriteTextFileResponseSchema; - }; - /** - * Binary file read / write - * - * @generated from rpc agent.v1.ControlService.ReadBinaryFile - */ - readBinaryFile: { - methodKind: "unary"; - input: typeof ReadBinaryFileRequestSchema; - output: typeof ReadBinaryFileResponseSchema; - }; - /** - * @generated from rpc agent.v1.ControlService.WriteBinaryFile - */ - writeBinaryFile: { - methodKind: "unary"; - input: typeof WriteBinaryFileRequestSchema; - output: typeof WriteBinaryFileResponseSchema; - }; - /** - * Git - * - * @generated from rpc agent.v1.ControlService.GetWorkspaceChangesHash - */ - getWorkspaceChangesHash: { - methodKind: "unary"; - input: typeof GetWorkspaceChangesHashRequestSchema; - output: typeof GetWorkspaceChangesHashResponseSchema; - }; - /** - * @generated from rpc agent.v1.ControlService.RefreshGithubAccessToken - */ - refreshGithubAccessToken: { - methodKind: "unary"; - input: typeof RefreshGithubAccessTokenRequestSchema; - output: typeof RefreshGithubAccessTokenResponseSchema; - }; - /** - * Remote access - * - * @generated from rpc agent.v1.ControlService.WarmRemoteAccessServer - */ - warmRemoteAccessServer: { - methodKind: "unary"; - input: typeof WarmRemoteAccessServerRequestSchema; - output: typeof WarmRemoteAccessServerResponseSchema; - }; - /** - * Artifact uploads - * - * @generated from rpc agent.v1.ControlService.ListArtifacts - */ - listArtifacts: { - methodKind: "unary"; - input: typeof ListArtifactsRequestSchema; - output: typeof ListArtifactsResponseSchema; - }; - /** - * @generated from rpc agent.v1.ControlService.UploadArtifacts - */ - uploadArtifacts: { - methodKind: "unary"; - input: typeof UploadArtifactsRequestSchema; - output: typeof UploadArtifactsResponseSchema; - }; - /** - * @generated from rpc agent.v1.ControlService.GetMcpRefreshTokens - */ - getMcpRefreshTokens: { - methodKind: "unary"; - input: typeof GetMcpRefreshTokensRequestSchema; - output: typeof GetMcpRefreshTokensResponseSchema; - }; - /** - * Update the exec-daemon's environment variables for subsequent process spawns. This does NOT affect already-running processes. - * - * @generated from rpc agent.v1.ControlService.UpdateEnvironmentVariables - */ - updateEnvironmentVariables: { - methodKind: "unary"; - input: typeof UpdateEnvironmentVariablesRequestSchema; - output: typeof UpdateEnvironmentVariablesResponseSchema; - }; -}>; -/** - * Agent Service with unary RPC - * - * @generated from service agent.v1.ExecService - */ -export declare const ExecService: GenService<{}>; -/** - * @generated from service agent.v1.PrivateWorkerBridgeExternalService - */ -export declare const PrivateWorkerBridgeExternalService: GenService<{ - /** - * @generated from rpc agent.v1.PrivateWorkerBridgeExternalService.Connect - */ - connect: { - methodKind: "unary"; - input: typeof FrameSchema; - output: typeof FrameSchema; - }; -}>; -/** - * LifecycleService is exposed by the bridge *client*, in addition to ExecService (tool calls) and ControlService (control operations "within the daemon"). It operates at a similar abstraction level as AnyrunService: it represents operations similar to creating a VM, checking out a repository, etc. - * - * @generated from service agent.v1.LifecycleService - */ -export declare const LifecycleService: GenService<{ - /** - * Resets a long-lived worker - * - * @generated from rpc agent.v1.LifecycleService.ResetInstance - */ - resetInstance: { - methodKind: "unary"; - input: typeof EmptySchema; - output: typeof EmptySchema; - }; - /** - * Asks worker to exit(0) so that a new worker can take his place - * - * @generated from rpc agent.v1.LifecycleService.RenewInstance - */ - renewInstance: { - methodKind: "unary"; - input: typeof EmptySchema; - output: typeof EmptySchema; - }; -}>; diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/proto/agent_pb.js b/references/upstream-plugins/opencode-cursor-oauth/dist/proto/agent_pb.js deleted file mode 100644 index 22410ab0..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/proto/agent_pb.js +++ /dev/null @@ -1,3250 +0,0 @@ -// @generated by protoc-gen-es v2.10.2 with parameter "target=ts" -// @generated from file agent.proto (package agent.v1, syntax proto3) -/* eslint-disable */ -import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; -/** - * Describes the file agent.proto. - */ -export const file_agent = -/*@__PURE__*/ -fileDesc("CgthZ2VudC5wcm90bxIIYWdlbnQudjEicgoOR2xvYlRvb2xSZXN1bHQSLAoHc3VjY2VzcxgBIAEoCzIZLmFnZW50LnYxLkdsb2JUb29sU3VjY2Vzc0gAEigKBWVycm9yGAIgASgLMhcuYWdlbnQudjEuR2xvYlRvb2xFcnJvckgAQggKBnJlc3VsdCIeCg1HbG9iVG9vbEVycm9yEg0KBWVycm9yGAEgASgJIokBCg9HbG9iVG9vbFN1Y2Nlc3MSDwoHcGF0dGVybhgBIAEoCRIMCgRwYXRoGAIgASgJEg0KBWZpbGVzGAMgAygJEhMKC3RvdGFsX2ZpbGVzGAQgASgFEhgKEGNsaWVudF90cnVuY2F0ZWQYBSABKAgSGQoRcmlwZ3JlcF90cnVuY2F0ZWQYBiABKAgiRgoMR2xvYlRvb2xDYWxsEgwKBGFyZ3MYASABKAwSKAoGcmVzdWx0GAIgASgLMhguYWdlbnQudjEuR2xvYlRvb2xSZXN1bHQibQoRUmVhZExpbnRzVG9vbENhbGwSKQoEYXJncxgBIAEoCzIbLmFnZW50LnYxLlJlYWRMaW50c1Rvb2xBcmdzEi0KBnJlc3VsdBgCIAEoCzIdLmFnZW50LnYxLlJlYWRMaW50c1Rvb2xSZXN1bHQiIgoRUmVhZExpbnRzVG9vbEFyZ3MSDQoFcGF0aHMYASADKAkigQEKE1JlYWRMaW50c1Rvb2xSZXN1bHQSMQoHc3VjY2VzcxgBIAEoCzIeLmFnZW50LnYxLlJlYWRMaW50c1Rvb2xTdWNjZXNzSAASLQoFZXJyb3IYAiABKAsyHC5hZ2VudC52MS5SZWFkTGludHNUb29sRXJyb3JIAEIICgZyZXN1bHQiewoUUmVhZExpbnRzVG9vbFN1Y2Nlc3MSMwoQZmlsZV9kaWFnbm9zdGljcxgBIAMoCzIZLmFnZW50LnYxLkZpbGVEaWFnbm9zdGljcxITCgt0b3RhbF9maWxlcxgCIAEoBRIZChF0b3RhbF9kaWFnbm9zdGljcxgDIAEoBSJpCg9GaWxlRGlhZ25vc3RpY3MSDAoEcGF0aBgBIAEoCRItCgtkaWFnbm9zdGljcxgCIAMoCzIYLmFnZW50LnYxLkRpYWdub3N0aWNJdGVtEhkKEWRpYWdub3N0aWNzX2NvdW50GAMgASgFIqsBCg5EaWFnbm9zdGljSXRlbRIuCghzZXZlcml0eRgBIAEoDjIcLmFnZW50LnYxLkRpYWdub3N0aWNTZXZlcml0eRIoCgVyYW5nZRgCIAEoCzIZLmFnZW50LnYxLkRpYWdub3N0aWNSYW5nZRIPCgdtZXNzYWdlGAMgASgJEg4KBnNvdXJjZRgEIAEoCRIMCgRjb2RlGAUgASgJEhAKCGlzX3N0YWxlGAYgASgIIlUKD0RpYWdub3N0aWNSYW5nZRIhCgVzdGFydBgBIAEoCzISLmFnZW50LnYxLlBvc2l0aW9uEh8KA2VuZBgCIAEoCzISLmFnZW50LnYxLlBvc2l0aW9uIisKElJlYWRMaW50c1Rvb2xFcnJvchIVCg1lcnJvcl9tZXNzYWdlGAEgASgJIh0KDE1jcFRvb2xFcnJvchINCgVlcnJvchgBIAEoCSLSAQoNTWNwVG9vbFJlc3VsdBInCgdzdWNjZXNzGAEgASgLMhQuYWdlbnQudjEuTWNwU3VjY2Vzc0gAEicKBWVycm9yGAIgASgLMhYuYWdlbnQudjEuTWNwVG9vbEVycm9ySAASKQoIcmVqZWN0ZWQYAyABKAsyFS5hZ2VudC52MS5NY3BSZWplY3RlZEgAEjoKEXBlcm1pc3Npb25fZGVuaWVkGAQgASgLMh0uYWdlbnQudjEuTWNwUGVybWlzc2lvbkRlbmllZEgAQggKBnJlc3VsdCJXCgtNY3BUb29sQ2FsbBIfCgRhcmdzGAEgASgLMhEuYWdlbnQudjEuTWNwQXJncxInCgZyZXN1bHQYAiABKAsyFy5hZ2VudC52MS5NY3BUb29sUmVzdWx0Im0KEVNlbVNlYXJjaFRvb2xDYWxsEikKBGFyZ3MYASABKAsyGy5hZ2VudC52MS5TZW1TZWFyY2hUb29sQXJncxItCgZyZXN1bHQYAiABKAsyHS5hZ2VudC52MS5TZW1TZWFyY2hUb29sUmVzdWx0IlMKEVNlbVNlYXJjaFRvb2xBcmdzEg0KBXF1ZXJ5GAEgASgJEhoKEnRhcmdldF9kaXJlY3RvcmllcxgCIAMoCRITCgtleHBsYW5hdGlvbhgDIAEoCSKBAQoTU2VtU2VhcmNoVG9vbFJlc3VsdBIxCgdzdWNjZXNzGAEgASgLMh4uYWdlbnQudjEuU2VtU2VhcmNoVG9vbFN1Y2Nlc3NIABItCgVlcnJvchgCIAEoCzIcLmFnZW50LnYxLlNlbVNlYXJjaFRvb2xFcnJvckgAQggKBnJlc3VsdCI9ChRTZW1TZWFyY2hUb29sU3VjY2VzcxIPCgdyZXN1bHRzGAEgASgJEhQKDGNvZGVfcmVzdWx0cxgCIAMoDCIrChJTZW1TZWFyY2hUb29sRXJyb3ISFQoNZXJyb3JfbWVzc2FnZRgBIAEoCSKCAQoYTGlzdE1jcFJlc291cmNlc1Rvb2xDYWxsEjAKBGFyZ3MYASABKAsyIi5hZ2VudC52MS5MaXN0TWNwUmVzb3VyY2VzRXhlY0FyZ3MSNAoGcmVzdWx0GAIgASgLMiQuYWdlbnQudjEuTGlzdE1jcFJlc291cmNlc0V4ZWNSZXN1bHQifwoXUmVhZE1jcFJlc291cmNlVG9vbENhbGwSLwoEYXJncxgBIAEoCzIhLmFnZW50LnYxLlJlYWRNY3BSZXNvdXJjZUV4ZWNBcmdzEjMKBnJlc3VsdBgCIAEoCzIjLmFnZW50LnYxLlJlYWRNY3BSZXNvdXJjZUV4ZWNSZXN1bHQiWQoNRmV0Y2hUb29sQ2FsbBIhCgRhcmdzGAEgASgLMhMuYWdlbnQudjEuRmV0Y2hBcmdzEiUKBnJlc3VsdBgCIAEoCzIVLmFnZW50LnYxLkZldGNoUmVzdWx0Im4KFFJlY29yZFNjcmVlblRvb2xDYWxsEigKBGFyZ3MYASABKAsyGi5hZ2VudC52MS5SZWNvcmRTY3JlZW5BcmdzEiwKBnJlc3VsdBgCIAEoCzIcLmFnZW50LnYxLlJlY29yZFNjcmVlblJlc3VsdCJ3ChdXcml0ZVNoZWxsU3RkaW5Ub29sQ2FsbBIrCgRhcmdzGAEgASgLMh0uYWdlbnQudjEuV3JpdGVTaGVsbFN0ZGluQXJncxIvCgZyZXN1bHQYAiABKAsyHy5hZ2VudC52MS5Xcml0ZVNoZWxsU3RkaW5SZXN1bHQisQEKC1JlZmxlY3RBcmdzEiIKGnVuZXhwZWN0ZWRfYWN0aW9uX291dGNvbWVzGAEgASgJEh0KFXJlbGV2YW50X2luc3RydWN0aW9ucxgCIAEoCRIZChFzY2VuYXJpb19hbmFseXNpcxgDIAEoCRIaChJjcml0aWNhbF9zeW50aGVzaXMYBCABKAkSEgoKbmV4dF9zdGVwcxgFIAEoCRIUCgx0b29sX2NhbGxfaWQYBiABKAkibwoNUmVmbGVjdFJlc3VsdBIrCgdzdWNjZXNzGAEgASgLMhguYWdlbnQudjEuUmVmbGVjdFN1Y2Nlc3NIABInCgVlcnJvchgCIAEoCzIWLmFnZW50LnYxLlJlZmxlY3RFcnJvckgAQggKBnJlc3VsdCIQCg5SZWZsZWN0U3VjY2VzcyIdCgxSZWZsZWN0RXJyb3ISDQoFZXJyb3IYASABKAkiXwoPUmVmbGVjdFRvb2xDYWxsEiMKBGFyZ3MYASABKAsyFS5hZ2VudC52MS5SZWZsZWN0QXJncxInCgZyZXN1bHQYAiABKAsyFy5hZ2VudC52MS5SZWZsZWN0UmVzdWx0IlkKF1N0YXJ0R3JpbmRFeGVjdXRpb25BcmdzEhgKC2V4cGxhbmF0aW9uGAEgASgJSACIAQESFAoMdG9vbF9jYWxsX2lkGAIgASgJQg4KDF9leHBsYW5hdGlvbiKTAQoZU3RhcnRHcmluZEV4ZWN1dGlvblJlc3VsdBI3CgdzdWNjZXNzGAEgASgLMiQuYWdlbnQudjEuU3RhcnRHcmluZEV4ZWN1dGlvblN1Y2Nlc3NIABIzCgVlcnJvchgCIAEoCzIiLmFnZW50LnYxLlN0YXJ0R3JpbmRFeGVjdXRpb25FcnJvckgAQggKBnJlc3VsdCIcChpTdGFydEdyaW5kRXhlY3V0aW9uU3VjY2VzcyIpChhTdGFydEdyaW5kRXhlY3V0aW9uRXJyb3ISDQoFZXJyb3IYASABKAkigwEKG1N0YXJ0R3JpbmRFeGVjdXRpb25Ub29sQ2FsbBIvCgRhcmdzGAEgASgLMiEuYWdlbnQudjEuU3RhcnRHcmluZEV4ZWN1dGlvbkFyZ3MSMwoGcmVzdWx0GAIgASgLMiMuYWdlbnQudjEuU3RhcnRHcmluZEV4ZWN1dGlvblJlc3VsdCJYChZTdGFydEdyaW5kUGxhbm5pbmdBcmdzEhgKC2V4cGxhbmF0aW9uGAEgASgJSACIAQESFAoMdG9vbF9jYWxsX2lkGAIgASgJQg4KDF9leHBsYW5hdGlvbiKQAQoYU3RhcnRHcmluZFBsYW5uaW5nUmVzdWx0EjYKB3N1Y2Nlc3MYASABKAsyIy5hZ2VudC52MS5TdGFydEdyaW5kUGxhbm5pbmdTdWNjZXNzSAASMgoFZXJyb3IYAiABKAsyIS5hZ2VudC52MS5TdGFydEdyaW5kUGxhbm5pbmdFcnJvckgAQggKBnJlc3VsdCIbChlTdGFydEdyaW5kUGxhbm5pbmdTdWNjZXNzIigKF1N0YXJ0R3JpbmRQbGFubmluZ0Vycm9yEg0KBWVycm9yGAEgASgJIoABChpTdGFydEdyaW5kUGxhbm5pbmdUb29sQ2FsbBIuCgRhcmdzGAEgASgLMiAuYWdlbnQudjEuU3RhcnRHcmluZFBsYW5uaW5nQXJncxIyCgZyZXN1bHQYAiABKAsyIi5hZ2VudC52MS5TdGFydEdyaW5kUGxhbm5pbmdSZXN1bHQinAEKCFRhc2tBcmdzEhMKC2Rlc2NyaXB0aW9uGAEgASgJEg4KBnByb21wdBgCIAEoCRItCg1zdWJhZ2VudF90eXBlGAMgASgLMhYuYWdlbnQudjEuU3ViYWdlbnRUeXBlEhIKBW1vZGVsGAQgASgJSACIAQESEwoGcmVzdW1lGAUgASgJSAGIAQFCCAoGX21vZGVsQgkKB19yZXN1bWUiqgEKC1Rhc2tTdWNjZXNzEjYKEmNvbnZlcnNhdGlvbl9zdGVwcxgBIAMoCzIaLmFnZW50LnYxLkNvbnZlcnNhdGlvblN0ZXASFQoIYWdlbnRfaWQYAiABKAlIAIgBARIVCg1pc19iYWNrZ3JvdW5kGAMgASgIEhgKC2R1cmF0aW9uX21zGAQgASgESAGIAQFCCwoJX2FnZW50X2lkQg4KDF9kdXJhdGlvbl9tcyIaCglUYXNrRXJyb3ISDQoFZXJyb3IYASABKAkiZgoKVGFza1Jlc3VsdBIoCgdzdWNjZXNzGAEgASgLMhUuYWdlbnQudjEuVGFza1N1Y2Nlc3NIABIkCgVlcnJvchgCIAEoCzITLmFnZW50LnYxLlRhc2tFcnJvckgAQggKBnJlc3VsdCJWCgxUYXNrVG9vbENhbGwSIAoEYXJncxgBIAEoCzISLmFnZW50LnYxLlRhc2tBcmdzEiQKBnJlc3VsdBgCIAEoCzIULmFnZW50LnYxLlRhc2tSZXN1bHQiTAoRVGFza1Rvb2xDYWxsRGVsdGESNwoSaW50ZXJhY3Rpb25fdXBkYXRlGAEgASgLMhsuYWdlbnQudjEuSW50ZXJhY3Rpb25VcGRhdGUiyw8KCFRvb2xDYWxsEjIKD3NoZWxsX3Rvb2xfY2FsbBgBIAEoCzIXLmFnZW50LnYxLlNoZWxsVG9vbENhbGxIABI0ChBkZWxldGVfdG9vbF9jYWxsGAMgASgLMhguYWdlbnQudjEuRGVsZXRlVG9vbENhbGxIABIwCg5nbG9iX3Rvb2xfY2FsbBgEIAEoCzIWLmFnZW50LnYxLkdsb2JUb29sQ2FsbEgAEjAKDmdyZXBfdG9vbF9jYWxsGAUgASgLMhYuYWdlbnQudjEuR3JlcFRvb2xDYWxsSAASMAoOcmVhZF90b29sX2NhbGwYCCABKAsyFi5hZ2VudC52MS5SZWFkVG9vbENhbGxIABI/ChZ1cGRhdGVfdG9kb3NfdG9vbF9jYWxsGAkgASgLMh0uYWdlbnQudjEuVXBkYXRlVG9kb3NUb29sQ2FsbEgAEjsKFHJlYWRfdG9kb3NfdG9vbF9jYWxsGAogASgLMhsuYWdlbnQudjEuUmVhZFRvZG9zVG9vbENhbGxIABIwCg5lZGl0X3Rvb2xfY2FsbBgMIAEoCzIWLmFnZW50LnYxLkVkaXRUb29sQ2FsbEgAEiwKDGxzX3Rvb2xfY2FsbBgNIAEoCzIULmFnZW50LnYxLkxzVG9vbENhbGxIABI7ChRyZWFkX2xpbnRzX3Rvb2xfY2FsbBgOIAEoCzIbLmFnZW50LnYxLlJlYWRMaW50c1Rvb2xDYWxsSAASLgoNbWNwX3Rvb2xfY2FsbBgPIAEoCzIVLmFnZW50LnYxLk1jcFRvb2xDYWxsSAASOwoUc2VtX3NlYXJjaF90b29sX2NhbGwYECABKAsyGy5hZ2VudC52MS5TZW1TZWFyY2hUb29sQ2FsbEgAEj0KFWNyZWF0ZV9wbGFuX3Rvb2xfY2FsbBgRIAEoCzIcLmFnZW50LnYxLkNyZWF0ZVBsYW5Ub29sQ2FsbEgAEjsKFHdlYl9zZWFyY2hfdG9vbF9jYWxsGBIgASgLMhsuYWdlbnQudjEuV2ViU2VhcmNoVG9vbENhbGxIABIwCg50YXNrX3Rvb2xfY2FsbBgTIAEoCzIWLmFnZW50LnYxLlRhc2tUb29sQ2FsbEgAEkoKHGxpc3RfbWNwX3Jlc291cmNlc190b29sX2NhbGwYFCABKAsyIi5hZ2VudC52MS5MaXN0TWNwUmVzb3VyY2VzVG9vbENhbGxIABJIChtyZWFkX21jcF9yZXNvdXJjZV90b29sX2NhbGwYFSABKAsyIS5hZ2VudC52MS5SZWFkTWNwUmVzb3VyY2VUb29sQ2FsbEgAEkYKGmFwcGx5X2FnZW50X2RpZmZfdG9vbF9jYWxsGBYgASgLMiAuYWdlbnQudjEuQXBwbHlBZ2VudERpZmZUb29sQ2FsbEgAEj8KFmFza19xdWVzdGlvbl90b29sX2NhbGwYFyABKAsyHS5hZ2VudC52MS5Bc2tRdWVzdGlvblRvb2xDYWxsSAASMgoPZmV0Y2hfdG9vbF9jYWxsGBggASgLMhcuYWdlbnQudjEuRmV0Y2hUb29sQ2FsbEgAEj0KFXN3aXRjaF9tb2RlX3Rvb2xfY2FsbBgZIAEoCzIcLmFnZW50LnYxLlN3aXRjaE1vZGVUb29sQ2FsbEgAEjsKFGV4YV9zZWFyY2hfdG9vbF9jYWxsGBogASgLMhsuYWdlbnQudjEuRXhhU2VhcmNoVG9vbENhbGxIABI5ChNleGFfZmV0Y2hfdG9vbF9jYWxsGBsgASgLMhouYWdlbnQudjEuRXhhRmV0Y2hUb29sQ2FsbEgAEkMKGGdlbmVyYXRlX2ltYWdlX3Rvb2xfY2FsbBgcIAEoCzIfLmFnZW50LnYxLkdlbmVyYXRlSW1hZ2VUb29sQ2FsbEgAEkEKF3JlY29yZF9zY3JlZW5fdG9vbF9jYWxsGB0gASgLMh4uYWdlbnQudjEuUmVjb3JkU2NyZWVuVG9vbENhbGxIABI/ChZjb21wdXRlcl91c2VfdG9vbF9jYWxsGB4gASgLMh0uYWdlbnQudjEuQ29tcHV0ZXJVc2VUb29sQ2FsbEgAEkgKG3dyaXRlX3NoZWxsX3N0ZGluX3Rvb2xfY2FsbBgfIAEoCzIhLmFnZW50LnYxLldyaXRlU2hlbGxTdGRpblRvb2xDYWxsSAASNgoRcmVmbGVjdF90b29sX2NhbGwYICABKAsyGS5hZ2VudC52MS5SZWZsZWN0VG9vbENhbGxIABJOCh5zZXR1cF92bV9lbnZpcm9ubWVudF90b29sX2NhbGwYISABKAsyJC5hZ2VudC52MS5TZXR1cFZtRW52aXJvbm1lbnRUb29sQ2FsbEgAEjoKE3RydW5jYXRlZF90b29sX2NhbGwYIiABKAsyGy5hZ2VudC52MS5UcnVuY2F0ZWRUb29sQ2FsbEgAElAKH3N0YXJ0X2dyaW5kX2V4ZWN1dGlvbl90b29sX2NhbGwYIyABKAsyJS5hZ2VudC52MS5TdGFydEdyaW5kRXhlY3V0aW9uVG9vbENhbGxIABJOCh5zdGFydF9ncmluZF9wbGFubmluZ190b29sX2NhbGwYJCABKAsyJC5hZ2VudC52MS5TdGFydEdyaW5kUGxhbm5pbmdUb29sQ2FsbEgAQgYKBHRvb2wiFwoVVHJ1bmNhdGVkVG9vbENhbGxBcmdzIhoKGFRydW5jYXRlZFRvb2xDYWxsU3VjY2VzcyInChZUcnVuY2F0ZWRUb29sQ2FsbEVycm9yEg0KBWVycm9yGAEgASgJIo0BChdUcnVuY2F0ZWRUb29sQ2FsbFJlc3VsdBI1CgdzdWNjZXNzGAEgASgLMiIuYWdlbnQudjEuVHJ1bmNhdGVkVG9vbENhbGxTdWNjZXNzSAASMQoFZXJyb3IYAiABKAsyIC5hZ2VudC52MS5UcnVuY2F0ZWRUb29sQ2FsbEVycm9ySABCCAoGcmVzdWx0IpQBChFUcnVuY2F0ZWRUb29sQ2FsbBIdChVvcmlnaW5hbF9zdGVwX2Jsb2JfaWQYASABKAwSLQoEYXJncxgCIAEoCzIfLmFnZW50LnYxLlRydW5jYXRlZFRvb2xDYWxsQXJncxIxCgZyZXN1bHQYAyABKAsyIS5hZ2VudC52MS5UcnVuY2F0ZWRUb29sQ2FsbFJlc3VsdCLRAQoNVG9vbENhbGxEZWx0YRI9ChVzaGVsbF90b29sX2NhbGxfZGVsdGEYASABKAsyHC5hZ2VudC52MS5TaGVsbFRvb2xDYWxsRGVsdGFIABI7ChR0YXNrX3Rvb2xfY2FsbF9kZWx0YRgCIAEoCzIbLmFnZW50LnYxLlRhc2tUb29sQ2FsbERlbHRhSAASOwoUZWRpdF90b29sX2NhbGxfZGVsdGEYAyABKAsyGy5hZ2VudC52MS5FZGl0VG9vbENhbGxEZWx0YUgAQgcKBWRlbHRhIrYBChBDb252ZXJzYXRpb25TdGVwEjcKEWFzc2lzdGFudF9tZXNzYWdlGAEgASgLMhouYWdlbnQudjEuQXNzaXN0YW50TWVzc2FnZUgAEicKCXRvb2xfY2FsbBgCIAEoCzISLmFnZW50LnYxLlRvb2xDYWxsSAASNQoQdGhpbmtpbmdfbWVzc2FnZRgDIAEoCzIZLmFnZW50LnYxLlRoaW5raW5nTWVzc2FnZUgAQgkKB21lc3NhZ2UigQQKEkNvbnZlcnNhdGlvbkFjdGlvbhI6ChN1c2VyX21lc3NhZ2VfYWN0aW9uGAEgASgLMhsuYWdlbnQudjEuVXNlck1lc3NhZ2VBY3Rpb25IABIvCg1yZXN1bWVfYWN0aW9uGAIgASgLMhYuYWdlbnQudjEuUmVzdW1lQWN0aW9uSAASLwoNY2FuY2VsX2FjdGlvbhgDIAEoCzIWLmFnZW50LnYxLkNhbmNlbEFjdGlvbkgAEjUKEHN1bW1hcml6ZV9hY3Rpb24YBCABKAsyGS5hZ2VudC52MS5TdW1tYXJpemVBY3Rpb25IABI8ChRzaGVsbF9jb21tYW5kX2FjdGlvbhgFIAEoCzIcLmFnZW50LnYxLlNoZWxsQ29tbWFuZEFjdGlvbkgAEjYKEXN0YXJ0X3BsYW5fYWN0aW9uGAYgASgLMhkuYWdlbnQudjEuU3RhcnRQbGFuQWN0aW9uSAASOgoTZXhlY3V0ZV9wbGFuX2FjdGlvbhgHIAEoCzIbLmFnZW50LnYxLkV4ZWN1dGVQbGFuQWN0aW9uSAASWgokYXN5bmNfYXNrX3F1ZXN0aW9uX2NvbXBsZXRpb25fYWN0aW9uGAggASgLMiouYWdlbnQudjEuQXN5bmNBc2tRdWVzdGlvbkNvbXBsZXRpb25BY3Rpb25IAEIICgZhY3Rpb24ivwEKEVVzZXJNZXNzYWdlQWN0aW9uEisKDHVzZXJfbWVzc2FnZRgBIAEoCzIVLmFnZW50LnYxLlVzZXJNZXNzYWdlEjEKD3JlcXVlc3RfY29udGV4dBgCIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0EikKHHNlbmRfdG9faW50ZXJhY3Rpb25fbGlzdGVuZXIYAyABKAhIAIgBAUIfCh1fc2VuZF90b19pbnRlcmFjdGlvbl9saXN0ZW5lciIOCgxDYW5jZWxBY3Rpb24iQQoMUmVzdW1lQWN0aW9uEjEKD3JlcXVlc3RfY29udGV4dBgCIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0IqABCiBBc3luY0Fza1F1ZXN0aW9uQ29tcGxldGlvbkFjdGlvbhIdChVvcmlnaW5hbF90b29sX2NhbGxfaWQYASABKAkSMAoNb3JpZ2luYWxfYXJncxgCIAEoCzIZLmFnZW50LnYxLkFza1F1ZXN0aW9uQXJncxIrCgZyZXN1bHQYAyABKAsyGy5hZ2VudC52MS5Bc2tRdWVzdGlvblJlc3VsdCIRCg9TdW1tYXJpemVBY3Rpb24iVAoSU2hlbGxDb21tYW5kQWN0aW9uEi0KDXNoZWxsX2NvbW1hbmQYASABKAsyFi5hZ2VudC52MS5TaGVsbENvbW1hbmQSDwoHZXhlY19pZBgCIAEoCSKCAQoPU3RhcnRQbGFuQWN0aW9uEisKDHVzZXJfbWVzc2FnZRgBIAEoCzIVLmFnZW50LnYxLlVzZXJNZXNzYWdlEjEKD3JlcXVlc3RfY29udGV4dBgCIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0Eg8KB2lzX3NwZWMYAyABKAgi4gEKEUV4ZWN1dGVQbGFuQWN0aW9uEjEKD3JlcXVlc3RfY29udGV4dBgBIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0Ei0KBHBsYW4YAiABKAsyGi5hZ2VudC52MS5Db252ZXJzYXRpb25QbGFuSACIAQESGgoNcGxhbl9maWxlX3VyaRgDIAEoCUgBiAEBEh4KEXBsYW5fZmlsZV9jb250ZW50GAQgASgJSAKIAQFCBwoFX3BsYW5CEAoOX3BsYW5fZmlsZV91cmlCFAoSX3BsYW5fZmlsZV9jb250ZW50IugCCgtVc2VyTWVzc2FnZRIMCgR0ZXh0GAEgASgJEhIKCm1lc3NhZ2VfaWQYAiABKAkSOAoQc2VsZWN0ZWRfY29udGV4dBgDIAEoCzIZLmFnZW50LnYxLlNlbGVjdGVkQ29udGV4dEgAiAEBEgwKBG1vZGUYBCABKAUSHQoQaXNfc2ltdWxhdGVkX21zZxgFIAEoCEgBiAEBEh8KEmJlc3Rfb2Zfbl9ncm91cF9pZBgGIAEoCUgCiAEBEigKG3RyeV91c2VfYmVzdF9vZl9uX3Byb21vdGlvbhgHIAEoCEgDiAEBEhYKCXJpY2hfdGV4dBgIIAEoCUgEiAEBQhMKEV9zZWxlY3RlZF9jb250ZXh0QhMKEV9pc19zaW11bGF0ZWRfbXNnQhUKE19iZXN0X29mX25fZ3JvdXBfaWRCHgocX3RyeV91c2VfYmVzdF9vZl9uX3Byb21vdGlvbkIMCgpfcmljaF90ZXh0IiAKEEFzc2lzdGFudE1lc3NhZ2USDAoEdGV4dBgBIAEoCSI0Cg9UaGlua2luZ01lc3NhZ2USDAoEdGV4dBgBIAEoCRITCgtkdXJhdGlvbl9tcxgCIAEoDSIfCgxTaGVsbENvbW1hbmQSDwoHY29tbWFuZBgBIAEoCSJACgtTaGVsbE91dHB1dBIOCgZzdGRvdXQYASABKAkSDgoGc3RkZXJyGAIgASgJEhEKCWV4aXRfY29kZRgDIAEoBSKiAQoQQ29udmVyc2F0aW9uVHVybhJCChdhZ2VudF9jb252ZXJzYXRpb25fdHVybhgBIAEoCzIfLmFnZW50LnYxLkFnZW50Q29udmVyc2F0aW9uVHVybkgAEkIKF3NoZWxsX2NvbnZlcnNhdGlvbl90dXJuGAIgASgLMh8uYWdlbnQudjEuU2hlbGxDb252ZXJzYXRpb25UdXJuSABCBgoEdHVybiIgChBDb252ZXJzYXRpb25QbGFuEgwKBHBsYW4YASABKAkivQEKGUNvbnZlcnNhdGlvblR1cm5TdHJ1Y3R1cmUSSwoXYWdlbnRfY29udmVyc2F0aW9uX3R1cm4YASABKAsyKC5hZ2VudC52MS5BZ2VudENvbnZlcnNhdGlvblR1cm5TdHJ1Y3R1cmVIABJLChdzaGVsbF9jb252ZXJzYXRpb25fdHVybhgCIAEoCzIoLmFnZW50LnYxLlNoZWxsQ29udmVyc2F0aW9uVHVyblN0cnVjdHVyZUgAQgYKBHR1cm4ilwEKFUFnZW50Q29udmVyc2F0aW9uVHVybhIrCgx1c2VyX21lc3NhZ2UYASABKAsyFS5hZ2VudC52MS5Vc2VyTWVzc2FnZRIpCgVzdGVwcxgCIAMoCzIaLmFnZW50LnYxLkNvbnZlcnNhdGlvblN0ZXASFwoKcmVxdWVzdF9pZBgDIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIm0KHkFnZW50Q29udmVyc2F0aW9uVHVyblN0cnVjdHVyZRIUCgx1c2VyX21lc3NhZ2UYASABKAwSDQoFc3RlcHMYAiADKAwSFwoKcmVxdWVzdF9pZBgDIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkInMKFVNoZWxsQ29udmVyc2F0aW9uVHVybhItCg1zaGVsbF9jb21tYW5kGAEgASgLMhYuYWdlbnQudjEuU2hlbGxDb21tYW5kEisKDHNoZWxsX291dHB1dBgCIAEoCzIVLmFnZW50LnYxLlNoZWxsT3V0cHV0Ik0KHlNoZWxsQ29udmVyc2F0aW9uVHVyblN0cnVjdHVyZRIVCg1zaGVsbF9jb21tYW5kGAEgASgMEhQKDHNoZWxsX291dHB1dBgCIAEoDCImChNDb252ZXJzYXRpb25TdW1tYXJ5Eg8KB3N1bW1hcnkYASABKAkieAoaQ29udmVyc2F0aW9uU3VtbWFyeUFyY2hpdmUSGwoTc3VtbWFyaXplZF9tZXNzYWdlcxgBIAMoDBIPCgdzdW1tYXJ5GAIgASgJEhMKC3dpbmRvd190YWlsGAMgASgNEhcKD3N1bW1hcnlfbWVzc2FnZRgEIAEoDCJDChhDb252ZXJzYXRpb25Ub2tlbkRldGFpbHMSEwoLdXNlZF90b2tlbnMYASABKA0SEgoKbWF4X3Rva2VucxgCIAEoDSJfCglGaWxlU3RhdGUSFAoHY29udGVudBgBIAEoCUgAiAEBEhwKD2luaXRpYWxfY29udGVudBgCIAEoCUgBiAEBQgoKCF9jb250ZW50QhIKEF9pbml0aWFsX2NvbnRlbnQiaAoSRmlsZVN0YXRlU3RydWN0dXJlEhQKB2NvbnRlbnQYASABKAxIAIgBARIcCg9pbml0aWFsX2NvbnRlbnQYAiABKAxIAYgBAUIKCghfY29udGVudEISChBfaW5pdGlhbF9jb250ZW50IjcKClN0ZXBUaW1pbmcSEwoLZHVyYXRpb25fbXMYASABKAQSFAoMdGltZXN0YW1wX21zGAIgASgEIvYEChFDb252ZXJzYXRpb25TdGF0ZRIhChlyb290X3Byb21wdF9tZXNzYWdlc19qc29uGAEgAygJEikKBXR1cm5zGAggAygLMhouYWdlbnQudjEuQ29udmVyc2F0aW9uVHVybhIhCgV0b2RvcxgDIAMoCzISLmFnZW50LnYxLlRvZG9JdGVtEhoKEnBlbmRpbmdfdG9vbF9jYWxscxgEIAMoCRI5Cg10b2tlbl9kZXRhaWxzGAUgASgLMiIuYWdlbnQudjEuQ29udmVyc2F0aW9uVG9rZW5EZXRhaWxzEjMKB3N1bW1hcnkYBiABKAsyHS5hZ2VudC52MS5Db252ZXJzYXRpb25TdW1tYXJ5SACIAQESLQoEcGxhbhgHIAEoCzIaLmFnZW50LnYxLkNvbnZlcnNhdGlvblBsYW5IAYgBARJCCg9zdW1tYXJ5X2FyY2hpdmUYCSABKAsyJC5hZ2VudC52MS5Db252ZXJzYXRpb25TdW1tYXJ5QXJjaGl2ZUgCiAEBEkAKC2ZpbGVfc3RhdGVzGAogAygLMisuYWdlbnQudjEuQ29udmVyc2F0aW9uU3RhdGUuRmlsZVN0YXRlc0VudHJ5Ej4KEHN1bW1hcnlfYXJjaGl2ZXMYCyADKAsyJC5hZ2VudC52MS5Db252ZXJzYXRpb25TdW1tYXJ5QXJjaGl2ZRpGCg9GaWxlU3RhdGVzRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVlGAIgASgLMhMuYWdlbnQudjEuRmlsZVN0YXRlOgI4AUIKCghfc3VtbWFyeUIHCgVfcGxhbkISChBfc3VtbWFyeV9hcmNoaXZlIscBChZTdWJhZ2VudFBlcnNpc3RlZFN0YXRlEkAKEmNvbnZlcnNhdGlvbl9zdGF0ZRgBIAEoCzIkLmFnZW50LnYxLkNvbnZlcnNhdGlvblN0YXRlU3RydWN0dXJlEhwKFGNyZWF0ZWRfdGltZXN0YW1wX21zGAIgASgEEh4KFmxhc3RfdXNlZF90aW1lc3RhbXBfbXMYAyABKAQSLQoNc3ViYWdlbnRfdHlwZRgEIAEoCzIWLmFnZW50LnYxLlN1YmFnZW50VHlwZSK3BwoaQ29udmVyc2F0aW9uU3RhdGVTdHJ1Y3R1cmUSEQoJdHVybnNfb2xkGAIgAygMEiEKGXJvb3RfcHJvbXB0X21lc3NhZ2VzX2pzb24YASADKAwSDQoFdHVybnMYCCADKAwSDQoFdG9kb3MYAyADKAwSGgoScGVuZGluZ190b29sX2NhbGxzGAQgAygJEjkKDXRva2VuX2RldGFpbHMYBSABKAsyIi5hZ2VudC52MS5Db252ZXJzYXRpb25Ub2tlbkRldGFpbHMSFAoHc3VtbWFyeRgGIAEoDEgAiAEBEhEKBHBsYW4YByABKAxIAYgBARIfChdwcmV2aW91c193b3Jrc3BhY2VfdXJpcxgJIAMoCRIRCgRtb2RlGAogASgFSAKIAQESHAoPc3VtbWFyeV9hcmNoaXZlGAsgASgMSAOIAQESSQoLZmlsZV9zdGF0ZXMYDCADKAsyNC5hZ2VudC52MS5Db252ZXJzYXRpb25TdGF0ZVN0cnVjdHVyZS5GaWxlU3RhdGVzRW50cnkSTgoOZmlsZV9zdGF0ZXNfdjIYDyADKAsyNi5hZ2VudC52MS5Db252ZXJzYXRpb25TdGF0ZVN0cnVjdHVyZS5GaWxlU3RhdGVzVjJFbnRyeRIYChBzdW1tYXJ5X2FyY2hpdmVzGA0gAygMEioKDHR1cm5fdGltaW5ncxgOIAMoCzIULmFnZW50LnYxLlN0ZXBUaW1pbmcSUQoPc3ViYWdlbnRfc3RhdGVzGBAgAygLMjguYWdlbnQudjEuQ29udmVyc2F0aW9uU3RhdGVTdHJ1Y3R1cmUuU3ViYWdlbnRTdGF0ZXNFbnRyeRIaChJzZWxmX3N1bW1hcnlfY291bnQYESABKA0SEgoKcmVhZF9wYXRocxgSIAMoCRoxCg9GaWxlU3RhdGVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ARpRChFGaWxlU3RhdGVzVjJFbnRyeRILCgNrZXkYASABKAkSKwoFdmFsdWUYAiABKAsyHC5hZ2VudC52MS5GaWxlU3RhdGVTdHJ1Y3R1cmU6AjgBGlcKE1N1YmFnZW50U3RhdGVzRW50cnkSCwoDa2V5GAEgASgJEi8KBXZhbHVlGAIgASgLMiAuYWdlbnQudjEuU3ViYWdlbnRQZXJzaXN0ZWRTdGF0ZToCOAFCCgoIX3N1bW1hcnlCBwoFX3BsYW5CBwoFX21vZGVCEgoQX3N1bW1hcnlfYXJjaGl2ZSIRCg9UaGlua2luZ0RldGFpbHMiSAoRQXBpS2V5Q3JlZGVudGlhbHMSDwoHYXBpX2tleRgBIAEoCRIVCghiYXNlX3VybBgCIAEoCUgAiAEBQgsKCV9iYXNlX3VybCJJChBBenVyZUNyZWRlbnRpYWxzEg8KB2FwaV9rZXkYASABKAkSEAoIYmFzZV91cmwYAiABKAkSEgoKZGVwbG95bWVudBgDIAEoCSJ6ChJCZWRyb2NrQ3JlZGVudGlhbHMSEgoKYWNjZXNzX2tleRgBIAEoCRISCgpzZWNyZXRfa2V5GAIgASgJEg4KBnJlZ2lvbhgDIAEoCRIaCg1zZXNzaW9uX3Rva2VuGAQgASgJSACIAQFCEAoOX3Nlc3Npb25fdG9rZW4isQMKDE1vZGVsRGV0YWlscxIQCghtb2RlbF9pZBgBIAEoCRIYChBkaXNwbGF5X21vZGVsX2lkGAMgASgJEhQKDGRpc3BsYXlfbmFtZRgEIAEoCRIaChJkaXNwbGF5X25hbWVfc2hvcnQYBSABKAkSDwoHYWxpYXNlcxgGIAMoCRI4ChB0aGlua2luZ19kZXRhaWxzGAIgASgLMhkuYWdlbnQudjEuVGhpbmtpbmdEZXRhaWxzSAGIAQESFQoIbWF4X21vZGUYByABKAhIAogBARI6ChNhcGlfa2V5X2NyZWRlbnRpYWxzGAggASgLMhsuYWdlbnQudjEuQXBpS2V5Q3JlZGVudGlhbHNIABI3ChFhenVyZV9jcmVkZW50aWFscxgJIAEoCzIaLmFnZW50LnYxLkF6dXJlQ3JlZGVudGlhbHNIABI7ChNiZWRyb2NrX2NyZWRlbnRpYWxzGAogASgLMhwuYWdlbnQudjEuQmVkcm9ja0NyZWRlbnRpYWxzSABCDQoLY3JlZGVudGlhbHNCEwoRX3RoaW5raW5nX2RldGFpbHNCCwoJX21heF9tb2RlIrcCCg5SZXF1ZXN0ZWRNb2RlbBIQCghtb2RlbF9pZBgBIAEoCRIQCghtYXhfbW9kZRgCIAEoCBJACgpwYXJhbWV0ZXJzGAMgAygLMiwuYWdlbnQudjEuUmVxdWVzdGVkTW9kZWxfTW9kZWxQYXJhbWV0ZXJieXRlcxI6ChNhcGlfa2V5X2NyZWRlbnRpYWxzGAQgASgLMhsuYWdlbnQudjEuQXBpS2V5Q3JlZGVudGlhbHNIABI3ChFhenVyZV9jcmVkZW50aWFscxgFIAEoCzIaLmFnZW50LnYxLkF6dXJlQ3JlZGVudGlhbHNIABI7ChNiZWRyb2NrX2NyZWRlbnRpYWxzGAYgASgLMhwuYWdlbnQudjEuQmVkcm9ja0NyZWRlbnRpYWxzSABCDQoLY3JlZGVudGlhbHMiPwoiUmVxdWVzdGVkTW9kZWxfTW9kZWxQYXJhbWV0ZXJieXRlcxIKCgJpZBgBIAEoCRINCgV2YWx1ZRgCIAEoCSK5BAoPQWdlbnRSdW5SZXF1ZXN0EkAKEmNvbnZlcnNhdGlvbl9zdGF0ZRgBIAEoCzIkLmFnZW50LnYxLkNvbnZlcnNhdGlvblN0YXRlU3RydWN0dXJlEiwKBmFjdGlvbhgCIAEoCzIcLmFnZW50LnYxLkNvbnZlcnNhdGlvbkFjdGlvbhItCg1tb2RlbF9kZXRhaWxzGAMgASgLMhYuYWdlbnQudjEuTW9kZWxEZXRhaWxzEjYKD3JlcXVlc3RlZF9tb2RlbBgJIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RlZE1vZGVsSACIAQESJQoJbWNwX3Rvb2xzGAQgASgLMhIuYWdlbnQudjEuTWNwVG9vbHMSHAoPY29udmVyc2F0aW9uX2lkGAUgASgJSAGIAQESRAoXbWNwX2ZpbGVfc3lzdGVtX29wdGlvbnMYBiABKAsyHi5hZ2VudC52MS5NY3BGaWxlU3lzdGVtT3B0aW9uc0gCiAEBEjIKDXNraWxsX29wdGlvbnMYByABKAsyFi5hZ2VudC52MS5Ta2lsbE9wdGlvbnNIA4gBARIhChRjdXN0b21fc3lzdGVtX3Byb21wdBgIIAEoCUgEiAEBQhIKEF9yZXF1ZXN0ZWRfbW9kZWxCEgoQX2NvbnZlcnNhdGlvbl9pZEIaChhfbWNwX2ZpbGVfc3lzdGVtX29wdGlvbnNCEAoOX3NraWxsX29wdGlvbnNCFwoVX2N1c3RvbV9zeXN0ZW1fcHJvbXB0Ih8KD1RleHREZWx0YVVwZGF0ZRIMCgR0ZXh0GAEgASgJImYKFVRvb2xDYWxsU3RhcnRlZFVwZGF0ZRIPCgdjYWxsX2lkGAEgASgJEiUKCXRvb2xfY2FsbBgCIAEoCzISLmFnZW50LnYxLlRvb2xDYWxsEhUKDW1vZGVsX2NhbGxfaWQYAyABKAkiaAoXVG9vbENhbGxDb21wbGV0ZWRVcGRhdGUSDwoHY2FsbF9pZBgBIAEoCRIlCgl0b29sX2NhbGwYAiABKAsyEi5hZ2VudC52MS5Ub29sQ2FsbBIVCg1tb2RlbF9jYWxsX2lkGAMgASgJIm8KE1Rvb2xDYWxsRGVsdGFVcGRhdGUSDwoHY2FsbF9pZBgBIAEoCRIwCg90b29sX2NhbGxfZGVsdGEYAiABKAsyFy5hZ2VudC52MS5Ub29sQ2FsbERlbHRhEhUKDW1vZGVsX2NhbGxfaWQYAyABKAkifwoVUGFydGlhbFRvb2xDYWxsVXBkYXRlEg8KB2NhbGxfaWQYASABKAkSJQoJdG9vbF9jYWxsGAIgASgLMhIuYWdlbnQudjEuVG9vbENhbGwSFwoPYXJnc190ZXh0X2RlbHRhGAMgASgJEhUKDW1vZGVsX2NhbGxfaWQYBCABKAkiIwoTVGhpbmtpbmdEZWx0YVVwZGF0ZRIMCgR0ZXh0GAEgASgJIjcKF1RoaW5raW5nQ29tcGxldGVkVXBkYXRlEhwKFHRoaW5raW5nX2R1cmF0aW9uX21zGAEgASgFIiIKEFRva2VuRGVsdGFVcGRhdGUSDgoGdG9rZW5zGAEgASgFIiAKDVN1bW1hcnlVcGRhdGUSDwoHc3VtbWFyeRgBIAEoCSIWChRTdW1tYXJ5U3RhcnRlZFVwZGF0ZSIRCg9IZWFydGJlYXRVcGRhdGUiGAoWU3VtbWFyeUNvbXBsZXRlZFVwZGF0ZSLXAQoWU2hlbGxPdXRwdXREZWx0YVVwZGF0ZRItCgZzdGRvdXQYASABKAsyGy5hZ2VudC52MS5TaGVsbFN0cmVhbVN0ZG91dEgAEi0KBnN0ZGVychgCIAEoCzIbLmFnZW50LnYxLlNoZWxsU3RyZWFtU3RkZXJySAASKQoEZXhpdBgDIAEoCzIZLmFnZW50LnYxLlNoZWxsU3RyZWFtRXhpdEgAEisKBXN0YXJ0GAQgASgLMhouYWdlbnQudjEuU2hlbGxTdHJlYW1TdGFydEgAQgcKBWV2ZW50IhEKD1R1cm5FbmRlZFVwZGF0ZSJIChlVc2VyTWVzc2FnZUFwcGVuZGVkVXBkYXRlEisKDHVzZXJfbWVzc2FnZRgBIAEoCzIVLmFnZW50LnYxLlVzZXJNZXNzYWdlIiQKEVN0ZXBTdGFydGVkVXBkYXRlEg8KB3N0ZXBfaWQYASABKAQiQAoTU3RlcENvbXBsZXRlZFVwZGF0ZRIPCgdzdGVwX2lkGAEgASgEEhgKEHN0ZXBfZHVyYXRpb25fbXMYAiABKAMi7wcKEUludGVyYWN0aW9uVXBkYXRlEi8KCnRleHRfZGVsdGEYASABKAsyGS5hZ2VudC52MS5UZXh0RGVsdGFVcGRhdGVIABI8ChFwYXJ0aWFsX3Rvb2xfY2FsbBgHIAEoCzIfLmFnZW50LnYxLlBhcnRpYWxUb29sQ2FsbFVwZGF0ZUgAEjgKD3Rvb2xfY2FsbF9kZWx0YRgPIAEoCzIdLmFnZW50LnYxLlRvb2xDYWxsRGVsdGFVcGRhdGVIABI8ChF0b29sX2NhbGxfc3RhcnRlZBgCIAEoCzIfLmFnZW50LnYxLlRvb2xDYWxsU3RhcnRlZFVwZGF0ZUgAEkAKE3Rvb2xfY2FsbF9jb21wbGV0ZWQYAyABKAsyIS5hZ2VudC52MS5Ub29sQ2FsbENvbXBsZXRlZFVwZGF0ZUgAEjcKDnRoaW5raW5nX2RlbHRhGAQgASgLMh0uYWdlbnQudjEuVGhpbmtpbmdEZWx0YVVwZGF0ZUgAEj8KEnRoaW5raW5nX2NvbXBsZXRlZBgFIAEoCzIhLmFnZW50LnYxLlRoaW5raW5nQ29tcGxldGVkVXBkYXRlSAASRAoVdXNlcl9tZXNzYWdlX2FwcGVuZGVkGAYgASgLMiMuYWdlbnQudjEuVXNlck1lc3NhZ2VBcHBlbmRlZFVwZGF0ZUgAEjEKC3Rva2VuX2RlbHRhGAggASgLMhouYWdlbnQudjEuVG9rZW5EZWx0YVVwZGF0ZUgAEioKB3N1bW1hcnkYCSABKAsyFy5hZ2VudC52MS5TdW1tYXJ5VXBkYXRlSAASOQoPc3VtbWFyeV9zdGFydGVkGAogASgLMh4uYWdlbnQudjEuU3VtbWFyeVN0YXJ0ZWRVcGRhdGVIABI9ChFzdW1tYXJ5X2NvbXBsZXRlZBgLIAEoCzIgLmFnZW50LnYxLlN1bW1hcnlDb21wbGV0ZWRVcGRhdGVIABI+ChJzaGVsbF9vdXRwdXRfZGVsdGEYDCABKAsyIC5hZ2VudC52MS5TaGVsbE91dHB1dERlbHRhVXBkYXRlSAASLgoJaGVhcnRiZWF0GA0gASgLMhkuYWdlbnQudjEuSGVhcnRiZWF0VXBkYXRlSAASLwoKdHVybl9lbmRlZBgOIAEoCzIZLmFnZW50LnYxLlR1cm5FbmRlZFVwZGF0ZUgAEjMKDHN0ZXBfc3RhcnRlZBgQIAEoCzIbLmFnZW50LnYxLlN0ZXBTdGFydGVkVXBkYXRlSAASNwoOc3RlcF9jb21wbGV0ZWQYESABKAsyHS5hZ2VudC52MS5TdGVwQ29tcGxldGVkVXBkYXRlSABCCQoHbWVzc2FnZSKaBAoQSW50ZXJhY3Rpb25RdWVyeRIKCgJpZBgBIAEoDRJDChh3ZWJfc2VhcmNoX3JlcXVlc3RfcXVlcnkYAiABKAsyHy5hZ2VudC52MS5XZWJTZWFyY2hSZXF1ZXN0UXVlcnlIABJPCh5hc2tfcXVlc3Rpb25faW50ZXJhY3Rpb25fcXVlcnkYAyABKAsyJS5hZ2VudC52MS5Bc2tRdWVzdGlvbkludGVyYWN0aW9uUXVlcnlIABJFChlzd2l0Y2hfbW9kZV9yZXF1ZXN0X3F1ZXJ5GAQgASgLMiAuYWdlbnQudjEuU3dpdGNoTW9kZVJlcXVlc3RRdWVyeUgAEkMKGGV4YV9zZWFyY2hfcmVxdWVzdF9xdWVyeRgFIAEoCzIfLmFnZW50LnYxLkV4YVNlYXJjaFJlcXVlc3RRdWVyeUgAEkEKF2V4YV9mZXRjaF9yZXF1ZXN0X3F1ZXJ5GAYgASgLMh4uYWdlbnQudjEuRXhhRmV0Y2hSZXF1ZXN0UXVlcnlIABJFChljcmVhdGVfcGxhbl9yZXF1ZXN0X3F1ZXJ5GAcgASgLMiAuYWdlbnQudjEuQ3JlYXRlUGxhblJlcXVlc3RRdWVyeUgAEkUKGXNldHVwX3ZtX2Vudmlyb25tZW50X2FyZ3MYCCABKAsyIC5hZ2VudC52MS5TZXR1cFZtRW52aXJvbm1lbnRBcmdzSABCBwoFcXVlcnkixgQKE0ludGVyYWN0aW9uUmVzcG9uc2USCgoCaWQYASABKA0SSQobd2ViX3NlYXJjaF9yZXF1ZXN0X3Jlc3BvbnNlGAIgASgLMiIuYWdlbnQudjEuV2ViU2VhcmNoUmVxdWVzdFJlc3BvbnNlSAASVQohYXNrX3F1ZXN0aW9uX2ludGVyYWN0aW9uX3Jlc3BvbnNlGAMgASgLMiguYWdlbnQudjEuQXNrUXVlc3Rpb25JbnRlcmFjdGlvblJlc3BvbnNlSAASSwocc3dpdGNoX21vZGVfcmVxdWVzdF9yZXNwb25zZRgEIAEoCzIjLmFnZW50LnYxLlN3aXRjaE1vZGVSZXF1ZXN0UmVzcG9uc2VIABJJChtleGFfc2VhcmNoX3JlcXVlc3RfcmVzcG9uc2UYBSABKAsyIi5hZ2VudC52MS5FeGFTZWFyY2hSZXF1ZXN0UmVzcG9uc2VIABJHChpleGFfZmV0Y2hfcmVxdWVzdF9yZXNwb25zZRgGIAEoCzIhLmFnZW50LnYxLkV4YUZldGNoUmVxdWVzdFJlc3BvbnNlSAASSwocY3JlYXRlX3BsYW5fcmVxdWVzdF9yZXNwb25zZRgHIAEoCzIjLmFnZW50LnYxLkNyZWF0ZVBsYW5SZXF1ZXN0UmVzcG9uc2VIABJJChtzZXR1cF92bV9lbnZpcm9ubWVudF9yZXN1bHQYCCABKAsyIi5hZ2VudC52MS5TZXR1cFZtRW52aXJvbm1lbnRSZXN1bHRIAEIICgZyZXN1bHQiXAobQXNrUXVlc3Rpb25JbnRlcmFjdGlvblF1ZXJ5EicKBGFyZ3MYASABKAsyGS5hZ2VudC52MS5Bc2tRdWVzdGlvbkFyZ3MSFAoMdG9vbF9jYWxsX2lkGAIgASgJIk0KHkFza1F1ZXN0aW9uSW50ZXJhY3Rpb25SZXNwb25zZRIrCgZyZXN1bHQYASABKAsyGy5hZ2VudC52MS5Bc2tRdWVzdGlvblJlc3VsdCIRCg9DbGllbnRIZWFydGJlYXQixgQKDlByZXdhcm1SZXF1ZXN0Ei0KDW1vZGVsX2RldGFpbHMYASABKAsyFi5hZ2VudC52MS5Nb2RlbERldGFpbHMSNgoPcmVxdWVzdGVkX21vZGVsGAkgASgLMhguYWdlbnQudjEuUmVxdWVzdGVkTW9kZWxIAIgBARIcCg9jb252ZXJzYXRpb25faWQYAiABKAlIAYgBARJAChJjb252ZXJzYXRpb25fc3RhdGUYAyABKAsyJC5hZ2VudC52MS5Db252ZXJzYXRpb25TdGF0ZVN0cnVjdHVyZRIlCgltY3BfdG9vbHMYBCABKAsyEi5hZ2VudC52MS5NY3BUb29scxJEChdtY3BfZmlsZV9zeXN0ZW1fb3B0aW9ucxgFIAEoCzIeLmFnZW50LnYxLk1jcEZpbGVTeXN0ZW1PcHRpb25zSAKIAQESHwoSYmVzdF9vZl9uX2dyb3VwX2lkGAYgASgJSAOIAQESKAobdHJ5X3VzZV9iZXN0X29mX25fcHJvbW90aW9uGAcgASgISASIAQESIQoUY3VzdG9tX3N5c3RlbV9wcm9tcHQYCCABKAlIBYgBAUISChBfcmVxdWVzdGVkX21vZGVsQhIKEF9jb252ZXJzYXRpb25faWRCGgoYX21jcF9maWxlX3N5c3RlbV9vcHRpb25zQhUKE19iZXN0X29mX25fZ3JvdXBfaWRCHgocX3RyeV91c2VfYmVzdF9vZl9uX3Byb21vdGlvbkIXChVfY3VzdG9tX3N5c3RlbV9wcm9tcHQiHQoPRXhlY1NlcnZlckFib3J0EgoKAmlkGAEgASgNIlEKGEV4ZWNTZXJ2ZXJDb250cm9sTWVzc2FnZRIqCgVhYm9ydBgBIAEoCzIZLmFnZW50LnYxLkV4ZWNTZXJ2ZXJBYm9ydEgAQgkKB21lc3NhZ2Ui+AMKEkFnZW50Q2xpZW50TWVzc2FnZRIwCgtydW5fcmVxdWVzdBgBIAEoCzIZLmFnZW50LnYxLkFnZW50UnVuUmVxdWVzdEgAEjoKE2V4ZWNfY2xpZW50X21lc3NhZ2UYAiABKAsyGy5hZ2VudC52MS5FeGVjQ2xpZW50TWVzc2FnZUgAEkkKG2V4ZWNfY2xpZW50X2NvbnRyb2xfbWVzc2FnZRgFIAEoCzIiLmFnZW50LnYxLkV4ZWNDbGllbnRDb250cm9sTWVzc2FnZUgAEjYKEWt2X2NsaWVudF9tZXNzYWdlGAMgASgLMhkuYWdlbnQudjEuS3ZDbGllbnRNZXNzYWdlSAASOwoTY29udmVyc2F0aW9uX2FjdGlvbhgEIAEoCzIcLmFnZW50LnYxLkNvbnZlcnNhdGlvbkFjdGlvbkgAEj0KFGludGVyYWN0aW9uX3Jlc3BvbnNlGAYgASgLMh0uYWdlbnQudjEuSW50ZXJhY3Rpb25SZXNwb25zZUgAEjUKEGNsaWVudF9oZWFydGJlYXQYByABKAsyGS5hZ2VudC52MS5DbGllbnRIZWFydGJlYXRIABIzCg9wcmV3YXJtX3JlcXVlc3QYCCABKAsyGC5hZ2VudC52MS5QcmV3YXJtUmVxdWVzdEgAQgkKB21lc3NhZ2UiogMKEkFnZW50U2VydmVyTWVzc2FnZRI5ChJpbnRlcmFjdGlvbl91cGRhdGUYASABKAsyGy5hZ2VudC52MS5JbnRlcmFjdGlvblVwZGF0ZUgAEjoKE2V4ZWNfc2VydmVyX21lc3NhZ2UYAiABKAsyGy5hZ2VudC52MS5FeGVjU2VydmVyTWVzc2FnZUgAEkkKG2V4ZWNfc2VydmVyX2NvbnRyb2xfbWVzc2FnZRgFIAEoCzIiLmFnZW50LnYxLkV4ZWNTZXJ2ZXJDb250cm9sTWVzc2FnZUgAEk4KHmNvbnZlcnNhdGlvbl9jaGVja3BvaW50X3VwZGF0ZRgDIAEoCzIkLmFnZW50LnYxLkNvbnZlcnNhdGlvblN0YXRlU3RydWN0dXJlSAASNgoRa3Zfc2VydmVyX21lc3NhZ2UYBCABKAsyGS5hZ2VudC52MS5LdlNlcnZlck1lc3NhZ2VIABI3ChFpbnRlcmFjdGlvbl9xdWVyeRgHIAEoCzIaLmFnZW50LnYxLkludGVyYWN0aW9uUXVlcnlIAEIJCgdtZXNzYWdlIigKEE5hbWVBZ2VudFJlcXVlc3QSFAoMdXNlcl9tZXNzYWdlGAEgASgJIiEKEU5hbWVBZ2VudFJlc3BvbnNlEgwKBG5hbWUYASABKAkiMgoWR2V0VXNhYmxlTW9kZWxzUmVxdWVzdBIYChBjdXN0b21fbW9kZWxfaWRzGAEgAygJIkEKF0dldFVzYWJsZU1vZGVsc1Jlc3BvbnNlEiYKBm1vZGVscxgBIAMoCzIWLmFnZW50LnYxLk1vZGVsRGV0YWlscyIeChxHZXREZWZhdWx0TW9kZWxGb3JDbGlSZXF1ZXN0IkYKHUdldERlZmF1bHRNb2RlbEZvckNsaVJlc3BvbnNlEiUKBW1vZGVsGAEgASgLMhYuYWdlbnQudjEuTW9kZWxEZXRhaWxzIh8KHUdldEFsbG93ZWRNb2RlbEludGVudHNSZXF1ZXN0IjcKHkdldEFsbG93ZWRNb2RlbEludGVudHNSZXNwb25zZRIVCg1tb2RlbF9pbnRlbnRzGAEgAygJIpcCChNJZGVFZGl0b3JzU3RhdGVGaWxlEhUKDXJlbGF0aXZlX3BhdGgYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCRIhChRpc19jdXJyZW50bHlfZm9jdXNlZBgDIAEoCEgAiAEBEiAKE2N1cnJlbnRfbGluZV9udW1iZXIYBCABKAVIAYgBARIeChFjdXJyZW50X2xpbmVfdGV4dBgFIAEoCUgCiAEBEhcKCmxpbmVfY291bnQYBiABKAVIA4gBAUIXChVfaXNfY3VycmVudGx5X2ZvY3VzZWRCFgoUX2N1cnJlbnRfbGluZV9udW1iZXJCFAoSX2N1cnJlbnRfbGluZV90ZXh0Qg0KC19saW5lX2NvdW50IlMKE0lkZUVkaXRvcnNTdGF0ZUxpdGUSPAoVcmVjZW50bHlfdmlld2VkX2ZpbGVzGAEgAygLMh0uYWdlbnQudjEuSWRlRWRpdG9yc1N0YXRlRmlsZSJ0ChZBcHBseUFnZW50RGlmZlRvb2xDYWxsEioKBGFyZ3MYASABKAsyHC5hZ2VudC52MS5BcHBseUFnZW50RGlmZkFyZ3MSLgoGcmVzdWx0GAIgASgLMh4uYWdlbnQudjEuQXBwbHlBZ2VudERpZmZSZXN1bHQiJgoSQXBwbHlBZ2VudERpZmZBcmdzEhAKCGFnZW50X2lkGAEgASgJIoQBChRBcHBseUFnZW50RGlmZlJlc3VsdBIyCgdzdWNjZXNzGAEgASgLMh8uYWdlbnQudjEuQXBwbHlBZ2VudERpZmZTdWNjZXNzSAASLgoFZXJyb3IYAiABKAsyHS5hZ2VudC52MS5BcHBseUFnZW50RGlmZkVycm9ySABCCAoGcmVzdWx0Ik4KFUFwcGx5QWdlbnREaWZmU3VjY2VzcxI1Cg9hcHBsaWVkX2NoYW5nZXMYASADKAsyHC5hZ2VudC52MS5BcHBsaWVkQWdlbnRDaGFuZ2Ui6QEKEkFwcGxpZWRBZ2VudENoYW5nZRIMCgRwYXRoGAEgASgJEhMKC2NoYW5nZV90eXBlGAIgASgFEhsKDmJlZm9yZV9jb250ZW50GAMgASgJSACIAQESGgoNYWZ0ZXJfY29udGVudBgEIAEoCUgBiAEBEhIKBWVycm9yGAUgASgJSAKIAQESHgoRbWVzc2FnZV9mb3JfbW9kZWwYBiABKAlIA4gBAUIRCg9fYmVmb3JlX2NvbnRlbnRCEAoOX2FmdGVyX2NvbnRlbnRCCAoGX2Vycm9yQhQKEl9tZXNzYWdlX2Zvcl9tb2RlbCJbChNBcHBseUFnZW50RGlmZkVycm9yEg0KBWVycm9yGAEgASgJEjUKD2FwcGxpZWRfY2hhbmdlcxgCIAMoCzIcLmFnZW50LnYxLkFwcGxpZWRBZ2VudENoYW5nZSJrChNBc2tRdWVzdGlvblRvb2xDYWxsEicKBGFyZ3MYASABKAsyGS5hZ2VudC52MS5Bc2tRdWVzdGlvbkFyZ3MSKwoGcmVzdWx0GAIgASgLMhsuYWdlbnQudjEuQXNrUXVlc3Rpb25SZXN1bHQijwEKD0Fza1F1ZXN0aW9uQXJncxINCgV0aXRsZRgBIAEoCRI1CglxdWVzdGlvbnMYAiADKAsyIi5hZ2VudC52MS5Bc2tRdWVzdGlvbkFyZ3NfUXVlc3Rpb24SEQoJcnVuX2FzeW5jGAUgASgIEiMKG2FzeW5jX29yaWdpbmFsX3Rvb2xfY2FsbF9pZBgGIAEoCSKBAQoYQXNrUXVlc3Rpb25BcmdzX1F1ZXN0aW9uEgoKAmlkGAEgASgJEg4KBnByb21wdBgCIAEoCRIxCgdvcHRpb25zGAMgAygLMiAuYWdlbnQudjEuQXNrUXVlc3Rpb25BcmdzX09wdGlvbhIWCg5hbGxvd19tdWx0aXBsZRgEIAEoCCIzChZBc2tRdWVzdGlvbkFyZ3NfT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJIhIKEEFza1F1ZXN0aW9uQXN5bmMi2wEKEUFza1F1ZXN0aW9uUmVzdWx0Ei8KB3N1Y2Nlc3MYASABKAsyHC5hZ2VudC52MS5Bc2tRdWVzdGlvblN1Y2Nlc3NIABIrCgVlcnJvchgCIAEoCzIaLmFnZW50LnYxLkFza1F1ZXN0aW9uRXJyb3JIABIxCghyZWplY3RlZBgDIAEoCzIdLmFnZW50LnYxLkFza1F1ZXN0aW9uUmVqZWN0ZWRIABIrCgVhc3luYxgEIAEoCzIaLmFnZW50LnYxLkFza1F1ZXN0aW9uQXN5bmNIAEIICgZyZXN1bHQiSgoSQXNrUXVlc3Rpb25TdWNjZXNzEjQKB2Fuc3dlcnMYASADKAsyIy5hZ2VudC52MS5Bc2tRdWVzdGlvblN1Y2Nlc3NfQW5zd2VyIk0KGUFza1F1ZXN0aW9uU3VjY2Vzc19BbnN3ZXISEwoLcXVlc3Rpb25faWQYASABKAkSGwoTc2VsZWN0ZWRfb3B0aW9uX2lkcxgCIAMoCSIpChBBc2tRdWVzdGlvbkVycm9yEhUKDWVycm9yX21lc3NhZ2UYASABKAkiJQoTQXNrUXVlc3Rpb25SZWplY3RlZBIOCgZyZWFzb24YASABKAkiiQIKGEJhY2tncm91bmRTaGVsbFNwYXduQXJncxIPCgdjb21tYW5kGAEgASgJEhkKEXdvcmtpbmdfZGlyZWN0b3J5GAIgASgJEhQKDHRvb2xfY2FsbF9pZBgDIAEoCRI7Cg5wYXJzaW5nX3Jlc3VsdBgEIAEoCzIjLmFnZW50LnYxLlNoZWxsQ29tbWFuZFBhcnNpbmdSZXN1bHQSNAoOc2FuZGJveF9wb2xpY3kYBSABKAsyFy5hZ2VudC52MS5TYW5kYm94UG9saWN5SACIAQESJQodZW5hYmxlX3dyaXRlX3NoZWxsX3N0ZGluX3Rvb2wYBiABKAhCEQoPX3NhbmRib3hfcG9saWN5IoECChpCYWNrZ3JvdW5kU2hlbGxTcGF3blJlc3VsdBI4CgdzdWNjZXNzGAEgASgLMiUuYWdlbnQudjEuQmFja2dyb3VuZFNoZWxsU3Bhd25TdWNjZXNzSAASNAoFZXJyb3IYAiABKAsyIy5hZ2VudC52MS5CYWNrZ3JvdW5kU2hlbGxTcGF3bkVycm9ySAASKwoIcmVqZWN0ZWQYAyABKAsyFy5hZ2VudC52MS5TaGVsbFJlamVjdGVkSAASPAoRcGVybWlzc2lvbl9kZW5pZWQYBCABKAsyHy5hZ2VudC52MS5TaGVsbFBlcm1pc3Npb25EZW5pZWRIAEIICgZyZXN1bHQidQobQmFja2dyb3VuZFNoZWxsU3Bhd25TdWNjZXNzEhAKCHNoZWxsX2lkGAEgASgNEg8KB2NvbW1hbmQYAiABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAyABKAkSEAoDcGlkGAQgASgNSACIAQFCBgoEX3BpZCJWChlCYWNrZ3JvdW5kU2hlbGxTcGF3bkVycm9yEg8KB2NvbW1hbmQYASABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAiABKAkSDQoFZXJyb3IYAyABKAkiNgoTV3JpdGVTaGVsbFN0ZGluQXJncxIQCghzaGVsbF9pZBgBIAEoDRINCgVjaGFycxgCIAEoCSKHAQoVV3JpdGVTaGVsbFN0ZGluUmVzdWx0EjMKB3N1Y2Nlc3MYASABKAsyIC5hZ2VudC52MS5Xcml0ZVNoZWxsU3RkaW5TdWNjZXNzSAASLwoFZXJyb3IYAiABKAsyHi5hZ2VudC52MS5Xcml0ZVNoZWxsU3RkaW5FcnJvckgAQggKBnJlc3VsdCJdChZXcml0ZVNoZWxsU3RkaW5TdWNjZXNzEhAKCHNoZWxsX2lkGAEgASgNEjEKKXRlcm1pbmFsX2ZpbGVfbGVuZ3RoX2JlZm9yZV9pbnB1dF93cml0dGVuGAIgASgNIiUKFFdyaXRlU2hlbGxTdGRpbkVycm9yEg0KBWVycm9yGAEgASgJIiIKCkNvb3JkaW5hdGUSCQoBeBgBIAEoBRIJCgF5GAIgASgFIlUKD0NvbXB1dGVyVXNlQXJncxIUCgx0b29sX2NhbGxfaWQYASABKAkSLAoHYWN0aW9ucxgCIAMoCzIbLmFnZW50LnYxLkNvbXB1dGVyVXNlQWN0aW9uIoEEChFDb21wdXRlclVzZUFjdGlvbhIvCgptb3VzZV9tb3ZlGAEgASgLMhkuYWdlbnQudjEuTW91c2VNb3ZlQWN0aW9uSAASJgoFY2xpY2sYAiABKAsyFS5hZ2VudC52MS5DbGlja0FjdGlvbkgAEi8KCm1vdXNlX2Rvd24YAyABKAsyGS5hZ2VudC52MS5Nb3VzZURvd25BY3Rpb25IABIrCghtb3VzZV91cBgEIAEoCzIXLmFnZW50LnYxLk1vdXNlVXBBY3Rpb25IABIkCgRkcmFnGAUgASgLMhQuYWdlbnQudjEuRHJhZ0FjdGlvbkgAEigKBnNjcm9sbBgGIAEoCzIWLmFnZW50LnYxLlNjcm9sbEFjdGlvbkgAEiQKBHR5cGUYByABKAsyFC5hZ2VudC52MS5UeXBlQWN0aW9uSAASIgoDa2V5GAggASgLMhMuYWdlbnQudjEuS2V5QWN0aW9uSAASJAoEd2FpdBgJIAEoCzIULmFnZW50LnYxLldhaXRBY3Rpb25IABIwCgpzY3JlZW5zaG90GAogASgLMhouYWdlbnQudjEuU2NyZWVuc2hvdEFjdGlvbkgAEjkKD2N1cnNvcl9wb3NpdGlvbhgLIAEoCzIeLmFnZW50LnYxLkN1cnNvclBvc2l0aW9uQWN0aW9uSABCCAoGYWN0aW9uIjsKD01vdXNlTW92ZUFjdGlvbhIoCgpjb29yZGluYXRlGAEgASgLMhQuYWdlbnQudjEuQ29vcmRpbmF0ZSKYAQoLQ2xpY2tBY3Rpb24SLQoKY29vcmRpbmF0ZRgBIAEoCzIULmFnZW50LnYxLkNvb3JkaW5hdGVIAIgBARIOCgZidXR0b24YAiABKAUSDQoFY291bnQYAyABKAUSGgoNbW9kaWZpZXJfa2V5cxgEIAEoCUgBiAEBQg0KC19jb29yZGluYXRlQhAKDl9tb2RpZmllcl9rZXlzIiEKD01vdXNlRG93bkFjdGlvbhIOCgZidXR0b24YASABKAUiHwoNTW91c2VVcEFjdGlvbhIOCgZidXR0b24YASABKAUiQAoKRHJhZ0FjdGlvbhIiCgRwYXRoGAEgAygLMhQuYWdlbnQudjEuQ29vcmRpbmF0ZRIOCgZidXR0b24YAiABKAUinQEKDFNjcm9sbEFjdGlvbhItCgpjb29yZGluYXRlGAEgASgLMhQuYWdlbnQudjEuQ29vcmRpbmF0ZUgAiAEBEhEKCWRpcmVjdGlvbhgCIAEoBRIOCgZhbW91bnQYAyABKAUSGgoNbW9kaWZpZXJfa2V5cxgEIAEoCUgBiAEBQg0KC19jb29yZGluYXRlQhAKDl9tb2RpZmllcl9rZXlzIhoKClR5cGVBY3Rpb24SDAoEdGV4dBgBIAEoCSJMCglLZXlBY3Rpb24SCwoDa2V5GAEgASgJEh0KEGhvbGRfZHVyYXRpb25fbXMYAiABKAVIAIgBAUITChFfaG9sZF9kdXJhdGlvbl9tcyIhCgpXYWl0QWN0aW9uEhMKC2R1cmF0aW9uX21zGAEgASgFIhIKEFNjcmVlbnNob3RBY3Rpb24iFgoUQ3Vyc29yUG9zaXRpb25BY3Rpb24iewoRQ29tcHV0ZXJVc2VSZXN1bHQSLwoHc3VjY2VzcxgBIAEoCzIcLmFnZW50LnYxLkNvbXB1dGVyVXNlU3VjY2Vzc0gAEisKBWVycm9yGAIgASgLMhouYWdlbnQudjEuQ29tcHV0ZXJVc2VFcnJvckgAQggKBnJlc3VsdCL7AQoSQ29tcHV0ZXJVc2VTdWNjZXNzEhQKDGFjdGlvbl9jb3VudBgBIAEoBRITCgtkdXJhdGlvbl9tcxgCIAEoBRIXCgpzY3JlZW5zaG90GAMgASgJSACIAQESEAoDbG9nGAQgASgJSAGIAQESHAoPc2NyZWVuc2hvdF9wYXRoGAUgASgJSAKIAQESMgoPY3Vyc29yX3Bvc2l0aW9uGAYgASgLMhQuYWdlbnQudjEuQ29vcmRpbmF0ZUgDiAEBQg0KC19zY3JlZW5zaG90QgYKBF9sb2dCEgoQX3NjcmVlbnNob3RfcGF0aEISChBfY3Vyc29yX3Bvc2l0aW9uIsABChBDb21wdXRlclVzZUVycm9yEg0KBWVycm9yGAEgASgJEhQKDGFjdGlvbl9jb3VudBgCIAEoBRITCgtkdXJhdGlvbl9tcxgDIAEoBRIQCgNsb2cYBCABKAlIAIgBARIXCgpzY3JlZW5zaG90GAUgASgJSAGIAQESHAoPc2NyZWVuc2hvdF9wYXRoGAYgASgJSAKIAQFCBgoEX2xvZ0INCgtfc2NyZWVuc2hvdEISChBfc2NyZWVuc2hvdF9wYXRoImsKE0NvbXB1dGVyVXNlVG9vbENhbGwSJwoEYXJncxgBIAEoCzIZLmFnZW50LnYxLkNvbXB1dGVyVXNlQXJncxIrCgZyZXN1bHQYAiABKAsyGy5hZ2VudC52MS5Db21wdXRlclVzZVJlc3VsdCJoChJDcmVhdGVQbGFuVG9vbENhbGwSJgoEYXJncxgBIAEoCzIYLmFnZW50LnYxLkNyZWF0ZVBsYW5BcmdzEioKBnJlc3VsdBgCIAEoCzIaLmFnZW50LnYxLkNyZWF0ZVBsYW5SZXN1bHQiOAoFUGhhc2USDAoEbmFtZRgBIAEoCRIhCgV0b2RvcxgCIAMoCzISLmFnZW50LnYxLlRvZG9JdGVtIpYBCg5DcmVhdGVQbGFuQXJncxIMCgRwbGFuGAEgASgJEiEKBXRvZG9zGAIgAygLMhIuYWdlbnQudjEuVG9kb0l0ZW0SEAoIb3ZlcnZpZXcYAyABKAkSDAoEbmFtZRgEIAEoCRISCgppc19wcm9qZWN0GAUgASgIEh8KBnBoYXNlcxgGIAMoCzIPLmFnZW50LnYxLlBoYXNlIooBChBDcmVhdGVQbGFuUmVzdWx0EhAKCHBsYW5fdXJpGAMgASgJEi4KB3N1Y2Nlc3MYASABKAsyGy5hZ2VudC52MS5DcmVhdGVQbGFuU3VjY2Vzc0gAEioKBWVycm9yGAIgASgLMhkuYWdlbnQudjEuQ3JlYXRlUGxhbkVycm9ySABCCAoGcmVzdWx0IhMKEUNyZWF0ZVBsYW5TdWNjZXNzIiAKD0NyZWF0ZVBsYW5FcnJvchINCgVlcnJvchgBIAEoCSJWChZDcmVhdGVQbGFuUmVxdWVzdFF1ZXJ5EiYKBGFyZ3MYASABKAsyGC5hZ2VudC52MS5DcmVhdGVQbGFuQXJncxIUCgx0b29sX2NhbGxfaWQYAiABKAkiRwoZQ3JlYXRlUGxhblJlcXVlc3RSZXNwb25zZRIqCgZyZXN1bHQYASABKAsyGi5hZ2VudC52MS5DcmVhdGVQbGFuUmVzdWx0IhYKFEN1cnNvclJ1bGVUeXBlR2xvYmFsIigKF0N1cnNvclJ1bGVUeXBlRmlsZUdsb2JzEg0KBWdsb2JzGAEgAygJIjEKGkN1cnNvclJ1bGVUeXBlQWdlbnRGZXRjaGVkEhMKC2Rlc2NyaXB0aW9uGAEgASgJIiAKHkN1cnNvclJ1bGVUeXBlTWFudWFsbHlBdHRhY2hlZCKLAgoOQ3Vyc29yUnVsZVR5cGUSMAoGZ2xvYmFsGAEgASgLMh4uYWdlbnQudjEuQ3Vyc29yUnVsZVR5cGVHbG9iYWxIABI5CgxmaWxlX2dsb2JiZWQYAiABKAsyIS5hZ2VudC52MS5DdXJzb3JSdWxlVHlwZUZpbGVHbG9ic0gAEj0KDWFnZW50X2ZldGNoZWQYAyABKAsyJC5hZ2VudC52MS5DdXJzb3JSdWxlVHlwZUFnZW50RmV0Y2hlZEgAEkUKEW1hbnVhbGx5X2F0dGFjaGVkGAQgASgLMiguYWdlbnQudjEuQ3Vyc29yUnVsZVR5cGVNYW51YWxseUF0dGFjaGVkSABCBgoEdHlwZSLIAQoKQ3Vyc29yUnVsZRIRCglmdWxsX3BhdGgYASABKAkSDwoHY29udGVudBgCIAEoCRImCgR0eXBlGAMgASgLMhguYWdlbnQudjEuQ3Vyc29yUnVsZVR5cGUSDgoGc291cmNlGAQgASgFEh4KEWdpdF9yZW1vdGVfb3JpZ2luGAUgASgJSACIAQESGAoLcGFyc2VfZXJyb3IYBiABKAlIAYgBAUIUChJfZ2l0X3JlbW90ZV9vcmlnaW5CDgoMX3BhcnNlX2Vycm9yIjAKCkRlbGV0ZUFyZ3MSDAoEcGF0aBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAki7QIKDERlbGV0ZVJlc3VsdBIqCgdzdWNjZXNzGAEgASgLMhcuYWdlbnQudjEuRGVsZXRlU3VjY2Vzc0gAEjYKDmZpbGVfbm90X2ZvdW5kGAIgASgLMhwuYWdlbnQudjEuRGVsZXRlRmlsZU5vdEZvdW5kSAASKwoIbm90X2ZpbGUYAyABKAsyFy5hZ2VudC52MS5EZWxldGVOb3RGaWxlSAASPQoRcGVybWlzc2lvbl9kZW5pZWQYBCABKAsyIC5hZ2VudC52MS5EZWxldGVQZXJtaXNzaW9uRGVuaWVkSAASLQoJZmlsZV9idXN5GAUgASgLMhguYWdlbnQudjEuRGVsZXRlRmlsZUJ1c3lIABIsCghyZWplY3RlZBgGIAEoCzIYLmFnZW50LnYxLkRlbGV0ZVJlamVjdGVkSAASJgoFZXJyb3IYByABKAsyFS5hZ2VudC52MS5EZWxldGVFcnJvckgAQggKBnJlc3VsdCJcCg1EZWxldGVTdWNjZXNzEgwKBHBhdGgYASABKAkSFAoMZGVsZXRlZF9maWxlGAIgASgJEhEKCWZpbGVfc2l6ZRgDIAEoAxIUCgxwcmV2X2NvbnRlbnQYBCABKAkiIgoSRGVsZXRlRmlsZU5vdEZvdW5kEgwKBHBhdGgYASABKAkiMgoNRGVsZXRlTm90RmlsZRIMCgRwYXRoGAEgASgJEhMKC2FjdHVhbF90eXBlGAIgASgJIlkKFkRlbGV0ZVBlcm1pc3Npb25EZW5pZWQSDAoEcGF0aBgBIAEoCRIcChRjbGllbnRfdmlzaWJsZV9lcnJvchgCIAEoCRITCgtpc19yZWFkb25seRgDIAEoCCIeCg5EZWxldGVGaWxlQnVzeRIMCgRwYXRoGAEgASgJIi4KDkRlbGV0ZVJlamVjdGVkEgwKBHBhdGgYASABKAkSDgoGcmVhc29uGAIgASgJIioKC0RlbGV0ZUVycm9yEgwKBHBhdGgYASABKAkSDQoFZXJyb3IYAiABKAkiXAoORGVsZXRlVG9vbENhbGwSIgoEYXJncxgBIAEoCzIULmFnZW50LnYxLkRlbGV0ZUFyZ3MSJgoGcmVzdWx0GAIgASgLMhYuYWdlbnQudjEuRGVsZXRlUmVzdWx0IjUKD0RpYWdub3N0aWNzQXJncxIMCgRwYXRoGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCSKvAgoRRGlhZ25vc3RpY3NSZXN1bHQSLwoHc3VjY2VzcxgBIAEoCzIcLmFnZW50LnYxLkRpYWdub3N0aWNzU3VjY2Vzc0gAEisKBWVycm9yGAIgASgLMhouYWdlbnQudjEuRGlhZ25vc3RpY3NFcnJvckgAEjEKCHJlamVjdGVkGAMgASgLMh0uYWdlbnQudjEuRGlhZ25vc3RpY3NSZWplY3RlZEgAEjsKDmZpbGVfbm90X2ZvdW5kGAQgASgLMiEuYWdlbnQudjEuRGlhZ25vc3RpY3NGaWxlTm90Rm91bmRIABJCChFwZXJtaXNzaW9uX2RlbmllZBgFIAEoCzIlLmFnZW50LnYxLkRpYWdub3N0aWNzUGVybWlzc2lvbkRlbmllZEgAQggKBnJlc3VsdCJoChJEaWFnbm9zdGljc1N1Y2Nlc3MSDAoEcGF0aBgBIAEoCRIpCgtkaWFnbm9zdGljcxgCIAMoCzIULmFnZW50LnYxLkRpYWdub3N0aWMSGQoRdG90YWxfZGlhZ25vc3RpY3MYAyABKAUifwoKRGlhZ25vc3RpYxIQCghzZXZlcml0eRgBIAEoBRIeCgVyYW5nZRgCIAEoCzIPLmFnZW50LnYxLlJhbmdlEg8KB21lc3NhZ2UYAyABKAkSDgoGc291cmNlGAQgASgJEgwKBGNvZGUYBSABKAkSEAoIaXNfc3RhbGUYBiABKAgiLwoQRGlhZ25vc3RpY3NFcnJvchIMCgRwYXRoGAEgASgJEg0KBWVycm9yGAIgASgJIjMKE0RpYWdub3N0aWNzUmVqZWN0ZWQSDAoEcGF0aBgBIAEoCRIOCgZyZWFzb24YAiABKAkiJwoXRGlhZ25vc3RpY3NGaWxlTm90Rm91bmQSDAoEcGF0aBgBIAEoCSIrChtEaWFnbm9zdGljc1Blcm1pc3Npb25EZW5pZWQSDAoEcGF0aBgBIAEoCSJICghFZGl0QXJncxIMCgRwYXRoGAEgASgJEhsKDnN0cmVhbV9jb250ZW50GAYgASgJSACIAQFCEQoPX3N0cmVhbV9jb250ZW50ItYCCgpFZGl0UmVzdWx0EigKB3N1Y2Nlc3MYASABKAsyFS5hZ2VudC52MS5FZGl0U3VjY2Vzc0gAEjQKDmZpbGVfbm90X2ZvdW5kGAIgASgLMhouYWdlbnQudjEuRWRpdEZpbGVOb3RGb3VuZEgAEkQKFnJlYWRfcGVybWlzc2lvbl9kZW5pZWQYAyABKAsyIi5hZ2VudC52MS5FZGl0UmVhZFBlcm1pc3Npb25EZW5pZWRIABJGChd3cml0ZV9wZXJtaXNzaW9uX2RlbmllZBgEIAEoCzIjLmFnZW50LnYxLkVkaXRXcml0ZVBlcm1pc3Npb25EZW5pZWRIABIqCghyZWplY3RlZBgGIAEoCzIWLmFnZW50LnYxLkVkaXRSZWplY3RlZEgAEiQKBWVycm9yGAcgASgLMhMuYWdlbnQudjEuRWRpdEVycm9ySABCCAoGcmVzdWx0IqQCCgtFZGl0U3VjY2VzcxIMCgRwYXRoGAEgASgJEhgKC2xpbmVzX2FkZGVkGAMgASgFSACIAQESGgoNbGluZXNfcmVtb3ZlZBgEIAEoBUgBiAEBEhgKC2RpZmZfc3RyaW5nGAUgASgJSAKIAQESJQoYYmVmb3JlX2Z1bGxfZmlsZV9jb250ZW50GAYgASgJSAOIAQESHwoXYWZ0ZXJfZnVsbF9maWxlX2NvbnRlbnQYByABKAkSFAoHbWVzc2FnZRgIIAEoCUgEiAEBQg4KDF9saW5lc19hZGRlZEIQCg5fbGluZXNfcmVtb3ZlZEIOCgxfZGlmZl9zdHJpbmdCGwoZX2JlZm9yZV9mdWxsX2ZpbGVfY29udGVudEIKCghfbWVzc2FnZSIgChBFZGl0RmlsZU5vdEZvdW5kEgwKBHBhdGgYASABKAkiKAoYRWRpdFJlYWRQZXJtaXNzaW9uRGVuaWVkEgwKBHBhdGgYASABKAkiTQoZRWRpdFdyaXRlUGVybWlzc2lvbkRlbmllZBIMCgRwYXRoGAEgASgJEg0KBWVycm9yGAIgASgJEhMKC2lzX3JlYWRvbmx5GAMgASgIIiwKDEVkaXRSZWplY3RlZBIMCgRwYXRoGAEgASgJEg4KBnJlYXNvbhgCIAEoCSJiCglFZGl0RXJyb3ISDAoEcGF0aBgBIAEoCRINCgVlcnJvchgCIAEoCRIgChNtb2RlbF92aXNpYmxlX2Vycm9yGAUgASgJSACIAQFCFgoUX21vZGVsX3Zpc2libGVfZXJyb3IiVgoMRWRpdFRvb2xDYWxsEiAKBGFyZ3MYASABKAsyEi5hZ2VudC52MS5FZGl0QXJncxIkCgZyZXN1bHQYAiABKAsyFC5hZ2VudC52MS5FZGl0UmVzdWx0IjEKEUVkaXRUb29sQ2FsbERlbHRhEhwKFHN0cmVhbV9jb250ZW50X2RlbHRhGAEgASgJIjEKDEV4YUZldGNoQXJncxILCgNpZHMYASADKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJIqIBCg5FeGFGZXRjaFJlc3VsdBIsCgdzdWNjZXNzGAEgASgLMhkuYWdlbnQudjEuRXhhRmV0Y2hTdWNjZXNzSAASKAoFZXJyb3IYAiABKAsyFy5hZ2VudC52MS5FeGFGZXRjaEVycm9ySAASLgoIcmVqZWN0ZWQYAyABKAsyGi5hZ2VudC52MS5FeGFGZXRjaFJlamVjdGVkSABCCAoGcmVzdWx0Ij4KD0V4YUZldGNoU3VjY2VzcxIrCghjb250ZW50cxgBIAMoCzIZLmFnZW50LnYxLkV4YUZldGNoQ29udGVudCIeCg1FeGFGZXRjaEVycm9yEg0KBWVycm9yGAEgASgJIiIKEEV4YUZldGNoUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJIlMKD0V4YUZldGNoQ29udGVudBINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkSDAoEdGV4dBgDIAEoCRIWCg5wdWJsaXNoZWRfZGF0ZRgEIAEoCSJiChBFeGFGZXRjaFRvb2xDYWxsEiQKBGFyZ3MYASABKAsyFi5hZ2VudC52MS5FeGFGZXRjaEFyZ3MSKAoGcmVzdWx0GAIgASgLMhguYWdlbnQudjEuRXhhRmV0Y2hSZXN1bHQiPAoURXhhRmV0Y2hSZXF1ZXN0UXVlcnkSJAoEYXJncxgBIAEoCzIWLmFnZW50LnYxLkV4YUZldGNoQXJncyKjAQoXRXhhRmV0Y2hSZXF1ZXN0UmVzcG9uc2USPgoIYXBwcm92ZWQYASABKAsyKi5hZ2VudC52MS5FeGFGZXRjaFJlcXVlc3RSZXNwb25zZV9BcHByb3ZlZEgAEj4KCHJlamVjdGVkGAIgASgLMiouYWdlbnQudjEuRXhhRmV0Y2hSZXF1ZXN0UmVzcG9uc2VfUmVqZWN0ZWRIAEIICgZyZXN1bHQiIgogRXhhRmV0Y2hSZXF1ZXN0UmVzcG9uc2VfQXBwcm92ZWQiMgogRXhhRmV0Y2hSZXF1ZXN0UmVzcG9uc2VfUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJIlcKDUV4YVNlYXJjaEFyZ3MSDQoFcXVlcnkYASABKAkSDAoEdHlwZRgCIAEoCRITCgtudW1fcmVzdWx0cxgDIAEoBRIUCgx0b29sX2NhbGxfaWQYBCABKAkipgEKD0V4YVNlYXJjaFJlc3VsdBItCgdzdWNjZXNzGAEgASgLMhouYWdlbnQudjEuRXhhU2VhcmNoU3VjY2Vzc0gAEikKBWVycm9yGAIgASgLMhguYWdlbnQudjEuRXhhU2VhcmNoRXJyb3JIABIvCghyZWplY3RlZBgDIAEoCzIbLmFnZW50LnYxLkV4YVNlYXJjaFJlamVjdGVkSABCCAoGcmVzdWx0IkQKEEV4YVNlYXJjaFN1Y2Nlc3MSMAoKcmVmZXJlbmNlcxgBIAMoCzIcLmFnZW50LnYxLkV4YVNlYXJjaFJlZmVyZW5jZSIfCg5FeGFTZWFyY2hFcnJvchINCgVlcnJvchgBIAEoCSIjChFFeGFTZWFyY2hSZWplY3RlZBIOCgZyZWFzb24YASABKAkiVgoSRXhhU2VhcmNoUmVmZXJlbmNlEg0KBXRpdGxlGAEgASgJEgsKA3VybBgCIAEoCRIMCgR0ZXh0GAMgASgJEhYKDnB1Ymxpc2hlZF9kYXRlGAQgASgJImUKEUV4YVNlYXJjaFRvb2xDYWxsEiUKBGFyZ3MYASABKAsyFy5hZ2VudC52MS5FeGFTZWFyY2hBcmdzEikKBnJlc3VsdBgCIAEoCzIZLmFnZW50LnYxLkV4YVNlYXJjaFJlc3VsdCI+ChVFeGFTZWFyY2hSZXF1ZXN0UXVlcnkSJQoEYXJncxgBIAEoCzIXLmFnZW50LnYxLkV4YVNlYXJjaEFyZ3MipgEKGEV4YVNlYXJjaFJlcXVlc3RSZXNwb25zZRI/CghhcHByb3ZlZBgBIAEoCzIrLmFnZW50LnYxLkV4YVNlYXJjaFJlcXVlc3RSZXNwb25zZV9BcHByb3ZlZEgAEj8KCHJlamVjdGVkGAIgASgLMisuYWdlbnQudjEuRXhhU2VhcmNoUmVxdWVzdFJlc3BvbnNlX1JlamVjdGVkSABCCAoGcmVzdWx0IiMKIUV4YVNlYXJjaFJlcXVlc3RSZXNwb25zZV9BcHByb3ZlZCIzCiFFeGFTZWFyY2hSZXF1ZXN0UmVzcG9uc2VfUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJIiMKFUV4ZWNDbGllbnRTdHJlYW1DbG9zZRIKCgJpZBgBIAEoDSJWCg9FeGVjQ2xpZW50VGhyb3cSCgoCaWQYASABKA0SDQoFZXJyb3IYAiABKAkSGAoLc3RhY2tfdHJhY2UYAyABKAlIAIgBAUIOCgxfc3RhY2tfdHJhY2UiIQoTRXhlY0NsaWVudEhlYXJ0YmVhdBIKCgJpZBgBIAEoDSK+AQoYRXhlY0NsaWVudENvbnRyb2xNZXNzYWdlEjcKDHN0cmVhbV9jbG9zZRgBIAEoCzIfLmFnZW50LnYxLkV4ZWNDbGllbnRTdHJlYW1DbG9zZUgAEioKBXRocm93GAIgASgLMhkuYWdlbnQudjEuRXhlY0NsaWVudFRocm93SAASMgoJaGVhcnRiZWF0GAMgASgLMh0uYWdlbnQudjEuRXhlY0NsaWVudEhlYXJ0YmVhdEgAQgkKB21lc3NhZ2UihAEKC1NwYW5Db250ZXh0EhAKCHRyYWNlX2lkGAEgASgJEg8KB3NwYW5faWQYAiABKAkSGAoLdHJhY2VfZmxhZ3MYAyABKA1IAIgBARIYCgt0cmFjZV9zdGF0ZRgEIAEoCUgBiAEBQg4KDF90cmFjZV9mbGFnc0IOCgxfdHJhY2Vfc3RhdGUiCwoJQWJvcnRBcmdzIg0KC0Fib3J0UmVzdWx0IoUIChFFeGVjU2VydmVyTWVzc2FnZRIKCgJpZBgBIAEoDRIPCgdleGVjX2lkGA8gASgJEjAKDHNwYW5fY29udGV4dBgTIAEoCzIVLmFnZW50LnYxLlNwYW5Db250ZXh0SAGIAQESKQoKc2hlbGxfYXJncxgCIAEoCzITLmFnZW50LnYxLlNoZWxsQXJnc0gAEikKCndyaXRlX2FyZ3MYAyABKAsyEy5hZ2VudC52MS5Xcml0ZUFyZ3NIABIrCgtkZWxldGVfYXJncxgEIAEoCzIULmFnZW50LnYxLkRlbGV0ZUFyZ3NIABInCglncmVwX2FyZ3MYBSABKAsyEi5hZ2VudC52MS5HcmVwQXJnc0gAEicKCXJlYWRfYXJncxgHIAEoCzISLmFnZW50LnYxLlJlYWRBcmdzSAASIwoHbHNfYXJncxgIIAEoCzIQLmFnZW50LnYxLkxzQXJnc0gAEjUKEGRpYWdub3N0aWNzX2FyZ3MYCSABKAsyGS5hZ2VudC52MS5EaWFnbm9zdGljc0FyZ3NIABI8ChRyZXF1ZXN0X2NvbnRleHRfYXJncxgKIAEoCzIcLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0QXJnc0gAEiUKCG1jcF9hcmdzGAsgASgLMhEuYWdlbnQudjEuTWNwQXJnc0gAEjAKEXNoZWxsX3N0cmVhbV9hcmdzGA4gASgLMhMuYWdlbnQudjEuU2hlbGxBcmdzSAASSQobYmFja2dyb3VuZF9zaGVsbF9zcGF3bl9hcmdzGBAgASgLMiIuYWdlbnQudjEuQmFja2dyb3VuZFNoZWxsU3Bhd25BcmdzSAASSgocbGlzdF9tY3BfcmVzb3VyY2VzX2V4ZWNfYXJncxgRIAEoCzIiLmFnZW50LnYxLkxpc3RNY3BSZXNvdXJjZXNFeGVjQXJnc0gAEkgKG3JlYWRfbWNwX3Jlc291cmNlX2V4ZWNfYXJncxgSIAEoCzIhLmFnZW50LnYxLlJlYWRNY3BSZXNvdXJjZUV4ZWNBcmdzSAASKQoKZmV0Y2hfYXJncxgUIAEoCzITLmFnZW50LnYxLkZldGNoQXJnc0gAEjgKEnJlY29yZF9zY3JlZW5fYXJncxgVIAEoCzIaLmFnZW50LnYxLlJlY29yZFNjcmVlbkFyZ3NIABI2ChFjb21wdXRlcl91c2VfYXJncxgWIAEoCzIZLmFnZW50LnYxLkNvbXB1dGVyVXNlQXJnc0gAEj8KFndyaXRlX3NoZWxsX3N0ZGluX2FyZ3MYFyABKAsyHS5hZ2VudC52MS5Xcml0ZVNoZWxsU3RkaW5BcmdzSABCCQoHbWVzc2FnZUIPCg1fc3Bhbl9jb250ZXh0Iv8HChFFeGVjQ2xpZW50TWVzc2FnZRIKCgJpZBgBIAEoDRIPCgdleGVjX2lkGA8gASgJEi0KDHNoZWxsX3Jlc3VsdBgCIAEoCzIVLmFnZW50LnYxLlNoZWxsUmVzdWx0SAASLQoMd3JpdGVfcmVzdWx0GAMgASgLMhUuYWdlbnQudjEuV3JpdGVSZXN1bHRIABIvCg1kZWxldGVfcmVzdWx0GAQgASgLMhYuYWdlbnQudjEuRGVsZXRlUmVzdWx0SAASKwoLZ3JlcF9yZXN1bHQYBSABKAsyFC5hZ2VudC52MS5HcmVwUmVzdWx0SAASKwoLcmVhZF9yZXN1bHQYByABKAsyFC5hZ2VudC52MS5SZWFkUmVzdWx0SAASJwoJbHNfcmVzdWx0GAggASgLMhIuYWdlbnQudjEuTHNSZXN1bHRIABI5ChJkaWFnbm9zdGljc19yZXN1bHQYCSABKAsyGy5hZ2VudC52MS5EaWFnbm9zdGljc1Jlc3VsdEgAEkAKFnJlcXVlc3RfY29udGV4dF9yZXN1bHQYCiABKAsyHi5hZ2VudC52MS5SZXF1ZXN0Q29udGV4dFJlc3VsdEgAEikKCm1jcF9yZXN1bHQYCyABKAsyEy5hZ2VudC52MS5NY3BSZXN1bHRIABItCgxzaGVsbF9zdHJlYW0YDiABKAsyFS5hZ2VudC52MS5TaGVsbFN0cmVhbUgAEk0KHWJhY2tncm91bmRfc2hlbGxfc3Bhd25fcmVzdWx0GBAgASgLMiQuYWdlbnQudjEuQmFja2dyb3VuZFNoZWxsU3Bhd25SZXN1bHRIABJOCh5saXN0X21jcF9yZXNvdXJjZXNfZXhlY19yZXN1bHQYESABKAsyJC5hZ2VudC52MS5MaXN0TWNwUmVzb3VyY2VzRXhlY1Jlc3VsdEgAEkwKHXJlYWRfbWNwX3Jlc291cmNlX2V4ZWNfcmVzdWx0GBIgASgLMiMuYWdlbnQudjEuUmVhZE1jcFJlc291cmNlRXhlY1Jlc3VsdEgAEi0KDGZldGNoX3Jlc3VsdBgUIAEoCzIVLmFnZW50LnYxLkZldGNoUmVzdWx0SAASPAoUcmVjb3JkX3NjcmVlbl9yZXN1bHQYFSABKAsyHC5hZ2VudC52MS5SZWNvcmRTY3JlZW5SZXN1bHRIABI6ChNjb21wdXRlcl91c2VfcmVzdWx0GBYgASgLMhsuYWdlbnQudjEuQ29tcHV0ZXJVc2VSZXN1bHRIABJDChh3cml0ZV9zaGVsbF9zdGRpbl9yZXN1bHQYFyABKAsyHy5hZ2VudC52MS5Xcml0ZVNoZWxsU3RkaW5SZXN1bHRIAEIJCgdtZXNzYWdlIi4KCUZldGNoQXJncxILCgN1cmwYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJImkKC0ZldGNoUmVzdWx0EikKB3N1Y2Nlc3MYASABKAsyFi5hZ2VudC52MS5GZXRjaFN1Y2Nlc3NIABIlCgVlcnJvchgCIAEoCzIULmFnZW50LnYxLkZldGNoRXJyb3JIAEIICgZyZXN1bHQiVwoMRmV0Y2hTdWNjZXNzEgsKA3VybBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhMKC3N0YXR1c19jb2RlGAMgASgFEhQKDGNvbnRlbnRfdHlwZRgEIAEoCSIoCgpGZXRjaEVycm9yEgsKA3VybBgBIAEoCRINCgVlcnJvchgCIAEoCSJtChFHZW5lcmF0ZUltYWdlQXJncxITCgtkZXNjcmlwdGlvbhgBIAEoCRIWCglmaWxlX3BhdGgYAiABKAlIAIgBARIdChVyZWZlcmVuY2VfaW1hZ2VfcGF0aHMYBSADKAlCDAoKX2ZpbGVfcGF0aCKBAQoTR2VuZXJhdGVJbWFnZVJlc3VsdBIxCgdzdWNjZXNzGAEgASgLMh4uYWdlbnQudjEuR2VuZXJhdGVJbWFnZVN1Y2Nlc3NIABItCgVlcnJvchgCIAEoCzIcLmFnZW50LnYxLkdlbmVyYXRlSW1hZ2VFcnJvckgAQggKBnJlc3VsdCI9ChRHZW5lcmF0ZUltYWdlU3VjY2VzcxIRCglmaWxlX3BhdGgYASABKAkSEgoKaW1hZ2VfZGF0YRgCIAEoCSIjChJHZW5lcmF0ZUltYWdlRXJyb3ISDQoFZXJyb3IYASABKAkicQoVR2VuZXJhdGVJbWFnZVRvb2xDYWxsEikKBGFyZ3MYASABKAsyGy5hZ2VudC52MS5HZW5lcmF0ZUltYWdlQXJncxItCgZyZXN1bHQYAiABKAsyHS5hZ2VudC52MS5HZW5lcmF0ZUltYWdlUmVzdWx0IsYECghHcmVwQXJncxIPCgdwYXR0ZXJuGAEgASgJEhEKBHBhdGgYAiABKAlIAIgBARIRCgRnbG9iGAMgASgJSAGIAQESGAoLb3V0cHV0X21vZGUYBCABKAlIAogBARIbCg5jb250ZXh0X2JlZm9yZRgFIAEoBUgDiAEBEhoKDWNvbnRleHRfYWZ0ZXIYBiABKAVIBIgBARIUCgdjb250ZXh0GAcgASgFSAWIAQESHQoQY2FzZV9pbnNlbnNpdGl2ZRgIIAEoCEgGiAEBEhEKBHR5cGUYCSABKAlIB4gBARIXCgpoZWFkX2xpbWl0GAogASgFSAiIAQESFgoJbXVsdGlsaW5lGAsgASgISAmIAQESEQoEc29ydBgMIAEoCUgKiAEBEhsKDnNvcnRfYXNjZW5kaW5nGA0gASgISAuIAQESFAoMdG9vbF9jYWxsX2lkGA4gASgJEjQKDnNhbmRib3hfcG9saWN5GA8gASgLMhcuYWdlbnQudjEuU2FuZGJveFBvbGljeUgMiAEBQgcKBV9wYXRoQgcKBV9nbG9iQg4KDF9vdXRwdXRfbW9kZUIRCg9fY29udGV4dF9iZWZvcmVCEAoOX2NvbnRleHRfYWZ0ZXJCCgoIX2NvbnRleHRCEwoRX2Nhc2VfaW5zZW5zaXRpdmVCBwoFX3R5cGVCDQoLX2hlYWRfbGltaXRCDAoKX211bHRpbGluZUIHCgVfc29ydEIRCg9fc29ydF9hc2NlbmRpbmdCEQoPX3NhbmRib3hfcG9saWN5ImYKCkdyZXBSZXN1bHQSKAoHc3VjY2VzcxgBIAEoCzIVLmFnZW50LnYxLkdyZXBTdWNjZXNzSAASJAoFZXJyb3IYAiABKAsyEy5hZ2VudC52MS5HcmVwRXJyb3JIAEIICgZyZXN1bHQiGgoJR3JlcEVycm9yEg0KBWVycm9yGAEgASgJIrQCCgtHcmVwU3VjY2VzcxIPCgdwYXR0ZXJuGAEgASgJEgwKBHBhdGgYAiABKAkSEwoLb3V0cHV0X21vZGUYAyABKAkSRgoRd29ya3NwYWNlX3Jlc3VsdHMYBCADKAsyKy5hZ2VudC52MS5HcmVwU3VjY2Vzcy5Xb3Jrc3BhY2VSZXN1bHRzRW50cnkSPAoUYWN0aXZlX2VkaXRvcl9yZXN1bHQYBSABKAsyGS5hZ2VudC52MS5HcmVwVW5pb25SZXN1bHRIAIgBARpSChVXb3Jrc3BhY2VSZXN1bHRzRW50cnkSCwoDa2V5GAEgASgJEigKBXZhbHVlGAIgASgLMhkuYWdlbnQudjEuR3JlcFVuaW9uUmVzdWx0OgI4AUIXChVfYWN0aXZlX2VkaXRvcl9yZXN1bHQiowEKD0dyZXBVbmlvblJlc3VsdBIqCgVjb3VudBgBIAEoCzIZLmFnZW50LnYxLkdyZXBDb3VudFJlc3VsdEgAEioKBWZpbGVzGAIgASgLMhkuYWdlbnQudjEuR3JlcEZpbGVzUmVzdWx0SAASLgoHY29udGVudBgDIAEoCzIbLmFnZW50LnYxLkdyZXBDb250ZW50UmVzdWx0SABCCAoGcmVzdWx0IpsBCg9HcmVwQ291bnRSZXN1bHQSJwoGY291bnRzGAEgAygLMhcuYWdlbnQudjEuR3JlcEZpbGVDb3VudBITCgt0b3RhbF9maWxlcxgCIAEoBRIVCg10b3RhbF9tYXRjaGVzGAMgASgFEhgKEGNsaWVudF90cnVuY2F0ZWQYBCABKAgSGQoRcmlwZ3JlcF90cnVuY2F0ZWQYBSABKAgiLAoNR3JlcEZpbGVDb3VudBIMCgRmaWxlGAEgASgJEg0KBWNvdW50GAIgASgFImoKD0dyZXBGaWxlc1Jlc3VsdBINCgVmaWxlcxgBIAMoCRITCgt0b3RhbF9maWxlcxgCIAEoBRIYChBjbGllbnRfdHJ1bmNhdGVkGAMgASgIEhkKEXJpcGdyZXBfdHJ1bmNhdGVkGAQgASgIIqQBChFHcmVwQ29udGVudFJlc3VsdBIoCgdtYXRjaGVzGAEgAygLMhcuYWdlbnQudjEuR3JlcEZpbGVNYXRjaBITCgt0b3RhbF9saW5lcxgCIAEoBRIbChN0b3RhbF9tYXRjaGVkX2xpbmVzGAMgASgFEhgKEGNsaWVudF90cnVuY2F0ZWQYBCABKAgSGQoRcmlwZ3JlcF90cnVuY2F0ZWQYBSABKAgiSgoNR3JlcEZpbGVNYXRjaBIMCgRmaWxlGAEgASgJEisKB21hdGNoZXMYAiADKAsyGi5hZ2VudC52MS5HcmVwQ29udGVudE1hdGNoImwKEEdyZXBDb250ZW50TWF0Y2gSEwoLbGluZV9udW1iZXIYASABKAUSDwoHY29udGVudBgCIAEoCRIZChFjb250ZW50X3RydW5jYXRlZBgDIAEoCBIXCg9pc19jb250ZXh0X2xpbmUYBCABKAgiHQoKR3JlcFN0cmVhbRIPCgdwYXR0ZXJuGAEgASgJIlYKDEdyZXBUb29sQ2FsbBIgCgRhcmdzGAEgASgLMhIuYWdlbnQudjEuR3JlcEFyZ3MSJAoGcmVzdWx0GAIgASgLMhQuYWdlbnQudjEuR3JlcFJlc3VsdCIeCgtHZXRCbG9iQXJncxIPCgdibG9iX2lkGAEgASgMIjUKDUdldEJsb2JSZXN1bHQSFgoJYmxvYl9kYXRhGAEgASgMSACIAQFCDAoKX2Jsb2JfZGF0YSIxCgtTZXRCbG9iQXJncxIPCgdibG9iX2lkGAEgASgMEhEKCWJsb2JfZGF0YRgCIAEoDCI+Cg1TZXRCbG9iUmVzdWx0EiMKBWVycm9yGAEgASgLMg8uYWdlbnQudjEuRXJyb3JIAIgBAUIICgZfZXJyb3IiywEKD0t2U2VydmVyTWVzc2FnZRIKCgJpZBgBIAEoDRIwCgxzcGFuX2NvbnRleHQYBCABKAsyFS5hZ2VudC52MS5TcGFuQ29udGV4dEgBiAEBEi4KDWdldF9ibG9iX2FyZ3MYAiABKAsyFS5hZ2VudC52MS5HZXRCbG9iQXJnc0gAEi4KDXNldF9ibG9iX2FyZ3MYAyABKAsyFS5hZ2VudC52MS5TZXRCbG9iQXJnc0gAQgkKB21lc3NhZ2VCDwoNX3NwYW5fY29udGV4dCKQAQoPS3ZDbGllbnRNZXNzYWdlEgoKAmlkGAEgASgNEjIKD2dldF9ibG9iX3Jlc3VsdBgCIAEoCzIXLmFnZW50LnYxLkdldEJsb2JSZXN1bHRIABIyCg9zZXRfYmxvYl9yZXN1bHQYAyABKAsyFy5hZ2VudC52MS5TZXRCbG9iUmVzdWx0SABCCQoHbWVzc2FnZSKtAQoGTHNBcmdzEgwKBHBhdGgYASABKAkSDgoGaWdub3JlGAIgAygJEhQKDHRvb2xfY2FsbF9pZBgDIAEoCRI0Cg5zYW5kYm94X3BvbGljeRgEIAEoCzIXLmFnZW50LnYxLlNhbmRib3hQb2xpY3lIAIgBARIXCgp0aW1lb3V0X21zGAUgASgNSAGIAQFCEQoPX3NhbmRib3hfcG9saWN5Qg0KC190aW1lb3V0X21zIrIBCghMc1Jlc3VsdBImCgdzdWNjZXNzGAEgASgLMhMuYWdlbnQudjEuTHNTdWNjZXNzSAASIgoFZXJyb3IYAiABKAsyES5hZ2VudC52MS5Mc0Vycm9ySAASKAoIcmVqZWN0ZWQYAyABKAsyFC5hZ2VudC52MS5Mc1JlamVjdGVkSAASJgoHdGltZW91dBgEIAEoCzITLmFnZW50LnYxLkxzVGltZW91dEgAQggKBnJlc3VsdCJHCglMc1N1Y2Nlc3MSOgoTZGlyZWN0b3J5X3RyZWVfcm9vdBgBIAEoCzIdLmFnZW50LnYxLkxzRGlyZWN0b3J5VHJlZU5vZGUi9gIKE0xzRGlyZWN0b3J5VHJlZU5vZGUSEAoIYWJzX3BhdGgYASABKAkSNAoNY2hpbGRyZW5fZGlycxgCIAMoCzIdLmFnZW50LnYxLkxzRGlyZWN0b3J5VHJlZU5vZGUSOgoOY2hpbGRyZW5fZmlsZXMYAyADKAsyIi5hZ2VudC52MS5Mc0RpcmVjdG9yeVRyZWVOb2RlX0ZpbGUSHwoXY2hpbGRyZW5fd2VyZV9wcm9jZXNzZWQYBCABKAgSZAodZnVsbF9zdWJ0cmVlX2V4dGVuc2lvbl9jb3VudHMYBSADKAsyPS5hZ2VudC52MS5Mc0RpcmVjdG9yeVRyZWVOb2RlLkZ1bGxTdWJ0cmVlRXh0ZW5zaW9uQ291bnRzRW50cnkSEQoJbnVtX2ZpbGVzGAYgASgFGkEKH0Z1bGxTdWJ0cmVlRXh0ZW5zaW9uQ291bnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgFOgI4ASJ6ChhMc0RpcmVjdG9yeVRyZWVOb2RlX0ZpbGUSDAoEbmFtZRgBIAEoCRI6ChF0ZXJtaW5hbF9tZXRhZGF0YRgCIAEoCzIaLmFnZW50LnYxLlRlcm1pbmFsTWV0YWRhdGFIAIgBAUIUChJfdGVybWluYWxfbWV0YWRhdGEiJgoHTHNFcnJvchIMCgRwYXRoGAEgASgJEg0KBWVycm9yGAIgASgJIioKCkxzUmVqZWN0ZWQSDAoEcGF0aBgBIAEoCRIOCgZyZWFzb24YAiABKAkiRwoJTHNUaW1lb3V0EjoKE2RpcmVjdG9yeV90cmVlX3Jvb3QYASABKAsyHS5hZ2VudC52MS5Mc0RpcmVjdG9yeVRyZWVOb2RlIvEBChBUZXJtaW5hbE1ldGFkYXRhEhAKA2N3ZBgBIAEoCUgAiAEBEjkKDWxhc3RfY29tbWFuZHMYAiADKAsyIi5hZ2VudC52MS5UZXJtaW5hbE1ldGFkYXRhX0NvbW1hbmQSHQoQbGFzdF9tb2RpZmllZF9tcxgDIAEoA0gBiAEBEkAKD2N1cnJlbnRfY29tbWFuZBgEIAEoCzIiLmFnZW50LnYxLlRlcm1pbmFsTWV0YWRhdGFfQ29tbWFuZEgCiAEBQgYKBF9jd2RCEwoRX2xhc3RfbW9kaWZpZWRfbXNCEgoQX2N1cnJlbnRfY29tbWFuZCKnAQoYVGVybWluYWxNZXRhZGF0YV9Db21tYW5kEg8KB2NvbW1hbmQYASABKAkSFgoJZXhpdF9jb2RlGAIgASgFSACIAQESGQoMdGltZXN0YW1wX21zGAMgASgDSAGIAQESGAoLZHVyYXRpb25fbXMYBCABKANIAogBAUIMCgpfZXhpdF9jb2RlQg8KDV90aW1lc3RhbXBfbXNCDgoMX2R1cmF0aW9uX21zIlAKCkxzVG9vbENhbGwSHgoEYXJncxgBIAEoCzIQLmFnZW50LnYxLkxzQXJncxIiCgZyZXN1bHQYAiABKAsyEi5hZ2VudC52MS5Mc1Jlc3VsdCK1AQoHTWNwQXJncxIMCgRuYW1lGAEgASgJEikKBGFyZ3MYAiADKAsyGy5hZ2VudC52MS5NY3BBcmdzLkFyZ3NFbnRyeRIUCgx0b29sX2NhbGxfaWQYAyABKAkSGwoTcHJvdmlkZXJfaWRlbnRpZmllchgEIAEoCRIRCgl0b29sX25hbWUYBSABKAkaKwoJQXJnc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoDDoCOAEi/wEKCU1jcFJlc3VsdBInCgdzdWNjZXNzGAEgASgLMhQuYWdlbnQudjEuTWNwU3VjY2Vzc0gAEiMKBWVycm9yGAIgASgLMhIuYWdlbnQudjEuTWNwRXJyb3JIABIpCghyZWplY3RlZBgDIAEoCzIVLmFnZW50LnYxLk1jcFJlamVjdGVkSAASOgoRcGVybWlzc2lvbl9kZW5pZWQYBCABKAsyHS5hZ2VudC52MS5NY3BQZXJtaXNzaW9uRGVuaWVkSAASMwoOdG9vbF9ub3RfZm91bmQYBSABKAsyGS5hZ2VudC52MS5NY3BUb29sTm90Rm91bmRIAEIICgZyZXN1bHQiOAoPTWNwVG9vbE5vdEZvdW5kEgwKBG5hbWUYASABKAkSFwoPYXZhaWxhYmxlX3Rvb2xzGAIgAygJImoKDk1jcFRleHRDb250ZW50EgwKBHRleHQYASABKAkSNgoPb3V0cHV0X2xvY2F0aW9uGAIgASgLMhguYWdlbnQudjEuT3V0cHV0TG9jYXRpb25IAIgBAUISChBfb3V0cHV0X2xvY2F0aW9uIjIKD01jcEltYWdlQ29udGVudBIMCgRkYXRhGAEgASgMEhEKCW1pbWVfdHlwZRgCIAEoCSJ7ChhNY3BUb29sUmVzdWx0Q29udGVudEl0ZW0SKAoEdGV4dBgBIAEoCzIYLmFnZW50LnYxLk1jcFRleHRDb250ZW50SAASKgoFaW1hZ2UYAiABKAsyGS5hZ2VudC52MS5NY3BJbWFnZUNvbnRlbnRIAEIJCgdjb250ZW50IlMKCk1jcFN1Y2Nlc3MSMwoHY29udGVudBgBIAMoCzIiLmFnZW50LnYxLk1jcFRvb2xSZXN1bHRDb250ZW50SXRlbRIQCghpc19lcnJvchgCIAEoCCIZCghNY3BFcnJvchINCgVlcnJvchgBIAEoCSIyCgtNY3BSZWplY3RlZBIOCgZyZWFzb24YASABKAkSEwoLaXNfcmVhZG9ubHkYAiABKAgiOQoTTWNwUGVybWlzc2lvbkRlbmllZBINCgVlcnJvchgBIAEoCRITCgtpc19yZWFkb25seRgCIAEoCCI6ChhMaXN0TWNwUmVzb3VyY2VzRXhlY0FyZ3MSEwoGc2VydmVyGAEgASgJSACIAQFCCQoHX3NlcnZlciLGAQoaTGlzdE1jcFJlc291cmNlc0V4ZWNSZXN1bHQSNAoHc3VjY2VzcxgBIAEoCzIhLmFnZW50LnYxLkxpc3RNY3BSZXNvdXJjZXNTdWNjZXNzSAASMAoFZXJyb3IYAiABKAsyHy5hZ2VudC52MS5MaXN0TWNwUmVzb3VyY2VzRXJyb3JIABI2CghyZWplY3RlZBgDIAEoCzIiLmFnZW50LnYxLkxpc3RNY3BSZXNvdXJjZXNSZWplY3RlZEgAQggKBnJlc3VsdCK9AgomTGlzdE1jcFJlc291cmNlc0V4ZWNSZXN1bHRfTWNwUmVzb3VyY2USCwoDdXJpGAEgASgJEhEKBG5hbWUYAiABKAlIAIgBARIYCgtkZXNjcmlwdGlvbhgDIAEoCUgBiAEBEhYKCW1pbWVfdHlwZRgEIAEoCUgCiAEBEg4KBnNlcnZlchgFIAEoCRJWCgthbm5vdGF0aW9ucxgGIAMoCzJBLmFnZW50LnYxLkxpc3RNY3BSZXNvdXJjZXNFeGVjUmVzdWx0X01jcFJlc291cmNlLkFubm90YXRpb25zRW50cnkaMgoQQW5ub3RhdGlvbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgcKBV9uYW1lQg4KDF9kZXNjcmlwdGlvbkIMCgpfbWltZV90eXBlIl4KF0xpc3RNY3BSZXNvdXJjZXNTdWNjZXNzEkMKCXJlc291cmNlcxgBIAMoCzIwLmFnZW50LnYxLkxpc3RNY3BSZXNvdXJjZXNFeGVjUmVzdWx0X01jcFJlc291cmNlIiYKFUxpc3RNY3BSZXNvdXJjZXNFcnJvchINCgVlcnJvchgBIAEoCSIqChhMaXN0TWNwUmVzb3VyY2VzUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJImQKF1JlYWRNY3BSZXNvdXJjZUV4ZWNBcmdzEg4KBnNlcnZlchgBIAEoCRILCgN1cmkYAiABKAkSGgoNZG93bmxvYWRfcGF0aBgDIAEoCUgAiAEBQhAKDl9kb3dubG9hZF9wYXRoIvoBChlSZWFkTWNwUmVzb3VyY2VFeGVjUmVzdWx0EjMKB3N1Y2Nlc3MYASABKAsyIC5hZ2VudC52MS5SZWFkTWNwUmVzb3VyY2VTdWNjZXNzSAASLwoFZXJyb3IYAiABKAsyHi5hZ2VudC52MS5SZWFkTWNwUmVzb3VyY2VFcnJvckgAEjUKCHJlamVjdGVkGAMgASgLMiEuYWdlbnQudjEuUmVhZE1jcFJlc291cmNlUmVqZWN0ZWRIABI2Cglub3RfZm91bmQYBCABKAsyIS5hZ2VudC52MS5SZWFkTWNwUmVzb3VyY2VOb3RGb3VuZEgAQggKBnJlc3VsdCLmAgoWUmVhZE1jcFJlc291cmNlU3VjY2VzcxILCgN1cmkYASABKAkSEQoEbmFtZRgCIAEoCUgBiAEBEhgKC2Rlc2NyaXB0aW9uGAMgASgJSAKIAQESFgoJbWltZV90eXBlGAQgASgJSAOIAQESRgoLYW5ub3RhdGlvbnMYByADKAsyMS5hZ2VudC52MS5SZWFkTWNwUmVzb3VyY2VTdWNjZXNzLkFubm90YXRpb25zRW50cnkSGgoNZG93bmxvYWRfcGF0aBgIIAEoCUgEiAEBEg4KBHRleHQYBSABKAlIABIOCgRibG9iGAYgASgMSAAaMgoQQW5ub3RhdGlvbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgkKB2NvbnRlbnRCBwoFX25hbWVCDgoMX2Rlc2NyaXB0aW9uQgwKCl9taW1lX3R5cGVCEAoOX2Rvd25sb2FkX3BhdGgiMgoUUmVhZE1jcFJlc291cmNlRXJyb3ISCwoDdXJpGAEgASgJEg0KBWVycm9yGAIgASgJIjYKF1JlYWRNY3BSZXNvdXJjZVJlamVjdGVkEgsKA3VyaRgBIAEoCRIOCgZyZWFzb24YAiABKAkiJgoXUmVhZE1jcFJlc291cmNlTm90Rm91bmQSCwoDdXJpGAEgASgJInwKEU1jcFRvb2xEZWZpbml0aW9uEgwKBG5hbWUYASABKAkSGwoTcHJvdmlkZXJfaWRlbnRpZmllchgEIAEoCRIRCgl0b29sX25hbWUYBSABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSFAoMaW5wdXRfc2NoZW1hGAMgASgMIjoKCE1jcFRvb2xzEi4KCW1jcF90b29scxgBIAMoCzIbLmFnZW50LnYxLk1jcFRvb2xEZWZpbml0aW9uIjwKD01jcEluc3RydWN0aW9ucxITCgtzZXJ2ZXJfbmFtZRgBIAEoCRIUCgxpbnN0cnVjdGlvbnMYAiABKAki1wEKDU1jcERlc2NyaXB0b3ISEwoLc2VydmVyX25hbWUYASABKAkSGQoRc2VydmVyX2lkZW50aWZpZXIYAiABKAkSGAoLZm9sZGVyX3BhdGgYAyABKAlIAIgBARIkChdzZXJ2ZXJfdXNlX2luc3RydWN0aW9ucxgEIAEoCUgBiAEBEioKBXRvb2xzGAUgAygLMhsuYWdlbnQudjEuTWNwVG9vbERlc2NyaXB0b3JCDgoMX2ZvbGRlcl9wYXRoQhoKGF9zZXJ2ZXJfdXNlX2luc3RydWN0aW9ucyJYChFNY3BUb29sRGVzY3JpcHRvchIRCgl0b29sX25hbWUYASABKAkSHAoPZGVmaW5pdGlvbl9wYXRoGAIgASgJSACIAQFCEgoQX2RlZmluaXRpb25fcGF0aCJ4ChRNY3BGaWxlU3lzdGVtT3B0aW9ucxIPCgdlbmFibGVkGAEgASgIEh0KFXdvcmtzcGFjZV9wcm9qZWN0X2RpchgCIAEoCRIwCg9tY3BfZGVzY3JpcHRvcnMYAyADKAsyFy5hZ2VudC52MS5NY3BEZXNjcmlwdG9yIi4KCFJlYWRBcmdzEgwKBHBhdGgYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJIrgCCgpSZWFkUmVzdWx0EigKB3N1Y2Nlc3MYASABKAsyFS5hZ2VudC52MS5SZWFkU3VjY2Vzc0gAEiQKBWVycm9yGAIgASgLMhMuYWdlbnQudjEuUmVhZEVycm9ySAASKgoIcmVqZWN0ZWQYAyABKAsyFi5hZ2VudC52MS5SZWFkUmVqZWN0ZWRIABI0Cg5maWxlX25vdF9mb3VuZBgEIAEoCzIaLmFnZW50LnYxLlJlYWRGaWxlTm90Rm91bmRIABI7ChFwZXJtaXNzaW9uX2RlbmllZBgFIAEoCzIeLmFnZW50LnYxLlJlYWRQZXJtaXNzaW9uRGVuaWVkSAASMQoMaW52YWxpZF9maWxlGAYgASgLMhkuYWdlbnQudjEuUmVhZEludmFsaWRGaWxlSABCCAoGcmVzdWx0IrMBCgtSZWFkU3VjY2VzcxIMCgRwYXRoGAEgASgJEhMKC3RvdGFsX2xpbmVzGAMgASgFEhEKCWZpbGVfc2l6ZRgEIAEoAxIRCgl0cnVuY2F0ZWQYBiABKAgSGwoOb3V0cHV0X2Jsb2JfaWQYByABKAxIAYgBARIRCgdjb250ZW50GAIgASgJSAASDgoEZGF0YRgFIAEoDEgAQggKBm91dHB1dEIRCg9fb3V0cHV0X2Jsb2JfaWQiKAoJUmVhZEVycm9yEgwKBHBhdGgYASABKAkSDQoFZXJyb3IYAiABKAkiLAoMUmVhZFJlamVjdGVkEgwKBHBhdGgYASABKAkSDgoGcmVhc29uGAIgASgJIiAKEFJlYWRGaWxlTm90Rm91bmQSDAoEcGF0aBgBIAEoCSIkChRSZWFkUGVybWlzc2lvbkRlbmllZBIMCgRwYXRoGAEgASgJIi8KD1JlYWRJbnZhbGlkRmlsZRIMCgRwYXRoGAEgASgJEg4KBnJlYXNvbhgCIAEoCSJeCgxSZWFkVG9vbENhbGwSJAoEYXJncxgBIAEoCzIWLmFnZW50LnYxLlJlYWRUb29sQXJncxIoCgZyZXN1bHQYAiABKAsyGC5hZ2VudC52MS5SZWFkVG9vbFJlc3VsdCJaCgxSZWFkVG9vbEFyZ3MSDAoEcGF0aBgBIAEoCRITCgZvZmZzZXQYAiABKAVIAIgBARISCgVsaW1pdBgDIAEoBUgBiAEBQgkKB19vZmZzZXRCCAoGX2xpbWl0InIKDlJlYWRUb29sUmVzdWx0EiwKB3N1Y2Nlc3MYASABKAsyGS5hZ2VudC52MS5SZWFkVG9vbFN1Y2Nlc3NIABIoCgVlcnJvchgCIAEoCzIXLmFnZW50LnYxLlJlYWRUb29sRXJyb3JIAEIICgZyZXN1bHQiMQoJUmVhZFJhbmdlEhIKCnN0YXJ0X2xpbmUYASABKA0SEAoIZW5kX2xpbmUYAiABKA0ijgIKD1JlYWRUb29sU3VjY2VzcxIQCghpc19lbXB0eRgCIAEoCBIWCg5leGNlZWRlZF9saW1pdBgDIAEoCBITCgt0b3RhbF9saW5lcxgEIAEoDRIRCglmaWxlX3NpemUYBSABKA0SDAoEcGF0aBgHIAEoCRIsCgpyZWFkX3JhbmdlGAggASgLMhMuYWdlbnQudjEuUmVhZFJhbmdlSAGIAQESEQoHY29udGVudBgBIAEoCUgAEg4KBGRhdGEYBiABKAxIABIWCgxkYXRhX2Jsb2JfaWQYCSABKAxIABIZCg9jb250ZW50X2Jsb2JfaWQYCiABKAxIAEIICgZvdXRwdXRCDQoLX3JlYWRfcmFuZ2UiJgoNUmVhZFRvb2xFcnJvchIVCg1lcnJvcl9tZXNzYWdlGAEgASgJImoKEFJlY29yZFNjcmVlbkFyZ3MSDAoEbW9kZRgBIAEoBRIUCgx0b29sX2NhbGxfaWQYAiABKAkSHQoQc2F2ZV9hc19maWxlbmFtZRgDIAEoCUgAiAEBQhMKEV9zYXZlX2FzX2ZpbGVuYW1lIokCChJSZWNvcmRTY3JlZW5SZXN1bHQSOwoNc3RhcnRfc3VjY2VzcxgBIAEoCzIiLmFnZW50LnYxLlJlY29yZFNjcmVlblN0YXJ0U3VjY2Vzc0gAEjkKDHNhdmVfc3VjY2VzcxgCIAEoCzIhLmFnZW50LnYxLlJlY29yZFNjcmVlblNhdmVTdWNjZXNzSAASPwoPZGlzY2FyZF9zdWNjZXNzGAMgASgLMiQuYWdlbnQudjEuUmVjb3JkU2NyZWVuRGlzY2FyZFN1Y2Nlc3NIABIwCgdmYWlsdXJlGAQgASgLMh0uYWdlbnQudjEuUmVjb3JkU2NyZWVuRmFpbHVyZUgAQggKBnJlc3VsdCJnChhSZWNvcmRTY3JlZW5TdGFydFN1Y2Nlc3MSJQodd2FzX3ByaW9yX3JlY29yZGluZ19jYW5jZWxsZWQYASABKAgSJAocd2FzX3NhdmVfYXNfZmlsZW5hbWVfaWdub3JlZBgCIAEoCCKgAQoXUmVjb3JkU2NyZWVuU2F2ZVN1Y2Nlc3MSDAoEcGF0aBgBIAEoCRIdChVyZWNvcmRpbmdfZHVyYXRpb25fbXMYAiABKAMSMAojcmVxdWVzdGVkX2ZpbGVfcGF0aF9yZWplY3RlZF9yZWFzb24YAyABKAVIAIgBAUImCiRfcmVxdWVzdGVkX2ZpbGVfcGF0aF9yZWplY3RlZF9yZWFzb24iHAoaUmVjb3JkU2NyZWVuRGlzY2FyZFN1Y2Nlc3MiJAoTUmVjb3JkU2NyZWVuRmFpbHVyZRINCgVlcnJvchgBIAEoCSI2ChNDdXJzb3JQYWNrYWdlUHJvbXB0EgwKBG5hbWUYASABKAkSEQoJZmlsZV9wYXRoGAIgASgJIuIBCg1DdXJzb3JQYWNrYWdlEgwKBG5hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSEwoLZm9sZGVyX3BhdGgYAyABKAkSDwoHZW5hYmxlZBgEIAEoCBIYCgtwYXJzZV9lcnJvchgFIAEoCUgAiAEBEi4KB3Byb21wdHMYBiADKAsyHS5hZ2VudC52MS5DdXJzb3JQYWNrYWdlUHJvbXB0EhgKEHJlYWRtZV9maWxlX3BhdGgYByABKAkSFAoMcGFja2FnZV90eXBlGAggASgFQg4KDF9wYXJzZV9lcnJvciKrAgoWUmVwb3NpdG9yeUluZGV4aW5nSW5mbxIfChdyZWxhdGl2ZV93b3Jrc3BhY2VfcGF0aBgBIAEoCRITCgtyZW1vdGVfdXJscxgCIAMoCRIUCgxyZW1vdGVfbmFtZXMYAyADKAkSEQoJcmVwb19uYW1lGAQgASgJEhIKCnJlcG9fb3duZXIYBSABKAkSEgoKaXNfdHJhY2tlZBgGIAEoCBIQCghpc19sb2NhbBgHIAEoCBImChlvcnRob2dvbmFsX3RyYW5zZm9ybV9zZWVkGAggASgBSACIAQESFQoNd29ya3NwYWNlX3VyaRgJIAEoCRIbChNwYXRoX2VuY3J5cHRpb25fa2V5GAogASgJQhwKGl9vcnRob2dvbmFsX3RyYW5zZm9ybV9zZWVkInQKElJlcXVlc3RDb250ZXh0QXJncxIdChBub3Rlc19zZXNzaW9uX2lkGAIgASgJSACIAQESGQoMd29ya3NwYWNlX2lkGAMgASgJSAGIAQFCEwoRX25vdGVzX3Nlc3Npb25faWRCDwoNX3dvcmtzcGFjZV9pZCK6AQoUUmVxdWVzdENvbnRleHRSZXN1bHQSMgoHc3VjY2VzcxgBIAEoCzIfLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0U3VjY2Vzc0gAEi4KBWVycm9yGAIgASgLMh0uYWdlbnQudjEuUmVxdWVzdENvbnRleHRFcnJvckgAEjQKCHJlamVjdGVkGAMgASgLMiAuYWdlbnQudjEuUmVxdWVzdENvbnRleHRSZWplY3RlZEgAQggKBnJlc3VsdCJKChVSZXF1ZXN0Q29udGV4dFN1Y2Nlc3MSMQoPcmVxdWVzdF9jb250ZXh0GAEgASgLMhguYWdlbnQudjEuUmVxdWVzdENvbnRleHQiJAoTUmVxdWVzdENvbnRleHRFcnJvchINCgVlcnJvchgBIAEoCSIoChZSZXF1ZXN0Q29udGV4dFJlamVjdGVkEg4KBnJlYXNvbhgBIAEoCSLCAQoKSW1hZ2VQcm90bxIMCgRkYXRhGAEgASgMEgwKBHV1aWQYAiABKAkSDAoEcGF0aBgDIAEoCRIxCglkaW1lbnNpb24YBCABKAsyHi5hZ2VudC52MS5JbWFnZVByb3RvX0RpbWVuc2lvbhImChl0YXNrX3NwZWNpZmljX2Rlc2NyaXB0aW9uGAYgASgJSACIAQESEQoJbWltZV90eXBlGAcgASgJQhwKGl90YXNrX3NwZWNpZmljX2Rlc2NyaXB0aW9uIjUKFEltYWdlUHJvdG9fRGltZW5zaW9uEg0KBXdpZHRoGAEgASgFEg4KBmhlaWdodBgCIAEoBSJoCgtHaXRSZXBvSW5mbxIMCgRwYXRoGAEgASgJEg4KBnN0YXR1cxgCIAEoCRITCgticmFuY2hfbmFtZRgDIAEoCRIXCgpyZW1vdGVfdXJsGAQgASgJSACIAQFCDQoLX3JlbW90ZV91cmwimwIKEVJlcXVlc3RDb250ZXh0RW52EhIKCm9zX3ZlcnNpb24YASABKAkSFwoPd29ya3NwYWNlX3BhdGhzGAIgAygJEg0KBXNoZWxsGAMgASgJEhcKD3NhbmRib3hfZW5hYmxlZBgFIAEoCBIYChB0ZXJtaW5hbHNfZm9sZGVyGAcgASgJEiEKGWFnZW50X3NoYXJlZF9ub3Rlc19mb2xkZXIYCCABKAkSJwofYWdlbnRfY29udmVyc2F0aW9uX25vdGVzX2ZvbGRlchgJIAEoCRIRCgl0aW1lX3pvbmUYCiABKAkSFgoOcHJvamVjdF9mb2xkZXIYCyABKAkSIAoYYWdlbnRfdHJhbnNjcmlwdHNfZm9sZGVyGAwgASgJIjwKD0RlYnVnTW9kZUNvbmZpZxIQCghsb2dfcGF0aBgBIAEoCRIXCg9zZXJ2ZXJfZW5kcG9pbnQYAiABKAkitAEKD1NraWxsRGVzY3JpcHRvchIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhMKC2ZvbGRlcl9wYXRoGAMgASgJEg8KB2VuYWJsZWQYBCABKAgSGAoLcGFyc2VfZXJyb3IYBSABKAlIAIgBARIYChByZWFkbWVfZmlsZV9wYXRoGAYgASgJEhQKDHBhY2thZ2VfdHlwZRgHIAEoBUIOCgxfcGFyc2VfZXJyb3IiRAoMU2tpbGxPcHRpb25zEjQKEXNraWxsX2Rlc2NyaXB0b3JzGAEgAygLMhkuYWdlbnQudjEuU2tpbGxEZXNjcmlwdG9yIvYICg5SZXF1ZXN0Q29udGV4dBIjCgVydWxlcxgCIAMoCzIULmFnZW50LnYxLkN1cnNvclJ1bGUSKAoDZW52GAQgASgLMhsuYWdlbnQudjEuUmVxdWVzdENvbnRleHRFbnYSOQoPcmVwb3NpdG9yeV9pbmZvGAYgAygLMiAuYWdlbnQudjEuUmVwb3NpdG9yeUluZGV4aW5nSW5mbxIqCgV0b29scxgHIAMoCzIbLmFnZW50LnYxLk1jcFRvb2xEZWZpbml0aW9uEicKGmNvbnZlcnNhdGlvbl9ub3Rlc19saXN0aW5nGAggASgJSACIAQESIQoUc2hhcmVkX25vdGVzX2xpc3RpbmcYCSABKAlIAYgBARIoCglnaXRfcmVwb3MYCyADKAsyFS5hZ2VudC52MS5HaXRSZXBvSW5mbxI2Cg9wcm9qZWN0X2xheW91dHMYDSADKAsyHS5hZ2VudC52MS5Mc0RpcmVjdG9yeVRyZWVOb2RlEjMKEG1jcF9pbnN0cnVjdGlvbnMYDiADKAsyGS5hZ2VudC52MS5NY3BJbnN0cnVjdGlvbnMSOQoRZGVidWdfbW9kZV9jb25maWcYDyABKAsyGS5hZ2VudC52MS5EZWJ1Z01vZGVDb25maWdIAogBARIXCgpjbG91ZF9ydWxlGBAgASgJSAOIAQESHwoSd2ViX3NlYXJjaF9lbmFibGVkGBEgASgISASIAQESMgoNc2tpbGxfb3B0aW9ucxgSIAEoCzIWLmFnZW50LnYxLlNraWxsT3B0aW9uc0gFiAEBEi4KIXJlcG9zaXRvcnlfaW5mb19zaG91bGRfcXVlcnlfcHJvZBgTIAEoCEgGiAEBEkEKDWZpbGVfY29udGVudHMYFCADKAsyKi5hZ2VudC52MS5SZXF1ZXN0Q29udGV4dC5GaWxlQ29udGVudHNFbnRyeRIgChN1c2VyX2ludGVudF9zdW1tYXJ5GBUgASgJSAeIAQESMgoQY3VzdG9tX3N1YmFnZW50cxgWIAMoCzIYLmFnZW50LnYxLkN1c3RvbVN1YmFnZW50EkQKF21jcF9maWxlX3N5c3RlbV9vcHRpb25zGBcgASgLMh4uYWdlbnQudjEuTWNwRmlsZVN5c3RlbU9wdGlvbnNICIgBARozChFGaWxlQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQh0KG19jb252ZXJzYXRpb25fbm90ZXNfbGlzdGluZ0IXChVfc2hhcmVkX25vdGVzX2xpc3RpbmdCFAoSX2RlYnVnX21vZGVfY29uZmlnQg0KC19jbG91ZF9ydWxlQhUKE193ZWJfc2VhcmNoX2VuYWJsZWRCEAoOX3NraWxsX29wdGlvbnNCJAoiX3JlcG9zaXRvcnlfaW5mb19zaG91bGRfcXVlcnlfcHJvZEIWChRfdXNlcl9pbnRlbnRfc3VtbWFyeUIaChhfbWNwX2ZpbGVfc3lzdGVtX29wdGlvbnMisgIKDVNhbmRib3hQb2xpY3kSDAoEdHlwZRgBIAEoBRIbCg5uZXR3b3JrX2FjY2VzcxgCIAEoCEgAiAEBEiIKGmFkZGl0aW9uYWxfcmVhZHdyaXRlX3BhdGhzGAMgAygJEiEKGWFkZGl0aW9uYWxfcmVhZG9ubHlfcGF0aHMYBCADKAkSHQoQZGVidWdfb3V0cHV0X2RpchgFIAEoCUgBiAEBEh0KEGJsb2NrX2dpdF93cml0ZXMYBiABKAhIAogBARIeChFkaXNhYmxlX3RtcF93cml0ZRgHIAEoCEgDiAEBQhEKD19uZXR3b3JrX2FjY2Vzc0ITChFfZGVidWdfb3V0cHV0X2RpckITChFfYmxvY2tfZ2l0X3dyaXRlc0IUChJfZGlzYWJsZV90bXBfd3JpdGUi7wEKDVNlbGVjdGVkSW1hZ2USDAoEdXVpZBgCIAEoCRIMCgRwYXRoGAMgASgJEjQKCWRpbWVuc2lvbhgEIAEoCzIhLmFnZW50LnYxLlNlbGVjdGVkSW1hZ2VfRGltZW5zaW9uEhEKCW1pbWVfdHlwZRgHIAEoCRIRCgdibG9iX2lkGAEgASgMSAASDgoEZGF0YRgIIAEoDEgAEkMKEWJsb2JfaWRfd2l0aF9kYXRhGAkgASgLMiYuYWdlbnQudjEuU2VsZWN0ZWRJbWFnZV9CbG9iSWRXaXRoRGF0YUgAQhEKD2RhdGFfb3JfYmxvYl9pZCI9ChxTZWxlY3RlZEltYWdlX0Jsb2JJZFdpdGhEYXRhEg8KB2Jsb2JfaWQYASABKAwSDAoEZGF0YRgCIAEoDCI4ChdTZWxlY3RlZEltYWdlX0RpbWVuc2lvbhINCgV3aWR0aBgBIAEoBRIOCgZoZWlnaHQYAiABKAUiSQoRRXh0cmFDb250ZXh0RW50cnkSDgoEZGF0YRgBIAEoCUgAEhEKB2Jsb2JfaWQYAiABKAxIAEIRCg9kYXRhX29yX2Jsb2JfaWQiWwoMU2VsZWN0ZWRGaWxlEg8KB2NvbnRlbnQYASABKAkSDAoEcGF0aBgCIAEoCRIaCg1yZWxhdGl2ZV9wYXRoGAMgASgJSACIAQFCEAoOX3JlbGF0aXZlX3BhdGgihAEKFVNlbGVjdGVkQ29kZVNlbGVjdGlvbhIPCgdjb250ZW50GAEgASgJEgwKBHBhdGgYAiABKAkSGgoNcmVsYXRpdmVfcGF0aBgDIAEoCUgAiAEBEh4KBXJhbmdlGAQgASgLMg8uYWdlbnQudjEuUmFuZ2VCEAoOX3JlbGF0aXZlX3BhdGgiXQoQU2VsZWN0ZWRUZXJtaW5hbBIPCgdjb250ZW50GAEgASgJEhIKBXRpdGxlGAIgASgJSACIAQESEQoEcGF0aBgDIAEoCUgBiAEBQggKBl90aXRsZUIHCgVfcGF0aCKGAQoZU2VsZWN0ZWRUZXJtaW5hbFNlbGVjdGlvbhIPCgdjb250ZW50GAEgASgJEhIKBXRpdGxlGAIgASgJSACIAQESEQoEcGF0aBgDIAEoCUgBiAEBEh4KBXJhbmdlGAQgASgLMg8uYWdlbnQudjEuUmFuZ2VCCAoGX3RpdGxlQgcKBV9wYXRoIoMBCg5TZWxlY3RlZEZvbGRlchIMCgRwYXRoGAEgASgJEhoKDXJlbGF0aXZlX3BhdGgYAiABKAlIAIgBARI1Cg5kaXJlY3RvcnlfdHJlZRgDIAEoCzIdLmFnZW50LnYxLkxzRGlyZWN0b3J5VHJlZU5vZGVCEAoOX3JlbGF0aXZlX3BhdGginwEKFFNlbGVjdGVkRXh0ZXJuYWxMaW5rEgsKA3VybBgBIAEoCRIMCgR1dWlkGAIgASgJEhgKC3BkZl9jb250ZW50GAMgASgJSACIAQESEwoGaXNfcGRmGAQgASgISAGIAQESFQoIZmlsZW5hbWUYBSABKAlIAogBAUIOCgxfcGRmX2NvbnRlbnRCCQoHX2lzX3BkZkILCglfZmlsZW5hbWUiOAoSU2VsZWN0ZWRDdXJzb3JSdWxlEiIKBHJ1bGUYASABKAsyFC5hZ2VudC52MS5DdXJzb3JSdWxlIiIKD1NlbGVjdGVkR2l0RGlmZhIPCgdjb250ZW50GAEgASgJIjIKH1NlbGVjdGVkR2l0RGlmZkZyb21CcmFuY2hUb01haW4SDwoHY29udGVudBgBIAEoCSJpChFTZWxlY3RlZEdpdENvbW1pdBILCgNzaGEYASABKAkSDwoHbWVzc2FnZRgCIAEoCRIYCgtkZXNjcmlwdGlvbhgDIAEoCUgAiAEBEgwKBGRpZmYYBCABKAlCDgoMX2Rlc2NyaXB0aW9uIt0BChNTZWxlY3RlZFB1bGxSZXF1ZXN0Eg4KBm51bWJlchgBIAEoBRILCgN1cmwYAiABKAkSEgoFdGl0bGUYAyABKAlIAIgBARITCgtmb2xkZXJfcGF0aBgEIAEoCRIZCgxzdW1tYXJ5X2pzb24YBSABKAlIAYgBARIYCgtkZXNjcmlwdGlvbhgGIAEoCUgCiAEBEhQKB2Jsb2JfaWQYByABKAxIA4gBAUIICgZfdGl0bGVCDwoNX3N1bW1hcnlfanNvbkIOCgxfZGVzY3JpcHRpb25CCgoIX2Jsb2JfaWQiswEKGlNlbGVjdGVkR2l0UFJEaWZmU2VsZWN0aW9uEg4KBnByX3VybBgBIAEoCRIRCglmaWxlX3BhdGgYAiABKAkSEgoKc3RhcnRfbGluZRgDIAEoBRIQCghlbmRfbGluZRgEIAEoBRIZCgxkaWZmX2NvbnRlbnQYBSABKAlIAIgBARIUCgdibG9iX2lkGAYgASgMSAGIAQFCDwoNX2RpZmZfY29udGVudEIKCghfYmxvYl9pZCI2ChVTZWxlY3RlZEN1cnNvckNvbW1hbmQSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIjUKFVNlbGVjdGVkRG9jdW1lbnRhdGlvbhIOCgZkb2NfaWQYASABKAkSDAoEbmFtZRgCIAEoCSIyChBTZWxlY3RlZFBhc3RDaGF0EhAKCGFnZW50X2lkGAEgASgJEgwKBG5hbWUYAiABKAkiqwEKCUNhbGxGcmFtZRIaCg1mdW5jdGlvbl9uYW1lGAEgASgJSACIAQESEAoDdXJsGAIgASgJSAGIAQESGAoLbGluZV9udW1iZXIYAyABKAVIAogBARIaCg1jb2x1bW5fbnVtYmVyGAQgASgFSAOIAQFCEAoOX2Z1bmN0aW9uX25hbWVCBgoEX3VybEIOCgxfbGluZV9udW1iZXJCEAoOX2NvbHVtbl9udW1iZXIiaAoKU3RhY2tUcmFjZRIoCgtjYWxsX2ZyYW1lcxgBIAMoCzITLmFnZW50LnYxLkNhbGxGcmFtZRIcCg9yYXdfc3RhY2tfdHJhY2UYAiABKAlIAIgBAUISChBfcmF3X3N0YWNrX3RyYWNlIuQBChJTZWxlY3RlZENvbnNvbGVMb2cSDwoHbWVzc2FnZRgBIAEoCRIRCgl0aW1lc3RhbXAYAiABKAESDQoFbGV2ZWwYAyABKAkSEwoLY2xpZW50X25hbWUYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIuCgtzdGFja190cmFjZRgGIAEoCzIULmFnZW50LnYxLlN0YWNrVHJhY2VIAIgBARIdChBvYmplY3RfZGF0YV9qc29uGAcgASgJSAGIAQFCDgoMX3N0YWNrX3RyYWNlQhMKEV9vYmplY3RfZGF0YV9qc29uIroBChFTZWxlY3RlZFVJRWxlbWVudBIPCgdlbGVtZW50GAEgASgJEg0KBXhwYXRoGAIgASgJEhQKDHRleHRfY29udGVudBgDIAEoCRINCgVleHRyYRgEIAEoCRIWCgljb21wb25lbnQYBSABKAlIAIgBARIhChRjb21wb25lbnRfcHJvcHNfanNvbhgGIAEoCUgBiAEBQgwKCl9jb21wb25lbnRCFwoVX2NvbXBvbmVudF9wcm9wc19qc29uIiAKEFNlbGVjdGVkU3ViYWdlbnQSDAoEbmFtZRgBIAEoCSKCCgoPU2VsZWN0ZWRDb250ZXh0EjAKD3NlbGVjdGVkX2ltYWdlcxgBIAMoCzIXLmFnZW50LnYxLlNlbGVjdGVkSW1hZ2USPAoSaW52b2NhdGlvbl9jb250ZXh0GAIgASgLMhsuYWdlbnQudjEuSW52b2NhdGlvbkNvbnRleHRIAIgBARIVCg1leHRyYV9jb250ZXh0GAMgAygJEjoKFWV4dHJhX2NvbnRleHRfZW50cmllcxgQIAMoCzIbLmFnZW50LnYxLkV4dHJhQ29udGV4dEVudHJ5EiUKBWZpbGVzGAQgAygLMhYuYWdlbnQudjEuU2VsZWN0ZWRGaWxlEjgKD2NvZGVfc2VsZWN0aW9ucxgFIAMoCzIfLmFnZW50LnYxLlNlbGVjdGVkQ29kZVNlbGVjdGlvbhItCgl0ZXJtaW5hbHMYBiADKAsyGi5hZ2VudC52MS5TZWxlY3RlZFRlcm1pbmFsEkAKE3Rlcm1pbmFsX3NlbGVjdGlvbnMYByADKAsyIy5hZ2VudC52MS5TZWxlY3RlZFRlcm1pbmFsU2VsZWN0aW9uEikKB2ZvbGRlcnMYCCADKAsyGC5hZ2VudC52MS5TZWxlY3RlZEZvbGRlchI2Cg5leHRlcm5hbF9saW5rcxgJIAMoCzIeLmFnZW50LnYxLlNlbGVjdGVkRXh0ZXJuYWxMaW5rEjIKDGN1cnNvcl9ydWxlcxgKIAMoCzIcLmFnZW50LnYxLlNlbGVjdGVkQ3Vyc29yUnVsZRIwCghnaXRfZGlmZhgSIAEoCzIZLmFnZW50LnYxLlNlbGVjdGVkR2l0RGlmZkgBiAEBElQKHGdpdF9kaWZmX2Zyb21fYnJhbmNoX3RvX21haW4YCyABKAsyKS5hZ2VudC52MS5TZWxlY3RlZEdpdERpZmZGcm9tQnJhbmNoVG9NYWluSAKIAQESOAoPY3Vyc29yX2NvbW1hbmRzGAwgAygLMh8uYWdlbnQudjEuU2VsZWN0ZWRDdXJzb3JDb21tYW5kEjcKDmRvY3VtZW50YXRpb25zGA0gAygLMh8uYWdlbnQudjEuU2VsZWN0ZWREb2N1bWVudGF0aW9uEjAKC3VpX2VsZW1lbnRzGA4gAygLMhsuYWdlbnQudjEuU2VsZWN0ZWRVSUVsZW1lbnQSMgoMY29uc29sZV9sb2dzGA8gAygLMhwuYWdlbnQudjEuU2VsZWN0ZWRDb25zb2xlTG9nEjAKC2dpdF9jb21taXRzGBEgAygLMhsuYWdlbnQudjEuU2VsZWN0ZWRHaXRDb21taXQSLgoKcGFzdF9jaGF0cxgTIAMoCzIaLmFnZW50LnYxLlNlbGVjdGVkUGFzdENoYXQSRAoWZ2l0X3ByX2RpZmZfc2VsZWN0aW9ucxgUIAMoCzIkLmFnZW50LnYxLlNlbGVjdGVkR2l0UFJEaWZmU2VsZWN0aW9uEj0KFnNlbGVjdGVkX3B1bGxfcmVxdWVzdHMYFSADKAsyHS5hZ2VudC52MS5TZWxlY3RlZFB1bGxSZXF1ZXN0EjYKEnNlbGVjdGVkX3N1YmFnZW50cxgWIAMoCzIaLmFnZW50LnYxLlNlbGVjdGVkU3ViYWdlbnRCFQoTX2ludm9jYXRpb25fY29udGV4dEILCglfZ2l0X2RpZmZCHwodX2dpdF9kaWZmX2Zyb21fYnJhbmNoX3RvX21haW4i5QEKEUludm9jYXRpb25Db250ZXh0Ej8KDHNsYWNrX3RocmVhZBgBIAEoCzInLmFnZW50LnYxLkludm9jYXRpb25Db250ZXh0X1NsYWNrVGhyZWFkSAASOQoJZ2l0aHViX3ByGAIgASgLMiQuYWdlbnQudjEuSW52b2NhdGlvbkNvbnRleHRfR2l0aHViUFJIABI5CglpZGVfc3RhdGUYAyABKAsyJC5hZ2VudC52MS5JbnZvY2F0aW9uQ29udGV4dF9JZGVTdGF0ZUgAEhEKB2Jsb2JfaWQYCiABKAxIAEIGCgRkYXRhIrsBCh1JbnZvY2F0aW9uQ29udGV4dF9TbGFja1RocmVhZBIOCgZ0aHJlYWQYASABKAkSGQoMY2hhbm5lbF9uYW1lGAIgASgJSACIAQESHAoPY2hhbm5lbF9wdXJwb3NlGAMgASgJSAGIAQESGgoNY2hhbm5lbF90b3BpYxgEIAEoCUgCiAEBQg8KDV9jaGFubmVsX25hbWVCEgoQX2NoYW5uZWxfcHVycG9zZUIQCg5fY2hhbm5lbF90b3BpYyJ8ChpJbnZvY2F0aW9uQ29udGV4dF9HaXRodWJQUhINCgV0aXRsZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIQCghjb21tZW50cxgDIAEoCRIYCgtjaV9mYWlsdXJlcxgEIAEoCUgAiAEBQg4KDF9jaV9mYWlsdXJlcyL+AQoaSW52b2NhdGlvbkNvbnRleHRfSWRlU3RhdGUSQAoNdmlzaWJsZV9maWxlcxgBIAMoCzIpLmFnZW50LnYxLkludm9jYXRpb25Db250ZXh0X0lkZVN0YXRlX0ZpbGUSSAoVcmVjZW50bHlfdmlld2VkX2ZpbGVzGAIgAygLMikuYWdlbnQudjEuSW52b2NhdGlvbkNvbnRleHRfSWRlU3RhdGVfRmlsZRJUChRjdXJyZW50bHlfdmlld2VkX3BycxgDIAMoCzI2LmFnZW50LnYxLkludm9jYXRpb25Db250ZXh0X0lkZVN0YXRlX1ZpZXdlZFB1bGxSZXF1ZXN0Io4CCh9JbnZvY2F0aW9uQ29udGV4dF9JZGVTdGF0ZV9GaWxlEgwKBHBhdGgYASABKAkSGgoNcmVsYXRpdmVfcGF0aBgCIAEoCUgAiAEBElYKD2N1cnNvcl9wb3NpdGlvbhgDIAEoCzI4LmFnZW50LnYxLkludm9jYXRpb25Db250ZXh0X0lkZVN0YXRlX0ZpbGVfQ3Vyc29yUG9zaXRpb25IAYgBARITCgt0b3RhbF9saW5lcxgEIAEoBRIbCg5hY3RpdmVfY29tbWFuZBgFIAEoCUgCiAEBQhAKDl9yZWxhdGl2ZV9wYXRoQhIKEF9jdXJzb3JfcG9zaXRpb25CEQoPX2FjdGl2ZV9jb21tYW5kIkwKLkludm9jYXRpb25Db250ZXh0X0lkZVN0YXRlX0ZpbGVfQ3Vyc29yUG9zaXRpb24SDAoEbGluZRgBIAEoBRIMCgR0ZXh0GAIgASgJIukBCixJbnZvY2F0aW9uQ29udGV4dF9JZGVTdGF0ZV9WaWV3ZWRQdWxsUmVxdWVzdBIOCgZudW1iZXIYASABKAUSCwoDdXJsGAIgASgJEhIKBXRpdGxlGAMgASgJSACIAQESGAoLZm9sZGVyX3BhdGgYBCABKAlIAYgBARIZCgxzdW1tYXJ5X2pzb24YBSABKAlIAogBARIYCgtkZXNjcmlwdGlvbhgGIAEoCUgDiAEBQggKBl90aXRsZUIOCgxfZm9sZGVyX3BhdGhCDwoNX3N1bW1hcnlfanNvbkIOCgxfZGVzY3JpcHRpb24iSAoWU2V0dXBWbUVudmlyb25tZW50QXJncxIXCg9pbnN0YWxsX2NvbW1hbmQYAiABKAkSFQoNc3RhcnRfY29tbWFuZBgDIAEoCSJcChhTZXR1cFZtRW52aXJvbm1lbnRSZXN1bHQSNgoHc3VjY2VzcxgBIAEoCzIjLmFnZW50LnYxLlNldHVwVm1FbnZpcm9ubWVudFN1Y2Nlc3NIAEIICgZyZXN1bHQiGwoZU2V0dXBWbUVudmlyb25tZW50U3VjY2VzcyKAAQoaU2V0dXBWbUVudmlyb25tZW50VG9vbENhbGwSLgoEYXJncxgBIAEoCzIgLmFnZW50LnYxLlNldHVwVm1FbnZpcm9ubWVudEFyZ3MSMgoGcmVzdWx0GAIgASgLMiIuYWdlbnQudjEuU2V0dXBWbUVudmlyb25tZW50UmVzdWx0IsABChlTaGVsbENvbW1hbmRQYXJzaW5nUmVzdWx0EhYKDnBhcnNpbmdfZmFpbGVkGAEgASgIElIKE2V4ZWN1dGFibGVfY29tbWFuZHMYAiADKAsyNS5hZ2VudC52MS5TaGVsbENvbW1hbmRQYXJzaW5nUmVzdWx0X0V4ZWN1dGFibGVDb21tYW5kEhUKDWhhc19yZWRpcmVjdHMYAyABKAgSIAoYaGFzX2NvbW1hbmRfc3Vic3RpdHV0aW9uGAQgASgIIk0KLlNoZWxsQ29tbWFuZFBhcnNpbmdSZXN1bHRfRXhlY3V0YWJsZUNvbW1hbmRBcmcSDAoEdHlwZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSKWAQorU2hlbGxDb21tYW5kUGFyc2luZ1Jlc3VsdF9FeGVjdXRhYmxlQ29tbWFuZBIMCgRuYW1lGAEgASgJEkYKBGFyZ3MYAiADKAsyOC5hZ2VudC52MS5TaGVsbENvbW1hbmRQYXJzaW5nUmVzdWx0X0V4ZWN1dGFibGVDb21tYW5kQXJnEhEKCWZ1bGxfdGV4dBgDIAEoCSKIBAoJU2hlbGxBcmdzEg8KB2NvbW1hbmQYASABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAiABKAkSDwoHdGltZW91dBgDIAEoBRIUCgx0b29sX2NhbGxfaWQYBCABKAkSFwoPc2ltcGxlX2NvbW1hbmRzGAUgAygJEhoKEmhhc19pbnB1dF9yZWRpcmVjdBgGIAEoCBIbChNoYXNfb3V0cHV0X3JlZGlyZWN0GAcgASgIEjsKDnBhcnNpbmdfcmVzdWx0GAggASgLMiMuYWdlbnQudjEuU2hlbGxDb21tYW5kUGFyc2luZ1Jlc3VsdBI+ChhyZXF1ZXN0ZWRfc2FuZGJveF9wb2xpY3kYCSABKAsyFy5hZ2VudC52MS5TYW5kYm94UG9saWN5SACIAQESKAobZmlsZV9vdXRwdXRfdGhyZXNob2xkX2J5dGVzGAogASgESAGIAQESFQoNaXNfYmFja2dyb3VuZBgLIAEoCBIVCg1za2lwX2FwcHJvdmFsGAwgASgIEhgKEHRpbWVvdXRfYmVoYXZpb3IYDSABKAUSGQoMaGFyZF90aW1lb3V0GA4gASgFSAKIAQFCGwoZX3JlcXVlc3RlZF9zYW5kYm94X3BvbGljeUIeChxfZmlsZV9vdXRwdXRfdGhyZXNob2xkX2J5dGVzQg8KDV9oYXJkX3RpbWVvdXQi+gMKC1NoZWxsUmVzdWx0EjQKDnNhbmRib3hfcG9saWN5GGUgASgLMhcuYWdlbnQudjEuU2FuZGJveFBvbGljeUgBiAEBEhoKDWlzX2JhY2tncm91bmQYZiABKAhIAogBARIdChB0ZXJtaW5hbHNfZm9sZGVyGGcgASgJSAOIAQESEAoDcGlkGGggASgNSASIAQESKQoHc3VjY2VzcxgBIAEoCzIWLmFnZW50LnYxLlNoZWxsU3VjY2Vzc0gAEikKB2ZhaWx1cmUYAiABKAsyFi5hZ2VudC52MS5TaGVsbEZhaWx1cmVIABIpCgd0aW1lb3V0GAMgASgLMhYuYWdlbnQudjEuU2hlbGxUaW1lb3V0SAASKwoIcmVqZWN0ZWQYBCABKAsyFy5hZ2VudC52MS5TaGVsbFJlamVjdGVkSAASMAoLc3Bhd25fZXJyb3IYBSABKAsyGS5hZ2VudC52MS5TaGVsbFNwYXduRXJyb3JIABI8ChFwZXJtaXNzaW9uX2RlbmllZBgHIAEoCzIfLmFnZW50LnYxLlNoZWxsUGVybWlzc2lvbkRlbmllZEgAQggKBnJlc3VsdEIRCg9fc2FuZGJveF9wb2xpY3lCEAoOX2lzX2JhY2tncm91bmRCEwoRX3Rlcm1pbmFsc19mb2xkZXJCBgoEX3BpZCIhChFTaGVsbFN0cmVhbVN0ZG91dBIMCgRkYXRhGAEgASgJIiEKEVNoZWxsU3RyZWFtU3RkZXJyEgwKBGRhdGEYASABKAkitQEKD1NoZWxsU3RyZWFtRXhpdBIMCgRjb2RlGAEgASgNEgsKA2N3ZBgCIAEoCRI2Cg9vdXRwdXRfbG9jYXRpb24YAyABKAsyGC5hZ2VudC52MS5PdXRwdXRMb2NhdGlvbkgAiAEBEg8KB2Fib3J0ZWQYBCABKAgSGQoMYWJvcnRfcmVhc29uGAUgASgFSAGIAQFCEgoQX291dHB1dF9sb2NhdGlvbkIPCg1fYWJvcnRfcmVhc29uIlsKEFNoZWxsU3RyZWFtU3RhcnQSNAoOc2FuZGJveF9wb2xpY3kYASABKAsyFy5hZ2VudC52MS5TYW5kYm94UG9saWN5SACIAQFCEQoPX3NhbmRib3hfcG9saWN5IpkBChdTaGVsbFN0cmVhbUJhY2tncm91bmRlZBIQCghzaGVsbF9pZBgBIAEoDRIPCgdjb21tYW5kGAIgASgJEhkKEXdvcmtpbmdfZGlyZWN0b3J5GAMgASgJEhAKA3BpZBgEIAEoDUgAiAEBEhcKCm1zX3RvX3dhaXQYBSABKAVIAYgBAUIGCgRfcGlkQg0KC19tc190b193YWl0IvICCgtTaGVsbFN0cmVhbRItCgZzdGRvdXQYASABKAsyGy5hZ2VudC52MS5TaGVsbFN0cmVhbVN0ZG91dEgAEi0KBnN0ZGVychgCIAEoCzIbLmFnZW50LnYxLlNoZWxsU3RyZWFtU3RkZXJySAASKQoEZXhpdBgDIAEoCzIZLmFnZW50LnYxLlNoZWxsU3RyZWFtRXhpdEgAEisKBXN0YXJ0GAQgASgLMhouYWdlbnQudjEuU2hlbGxTdHJlYW1TdGFydEgAEisKCHJlamVjdGVkGAUgASgLMhcuYWdlbnQudjEuU2hlbGxSZWplY3RlZEgAEjwKEXBlcm1pc3Npb25fZGVuaWVkGAYgASgLMh8uYWdlbnQudjEuU2hlbGxQZXJtaXNzaW9uRGVuaWVkSAASOQoMYmFja2dyb3VuZGVkGAcgASgLMiEuYWdlbnQudjEuU2hlbGxTdHJlYW1CYWNrZ3JvdW5kZWRIAEIHCgVldmVudCJLCg5PdXRwdXRMb2NhdGlvbhIRCglmaWxlX3BhdGgYASABKAkSEgoKc2l6ZV9ieXRlcxgCIAEoAxISCgpsaW5lX2NvdW50GAMgASgDIv8CCgxTaGVsbFN1Y2Nlc3MSDwoHY29tbWFuZBgBIAEoCRIZChF3b3JraW5nX2RpcmVjdG9yeRgCIAEoCRIRCglleGl0X2NvZGUYAyABKAUSDgoGc2lnbmFsGAQgASgJEg4KBnN0ZG91dBgFIAEoCRIOCgZzdGRlcnIYBiABKAkSFgoOZXhlY3V0aW9uX3RpbWUYByABKAUSNgoPb3V0cHV0X2xvY2F0aW9uGAggASgLMhguYWdlbnQudjEuT3V0cHV0TG9jYXRpb25IAIgBARIVCghzaGVsbF9pZBgJIAEoDUgBiAEBEh8KEmludGVybGVhdmVkX291dHB1dBgKIAEoCUgCiAEBEhAKA3BpZBgLIAEoDUgDiAEBEhcKCm1zX3RvX3dhaXQYDCABKAVIBIgBAUISChBfb3V0cHV0X2xvY2F0aW9uQgsKCV9zaGVsbF9pZEIVChNfaW50ZXJsZWF2ZWRfb3V0cHV0QgYKBF9waWRCDQoLX21zX3RvX3dhaXQi1gIKDFNoZWxsRmFpbHVyZRIPCgdjb21tYW5kGAEgASgJEhkKEXdvcmtpbmdfZGlyZWN0b3J5GAIgASgJEhEKCWV4aXRfY29kZRgDIAEoBRIOCgZzaWduYWwYBCABKAkSDgoGc3Rkb3V0GAUgASgJEg4KBnN0ZGVychgGIAEoCRIWCg5leGVjdXRpb25fdGltZRgHIAEoBRI2Cg9vdXRwdXRfbG9jYXRpb24YCCABKAsyGC5hZ2VudC52MS5PdXRwdXRMb2NhdGlvbkgAiAEBEh8KEmludGVybGVhdmVkX291dHB1dBgJIAEoCUgBiAEBEhkKDGFib3J0X3JlYXNvbhgKIAEoBUgCiAEBEg8KB2Fib3J0ZWQYCyABKAhCEgoQX291dHB1dF9sb2NhdGlvbkIVChNfaW50ZXJsZWF2ZWRfb3V0cHV0Qg8KDV9hYm9ydF9yZWFzb24iTgoMU2hlbGxUaW1lb3V0Eg8KB2NvbW1hbmQYASABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAiABKAkSEgoKdGltZW91dF9tcxgDIAEoBSJgCg1TaGVsbFJlamVjdGVkEg8KB2NvbW1hbmQYASABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAiABKAkSDgoGcmVhc29uGAMgASgJEhMKC2lzX3JlYWRvbmx5GAQgASgIImcKFVNoZWxsUGVybWlzc2lvbkRlbmllZBIPCgdjb21tYW5kGAEgASgJEhkKEXdvcmtpbmdfZGlyZWN0b3J5GAIgASgJEg0KBWVycm9yGAMgASgJEhMKC2lzX3JlYWRvbmx5GAQgASgIIkwKD1NoZWxsU3Bhd25FcnJvchIPCgdjb21tYW5kGAEgASgJEhkKEXdvcmtpbmdfZGlyZWN0b3J5GAIgASgJEg0KBWVycm9yGAMgASgJIkAKElNoZWxsUGFydGlhbFJlc3VsdBIUCgxzdGRvdXRfZGVsdGEYASABKAkSFAoMc3RkZXJyX2RlbHRhGAIgASgJIlkKDVNoZWxsVG9vbENhbGwSIQoEYXJncxgBIAEoCzITLmFnZW50LnYxLlNoZWxsQXJncxIlCgZyZXN1bHQYAiABKAsyFS5hZ2VudC52MS5TaGVsbFJlc3VsdCIrChhTaGVsbFRvb2xDYWxsU3Rkb3V0RGVsdGESDwoHY29udGVudBgBIAEoCSIrChhTaGVsbFRvb2xDYWxsU3RkZXJyRGVsdGESDwoHY29udGVudBgBIAEoCSKJAQoSU2hlbGxUb29sQ2FsbERlbHRhEjQKBnN0ZG91dBgBIAEoCzIiLmFnZW50LnYxLlNoZWxsVG9vbENhbGxTdGRvdXREZWx0YUgAEjQKBnN0ZGVychgCIAEoCzIiLmFnZW50LnYxLlNoZWxsVG9vbENhbGxTdGRlcnJEZWx0YUgAQgcKBWRlbHRhIu0BCgxTdWJhZ2VudFR5cGUSOAoLdW5zcGVjaWZpZWQYASABKAsyIS5hZ2VudC52MS5TdWJhZ2VudFR5cGVVbnNwZWNpZmllZEgAEjkKDGNvbXB1dGVyX3VzZRgCIAEoCzIhLmFnZW50LnYxLlN1YmFnZW50VHlwZUNvbXB1dGVyVXNlSAASLgoGY3VzdG9tGAMgASgLMhwuYWdlbnQudjEuU3ViYWdlbnRUeXBlQ3VzdG9tSAASMAoHZXhwbG9yZRgEIAEoCzIdLmFnZW50LnYxLlN1YmFnZW50VHlwZUV4cGxvcmVIAEIGCgR0eXBlIhkKF1N1YmFnZW50VHlwZVVuc3BlY2lmaWVkIhkKF1N1YmFnZW50VHlwZUNvbXB1dGVyVXNlIhUKE1N1YmFnZW50VHlwZUV4cGxvcmUiIgoSU3ViYWdlbnRUeXBlQ3VzdG9tEgwKBG5hbWUYASABKAkijQEKDkN1c3RvbVN1YmFnZW50EhEKCWZ1bGxfcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg0KBXRvb2xzGAQgAygJEg0KBW1vZGVsGAUgASgJEg4KBnByb21wdBgGIAEoCRIXCg9wZXJtaXNzaW9uX21vZGUYByABKAUiaAoOU3dpdGNoTW9kZUFyZ3MSFgoOdGFyZ2V0X21vZGVfaWQYASABKAkSGAoLZXhwbGFuYXRpb24YAiABKAlIAIgBARIUCgx0b29sX2NhbGxfaWQYAyABKAlCDgoMX2V4cGxhbmF0aW9uIqoBChBTd2l0Y2hNb2RlUmVzdWx0Ei4KB3N1Y2Nlc3MYASABKAsyGy5hZ2VudC52MS5Td2l0Y2hNb2RlU3VjY2Vzc0gAEioKBWVycm9yGAIgASgLMhkuYWdlbnQudjEuU3dpdGNoTW9kZUVycm9ySAASMAoIcmVqZWN0ZWQYAyABKAsyHC5hZ2VudC52MS5Td2l0Y2hNb2RlUmVqZWN0ZWRIAEIICgZyZXN1bHQiPQoRU3dpdGNoTW9kZVN1Y2Nlc3MSFAoMZnJvbV9tb2RlX2lkGAEgASgJEhIKCnRvX21vZGVfaWQYAiABKAkiIAoPU3dpdGNoTW9kZUVycm9yEg0KBWVycm9yGAEgASgJIiQKElN3aXRjaE1vZGVSZWplY3RlZBIOCgZyZWFzb24YASABKAkiaAoSU3dpdGNoTW9kZVRvb2xDYWxsEiYKBGFyZ3MYASABKAsyGC5hZ2VudC52MS5Td2l0Y2hNb2RlQXJncxIqCgZyZXN1bHQYAiABKAsyGi5hZ2VudC52MS5Td2l0Y2hNb2RlUmVzdWx0IkAKFlN3aXRjaE1vZGVSZXF1ZXN0UXVlcnkSJgoEYXJncxgBIAEoCzIYLmFnZW50LnYxLlN3aXRjaE1vZGVBcmdzIqkBChlTd2l0Y2hNb2RlUmVxdWVzdFJlc3BvbnNlEkAKCGFwcHJvdmVkGAEgASgLMiwuYWdlbnQudjEuU3dpdGNoTW9kZVJlcXVlc3RSZXNwb25zZV9BcHByb3ZlZEgAEkAKCHJlamVjdGVkGAIgASgLMiwuYWdlbnQudjEuU3dpdGNoTW9kZVJlcXVlc3RSZXNwb25zZV9SZWplY3RlZEgAQggKBnJlc3VsdCIkCiJTd2l0Y2hNb2RlUmVxdWVzdFJlc3BvbnNlX0FwcHJvdmVkIjQKIlN3aXRjaE1vZGVSZXF1ZXN0UmVzcG9uc2VfUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJInUKCFRvZG9JdGVtEgoKAmlkGAEgASgJEg8KB2NvbnRlbnQYAiABKAkSDgoGc3RhdHVzGAMgASgFEhIKCmNyZWF0ZWRfYXQYBCABKAMSEgoKdXBkYXRlZF9hdBgFIAEoAxIUCgxkZXBlbmRlbmNpZXMYBiADKAkiawoTVXBkYXRlVG9kb3NUb29sQ2FsbBInCgRhcmdzGAEgASgLMhkuYWdlbnQudjEuVXBkYXRlVG9kb3NBcmdzEisKBnJlc3VsdBgCIAEoCzIbLmFnZW50LnYxLlVwZGF0ZVRvZG9zUmVzdWx0IkMKD1VwZGF0ZVRvZG9zQXJncxIhCgV0b2RvcxgBIAMoCzISLmFnZW50LnYxLlRvZG9JdGVtEg0KBW1lcmdlGAIgASgIInsKEVVwZGF0ZVRvZG9zUmVzdWx0Ei8KB3N1Y2Nlc3MYASABKAsyHC5hZ2VudC52MS5VcGRhdGVUb2Rvc1N1Y2Nlc3NIABIrCgVlcnJvchgCIAEoCzIaLmFnZW50LnYxLlVwZGF0ZVRvZG9zRXJyb3JIAEIICgZyZXN1bHQiXwoSVXBkYXRlVG9kb3NTdWNjZXNzEiEKBXRvZG9zGAEgAygLMhIuYWdlbnQudjEuVG9kb0l0ZW0SEwoLdG90YWxfY291bnQYAiABKAUSEQoJd2FzX21lcmdlGAMgASgIIiEKEFVwZGF0ZVRvZG9zRXJyb3ISDQoFZXJyb3IYASABKAkiZQoRUmVhZFRvZG9zVG9vbENhbGwSJQoEYXJncxgBIAEoCzIXLmFnZW50LnYxLlJlYWRUb2Rvc0FyZ3MSKQoGcmVzdWx0GAIgASgLMhkuYWdlbnQudjEuUmVhZFRvZG9zUmVzdWx0IjkKDVJlYWRUb2Rvc0FyZ3MSFQoNc3RhdHVzX2ZpbHRlchgBIAMoBRIRCglpZF9maWx0ZXIYAiADKAkidQoPUmVhZFRvZG9zUmVzdWx0Ei0KB3N1Y2Nlc3MYASABKAsyGi5hZ2VudC52MS5SZWFkVG9kb3NTdWNjZXNzSAASKQoFZXJyb3IYAiABKAsyGC5hZ2VudC52MS5SZWFkVG9kb3NFcnJvckgAQggKBnJlc3VsdCJKChBSZWFkVG9kb3NTdWNjZXNzEiEKBXRvZG9zGAEgAygLMhIuYWdlbnQudjEuVG9kb0l0ZW0SEwoLdG90YWxfY291bnQYAiABKAUiHwoOUmVhZFRvZG9zRXJyb3ISDQoFZXJyb3IYASABKAkiSwoFUmFuZ2USIQoFc3RhcnQYASABKAsyEi5hZ2VudC52MS5Qb3NpdGlvbhIfCgNlbmQYAiABKAsyEi5hZ2VudC52MS5Qb3NpdGlvbiIoCghQb3NpdGlvbhIMCgRsaW5lGAEgASgNEg4KBmNvbHVtbhgCIAEoDSIYCgVFcnJvchIPCgdtZXNzYWdlGAEgASgJIjoKDVdlYlNlYXJjaEFyZ3MSEwoLc2VhcmNoX3Rlcm0YASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJIqYBCg9XZWJTZWFyY2hSZXN1bHQSLQoHc3VjY2VzcxgBIAEoCzIaLmFnZW50LnYxLldlYlNlYXJjaFN1Y2Nlc3NIABIpCgVlcnJvchgCIAEoCzIYLmFnZW50LnYxLldlYlNlYXJjaEVycm9ySAASLwoIcmVqZWN0ZWQYAyABKAsyGy5hZ2VudC52MS5XZWJTZWFyY2hSZWplY3RlZEgAQggKBnJlc3VsdCJEChBXZWJTZWFyY2hTdWNjZXNzEjAKCnJlZmVyZW5jZXMYASADKAsyHC5hZ2VudC52MS5XZWJTZWFyY2hSZWZlcmVuY2UiHwoOV2ViU2VhcmNoRXJyb3ISDQoFZXJyb3IYASABKAkiIwoRV2ViU2VhcmNoUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJIj8KEldlYlNlYXJjaFJlZmVyZW5jZRINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkSDQoFY2h1bmsYAyABKAkiZQoRV2ViU2VhcmNoVG9vbENhbGwSJQoEYXJncxgBIAEoCzIXLmFnZW50LnYxLldlYlNlYXJjaEFyZ3MSKQoGcmVzdWx0GAIgASgLMhkuYWdlbnQudjEuV2ViU2VhcmNoUmVzdWx0Ij4KFVdlYlNlYXJjaFJlcXVlc3RRdWVyeRIlCgRhcmdzGAEgASgLMhcuYWdlbnQudjEuV2ViU2VhcmNoQXJncyKmAQoYV2ViU2VhcmNoUmVxdWVzdFJlc3BvbnNlEj8KCGFwcHJvdmVkGAEgASgLMisuYWdlbnQudjEuV2ViU2VhcmNoUmVxdWVzdFJlc3BvbnNlX0FwcHJvdmVkSAASPwoIcmVqZWN0ZWQYAiABKAsyKy5hZ2VudC52MS5XZWJTZWFyY2hSZXF1ZXN0UmVzcG9uc2VfUmVqZWN0ZWRIAEIICgZyZXN1bHQiIwohV2ViU2VhcmNoUmVxdWVzdFJlc3BvbnNlX0FwcHJvdmVkIjMKIVdlYlNlYXJjaFJlcXVlc3RSZXNwb25zZV9SZWplY3RlZBIOCgZyZWFzb24YASABKAkifwoJV3JpdGVBcmdzEgwKBHBhdGgYASABKAkSEQoJZmlsZV90ZXh0GAIgASgJEhQKDHRvb2xfY2FsbF9pZBgDIAEoCRInCh9yZXR1cm5fZmlsZV9jb250ZW50X2FmdGVyX3dyaXRlGAQgASgIEhIKCmZpbGVfYnl0ZXMYBSABKAwigAIKC1dyaXRlUmVzdWx0EikKB3N1Y2Nlc3MYASABKAsyFi5hZ2VudC52MS5Xcml0ZVN1Y2Nlc3NIABI8ChFwZXJtaXNzaW9uX2RlbmllZBgDIAEoCzIfLmFnZW50LnYxLldyaXRlUGVybWlzc2lvbkRlbmllZEgAEioKCG5vX3NwYWNlGAQgASgLMhYuYWdlbnQudjEuV3JpdGVOb1NwYWNlSAASJQoFZXJyb3IYBSABKAsyFC5hZ2VudC52MS5Xcml0ZUVycm9ySAASKwoIcmVqZWN0ZWQYBiABKAsyFy5hZ2VudC52MS5Xcml0ZVJlamVjdGVkSABCCAoGcmVzdWx0IooBCgxXcml0ZVN1Y2Nlc3MSDAoEcGF0aBgBIAEoCRIVCg1saW5lc19jcmVhdGVkGAIgASgFEhEKCWZpbGVfc2l6ZRgDIAEoBRIlChhmaWxlX2NvbnRlbnRfYWZ0ZXJfd3JpdGUYBCABKAlIAIgBAUIbChlfZmlsZV9jb250ZW50X2FmdGVyX3dyaXRlIm8KFVdyaXRlUGVybWlzc2lvbkRlbmllZBIMCgRwYXRoGAEgASgJEhEKCWRpcmVjdG9yeRgCIAEoCRIRCglvcGVyYXRpb24YAyABKAkSDQoFZXJyb3IYBCABKAkSEwoLaXNfcmVhZG9ubHkYBSABKAgiHAoMV3JpdGVOb1NwYWNlEgwKBHBhdGgYASABKAkiKQoKV3JpdGVFcnJvchIMCgRwYXRoGAEgASgJEg0KBWVycm9yGAIgASgJIi0KDVdyaXRlUmVqZWN0ZWQSDAoEcGF0aBgBIAEoCRIOCgZyZWFzb24YAiABKAkigwEKF0Jvb3RzdHJhcFN0YXRzaWdSZXF1ZXN0Eh4KEWlnbm9yZV9kZXZfc3RhdHVzGAEgASgISACIAQESHQoQb3BlcmF0aW5nX3N5c3RlbRgCIAEoBUgBiAEBQhQKEl9pZ25vcmVfZGV2X3N0YXR1c0ITChFfb3BlcmF0aW5nX3N5c3RlbSIOCgxQaW5nUmVzcG9uc2UitwEKC0V4ZWNSZXF1ZXN0Eg8KB2NvbW1hbmQYASABKAkSEAoDY3dkGAIgASgJSACIAQESDAoEYXJncxgDIAMoCRI7CgtlbnZpcm9ubWVudBgEIAMoCzImLmFnZW50LnYxLkV4ZWNSZXF1ZXN0LkVudmlyb25tZW50RW50cnkaMgoQRW52aXJvbm1lbnRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgYKBF9jd2QioAEKDEV4ZWNSZXNwb25zZRItCgxzdGRvdXRfZXZlbnQYASABKAsyFS5hZ2VudC52MS5TdGRvdXRFdmVudEgAEi0KDHN0ZGVycl9ldmVudBgCIAEoCzIVLmFnZW50LnYxLlN0ZGVyckV2ZW50SAASKQoKZXhpdF9ldmVudBgDIAEoCzITLmFnZW50LnYxLkV4aXRFdmVudEgAQgcKBWV2ZW50IhsKC1N0ZG91dEV2ZW50EgwKBGRhdGEYASABKAkiGwoLU3RkZXJyRXZlbnQSDAoEZGF0YRgBIAEoCSIeCglFeGl0RXZlbnQSEQoJZXhpdF9jb2RlGAEgASgFIiMKE1JlYWRUZXh0RmlsZVJlcXVlc3QSDAoEcGF0aBgBIAEoCSInChRSZWFkVGV4dEZpbGVSZXNwb25zZRIPCgdjb250ZW50GAEgASgJIjUKFFdyaXRlVGV4dEZpbGVSZXF1ZXN0EgwKBHBhdGgYASABKAkSDwoHY29udGVudBgCIAEoCSIXChVXcml0ZVRleHRGaWxlUmVzcG9uc2UiJQoVUmVhZEJpbmFyeUZpbGVSZXF1ZXN0EgwKBHBhdGgYASABKAkiKQoWUmVhZEJpbmFyeUZpbGVSZXNwb25zZRIPCgdjb250ZW50GAEgASgMIjcKFldyaXRlQmluYXJ5RmlsZVJlcXVlc3QSDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgMIhkKF1dyaXRlQmluYXJ5RmlsZVJlc3BvbnNlIkUKHkdldFdvcmtzcGFjZUNoYW5nZXNIYXNoUmVxdWVzdBIRCglyb290X3BhdGgYASABKAkSEAoIYmFzZV9yZWYYAiABKAkiLwofR2V0V29ya3NwYWNlQ2hhbmdlc0hhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIlAKH1JlZnJlc2hHaXRodWJBY2Nlc3NUb2tlblJlcXVlc3QSGwoTZ2l0aHViX2FjY2Vzc190b2tlbhgBIAEoCRIQCghob3N0bmFtZRgCIAEoCSIiCiBSZWZyZXNoR2l0aHViQWNjZXNzVG9rZW5SZXNwb25zZSJXCh1XYXJtUmVtb3RlQWNjZXNzU2VydmVyUmVxdWVzdBIOCgZjb21taXQYASABKAkSDAoEcG9ydBgCIAEoBRIYChBjb25uZWN0aW9uX3Rva2VuGAMgASgJIiAKHldhcm1SZW1vdGVBY2Nlc3NTZXJ2ZXJSZXNwb25zZSIWChRMaXN0QXJ0aWZhY3RzUmVxdWVzdCKKAgoWQXJ0aWZhY3RVcGxvYWRNZXRhZGF0YRIVCg1hYnNvbHV0ZV9wYXRoGAEgASgJEhIKCnNpemVfYnl0ZXMYAiABKAQSGgoSdXBkYXRlZF9hdF91bml4X21zGAMgASgDEg4KBnN0YXR1cxgEIAEoBRIWCg5ieXRlc191cGxvYWRlZBgFIAEoBBISCgpsYXN0X2Vycm9yGAYgASgJEhcKD3VwbG9hZF9hdHRlbXB0cxgHIAEoDRIfChdsYXN0X3N0YXJ0ZWRfYXRfdW5peF9tcxgIIAEoAxIgChhsYXN0X2ZpbmlzaGVkX2F0X3VuaXhfbXMYCSABKAMSEQoJdXBsb2FkX2lkGAogASgJIkwKFUxpc3RBcnRpZmFjdHNSZXNwb25zZRIzCglhcnRpZmFjdHMYASADKAsyIC5hZ2VudC52MS5BcnRpZmFjdFVwbG9hZE1ldGFkYXRhIk4KFlVwbG9hZEFydGlmYWN0c1JlcXVlc3QSNAoHdXBsb2FkcxgBIAMoCzIjLmFnZW50LnYxLkFydGlmYWN0VXBsb2FkSW5zdHJ1Y3Rpb24i1wIKGUFydGlmYWN0VXBsb2FkSW5zdHJ1Y3Rpb24SFQoNYWJzb2x1dGVfcGF0aBgBIAEoCRISCgp1cGxvYWRfdXJsGAIgASgJEg4KBm1ldGhvZBgDIAEoCRJBCgdoZWFkZXJzGAQgAygLMjAuYWdlbnQudjEuQXJ0aWZhY3RVcGxvYWRJbnN0cnVjdGlvbi5IZWFkZXJzRW50cnkSGQoMY29udGVudF90eXBlGAUgASgJSACIAQESHQoQc2xhY2tfdXBsb2FkX3VybBgGIAEoCUgBiAEBEhoKDXNsYWNrX2ZpbGVfaWQYByABKAlIAogBARouCgxIZWFkZXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIPCg1fY29udGVudF90eXBlQhMKEV9zbGFja191cGxvYWRfdXJsQhAKDl9zbGFja19maWxlX2lkIoQBChxBcnRpZmFjdFVwbG9hZERpc3BhdGNoUmVzdWx0EhUKDWFic29sdXRlX3BhdGgYASABKAkSDgoGc3RhdHVzGAIgASgFEg8KB21lc3NhZ2UYAyABKAkSGgoNc2xhY2tfZmlsZV9pZBgEIAEoCUgAiAEBQhAKDl9zbGFja19maWxlX2lkIlIKF1VwbG9hZEFydGlmYWN0c1Jlc3BvbnNlEjcKB3Jlc3VsdHMYASADKAsyJi5hZ2VudC52MS5BcnRpZmFjdFVwbG9hZERpc3BhdGNoUmVzdWx0IhwKGkdldE1jcFJlZnJlc2hUb2tlbnNSZXF1ZXN0IqUBChtHZXRNY3BSZWZyZXNoVG9rZW5zUmVzcG9uc2USUAoOcmVmcmVzaF90b2tlbnMYASADKAsyOC5hZ2VudC52MS5HZXRNY3BSZWZyZXNoVG9rZW5zUmVzcG9uc2UuUmVmcmVzaFRva2Vuc0VudHJ5GjQKElJlZnJlc2hUb2tlbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqMBCiFVcGRhdGVFbnZpcm9ubWVudFZhcmlhYmxlc1JlcXVlc3QSQQoDZW52GAEgAygLMjQuYWdlbnQudjEuVXBkYXRlRW52aXJvbm1lbnRWYXJpYWJsZXNSZXF1ZXN0LkVudkVudHJ5Eg8KB3JlcGxhY2UYAiABKAgaKgoIRW52RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJGCiJVcGRhdGVFbnZpcm9ubWVudFZhcmlhYmxlc1Jlc3BvbnNlEg8KB2FwcGxpZWQYASABKA0SDwoHcmVtb3ZlZBgCIAEoDSKDAQoSTWNwT0F1dGhTdG9yZWREYXRhEhUKDXJlZnJlc2hfdG9rZW4YASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhoKDWNsaWVudF9zZWNyZXQYAyABKAlIAIgBARIVCg1yZWRpcmVjdF91cmlzGAQgAygJQhAKDl9jbGllbnRfc2VjcmV0Ik4KBUZyYW1lEgoKAmlkGAEgASgJEg4KBm1ldGhvZBgCIAEoCRIMCgRkYXRhGAMgASgMEgwKBGtpbmQYBCABKAUSDQoFZXJyb3IYBSABKAkiBwoFRW1wdHkiIwoNQmlkaVJlcXVlc3RJZBISCgpyZXF1ZXN0X2lkGAEgASgJKogBCh1BcHBsaWVkQWdlbnRDaGFuZ2VfQ2hhbmdlVHlwZRIbChdDSEFOR0VfVFlQRV9VTlNQRUNJRklFRBAAEhcKE0NIQU5HRV9UWVBFX0NSRUFURUQQARIYChRDSEFOR0VfVFlQRV9NT0RJRklFRBACEhcKE0NIQU5HRV9UWVBFX0RFTEVURUQQAyqkAQoLTW91c2VCdXR0b24SHAoYTU9VU0VfQlVUVE9OX1VOU1BFQ0lGSUVEEAASFQoRTU9VU0VfQlVUVE9OX0xFRlQQARIWChJNT1VTRV9CVVRUT05fUklHSFQQAhIXChNNT1VTRV9CVVRUT05fTUlERExFEAMSFQoRTU9VU0VfQlVUVE9OX0JBQ0sQBBIYChRNT1VTRV9CVVRUT05fRk9SV0FSRBAFKp4BCg9TY3JvbGxEaXJlY3Rpb24SIAocU0NST0xMX0RJUkVDVElPTl9VTlNQRUNJRklFRBAAEhcKE1NDUk9MTF9ESVJFQ1RJT05fVVAQARIZChVTQ1JPTExfRElSRUNUSU9OX0RPV04QAhIZChVTQ1JPTExfRElSRUNUSU9OX0xFRlQQAxIaChZTQ1JPTExfRElSRUNUSU9OX1JJR0hUEAQqcAoQQ3Vyc29yUnVsZVNvdXJjZRIiCh5DVVJTT1JfUlVMRV9TT1VSQ0VfVU5TUEVDSUZJRUQQABIbChdDVVJTT1JfUlVMRV9TT1VSQ0VfVEVBTRABEhsKF0NVUlNPUl9SVUxFX1NPVVJDRV9VU0VSEAIqvAEKEkRpYWdub3N0aWNTZXZlcml0eRIjCh9ESUFHTk9TVElDX1NFVkVSSVRZX1VOU1BFQ0lGSUVEEAASHQoZRElBR05PU1RJQ19TRVZFUklUWV9FUlJPUhABEh8KG0RJQUdOT1NUSUNfU0VWRVJJVFlfV0FSTklORxACEiMKH0RJQUdOT1NUSUNfU0VWRVJJVFlfSU5GT1JNQVRJT04QAxIcChhESUFHTk9TVElDX1NFVkVSSVRZX0hJTlQQBCqcAQoNUmVjb3JkaW5nTW9kZRIeChpSRUNPUkRJTkdfTU9ERV9VTlNQRUNJRklFRBAAEiIKHlJFQ09SRElOR19NT0RFX1NUQVJUX1JFQ09SRElORxABEiEKHVJFQ09SRElOR19NT0RFX1NBVkVfUkVDT1JESU5HEAISJAogUkVDT1JESU5HX01PREVfRElTQ0FSRF9SRUNPUkRJTkcQAyqTAQofUmVxdWVzdGVkRmlsZVBhdGhSZWplY3RlZFJlYXNvbhIzCi9SRVFVRVNURURfRklMRV9QQVRIX1JFSkVDVEVEX1JFQVNPTl9VTlNQRUNJRklFRBAAEjsKN1JFUVVFU1RFRF9GSUxFX1BBVEhfUkVKRUNURURfUkVBU09OX1NMQVNIRVNfTk9UX0FMTE9XRUQQASqtAQoLUGFja2FnZVR5cGUSHAoYUEFDS0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobUEFDS0FHRV9UWVBFX0NVUlNPUl9QUk9KRUNUEAESIAocUEFDS0FHRV9UWVBFX0NVUlNPUl9QRVJTT05BTBACEh0KGVBBQ0tBR0VfVFlQRV9DTEFVREVfU0tJTEwQAxIeChpQQUNLQUdFX1RZUEVfQ0xBVURFX1BMVUdJThAEKn0KElNhbmRib3hQb2xpY3lfVHlwZRIUChBUWVBFX1VOU1BFQ0lGSUVEEAASFgoSVFlQRV9JTlNFQ1VSRV9OT05FEAESHAoYVFlQRV9XT1JLU1BBQ0VfUkVBRFdSSVRFEAISGwoXVFlQRV9XT1JLU1BBQ0VfUkVBRE9OTFkQAypxCg9UaW1lb3V0QmVoYXZpb3ISIAocVElNRU9VVF9CRUhBVklPUl9VTlNQRUNJRklFRBAAEhsKF1RJTUVPVVRfQkVIQVZJT1JfQ0FOQ0VMEAESHwobVElNRU9VVF9CRUhBVklPUl9CQUNLR1JPVU5EEAIqeQoQU2hlbGxBYm9ydFJlYXNvbhIiCh5TSEVMTF9BQk9SVF9SRUFTT05fVU5TUEVDSUZJRUQQABIhCh1TSEVMTF9BQk9SVF9SRUFTT05fVVNFUl9BQk9SVBABEh4KGlNIRUxMX0FCT1JUX1JFQVNPTl9USU1FT1VUEAIqqgEKHEN1c3RvbVN1YmFnZW50UGVybWlzc2lvbk1vZGUSLworQ1VTVE9NX1NVQkFHRU5UX1BFUk1JU1NJT05fTU9ERV9VTlNQRUNJRklFRBAAEisKJ0NVU1RPTV9TVUJBR0VOVF9QRVJNSVNTSU9OX01PREVfREVGQVVMVBABEiwKKENVU1RPTV9TVUJBR0VOVF9QRVJNSVNTSU9OX01PREVfUkVBRE9OTFkQAiqVAQoKVG9kb1N0YXR1cxIbChdUT0RPX1NUQVRVU19VTlNQRUNJRklFRBAAEhcKE1RPRE9fU1RBVFVTX1BFTkRJTkcQARIbChdUT0RPX1NUQVRVU19JTl9QUk9HUkVTUxACEhkKFVRPRE9fU1RBVFVTX0NPTVBMRVRFRBADEhkKFVRPRE9fU1RBVFVTX0NBTkNFTExFRBAEKmYKCENsaWVudE9TEhkKFUNMSUVOVF9PU19VTlNQRUNJRklFRBAAEhUKEUNMSUVOVF9PU19XSU5ET1dTEAESEwoPQ0xJRU5UX09TX01BQ09TEAISEwoPQ0xJRU5UX09TX0xJTlVYEAMq7AEKHEFydGlmYWN0VXBsb2FkRGlzcGF0Y2hTdGF0dXMSLworQVJUSUZBQ1RfVVBMT0FEX0RJU1BBVENIX1NUQVRVU19VTlNQRUNJRklFRBAAEiwKKEFSVElGQUNUX1VQTE9BRF9ESVNQQVRDSF9TVEFUVVNfQUNDRVBURUQQARIsCihBUlRJRkFDVF9VUExPQURfRElTUEFUQ0hfU1RBVFVTX1JFSkVDVEVEEAISPwo7QVJUSUZBQ1RfVVBMT0FEX0RJU1BBVENIX1NUQVRVU19TS0lQUEVEX0FMUkVBRFlfSU5fUFJPR1JFU1MQAypXCgpGcmFtZV9LaW5kEhQKEEtJTkRfVU5TUEVDSUZJRUQQABIQCgxLSU5EX1JFUVVFU1QQARIRCg1LSU5EX1JFU1BPTlNFEAISDgoKS0lORF9FUlJPUhADKrACChdCdWdib3REZWVwbGlua0V2ZW50S2luZBIqCiZCVUdCT1RfREVFUExJTktfRVZFTlRfS0lORF9VTlNQRUNJRklFRBAAEiYKIkJVR0JPVF9ERUVQTElOS19FVkVOVF9LSU5EX0NMSUNLRUQQARIzCi9CVUdCT1RfREVFUExJTktfRVZFTlRfS0lORF9IQU5ETEVEX0RJQUxPR19TSE9XThACEjMKL0JVR0JPVF9ERUVQTElOS19FVkVOVF9LSU5EX0hBTkRMRURfQ0hBVF9DUkVBVEVEEAMSJAogQlVHQk9UX0RFRVBMSU5LX0VWRU5UX0tJTkRfRVJST1IQBBIxCi1CVUdCT1RfREVFUExJTktfRVZFTlRfS0lORF9IQU5ETEVEX0ZJWF9JTl9XRUIQBTKHBAoMQWdlbnRTZXJ2aWNlEkEKA1J1bhIcLmFnZW50LnYxLkFnZW50Q2xpZW50TWVzc2FnZRocLmFnZW50LnYxLkFnZW50U2VydmVyTWVzc2FnZRI/CgZSdW5TU0USFy5hZ2VudC52MS5CaWRpUmVxdWVzdElkGhwuYWdlbnQudjEuQWdlbnRTZXJ2ZXJNZXNzYWdlEkQKCU5hbWVBZ2VudBIaLmFnZW50LnYxLk5hbWVBZ2VudFJlcXVlc3QaGy5hZ2VudC52MS5OYW1lQWdlbnRSZXNwb25zZRJWCg9HZXRVc2FibGVNb2RlbHMSIC5hZ2VudC52MS5HZXRVc2FibGVNb2RlbHNSZXF1ZXN0GiEuYWdlbnQudjEuR2V0VXNhYmxlTW9kZWxzUmVzcG9uc2USaAoVR2V0RGVmYXVsdE1vZGVsRm9yQ2xpEiYuYWdlbnQudjEuR2V0RGVmYXVsdE1vZGVsRm9yQ2xpUmVxdWVzdBonLmFnZW50LnYxLkdldERlZmF1bHRNb2RlbEZvckNsaVJlc3BvbnNlEmsKFkdldEFsbG93ZWRNb2RlbEludGVudHMSJy5hZ2VudC52MS5HZXRBbGxvd2VkTW9kZWxJbnRlbnRzUmVxdWVzdBooLmFnZW50LnYxLkdldEFsbG93ZWRNb2RlbEludGVudHNSZXNwb25zZTK1CAoOQ29udHJvbFNlcnZpY2USTQoMUmVhZFRleHRGaWxlEh0uYWdlbnQudjEuUmVhZFRleHRGaWxlUmVxdWVzdBoeLmFnZW50LnYxLlJlYWRUZXh0RmlsZVJlc3BvbnNlElAKDVdyaXRlVGV4dEZpbGUSHi5hZ2VudC52MS5Xcml0ZVRleHRGaWxlUmVxdWVzdBofLmFnZW50LnYxLldyaXRlVGV4dEZpbGVSZXNwb25zZRJTCg5SZWFkQmluYXJ5RmlsZRIfLmFnZW50LnYxLlJlYWRCaW5hcnlGaWxlUmVxdWVzdBogLmFnZW50LnYxLlJlYWRCaW5hcnlGaWxlUmVzcG9uc2USVgoPV3JpdGVCaW5hcnlGaWxlEiAuYWdlbnQudjEuV3JpdGVCaW5hcnlGaWxlUmVxdWVzdBohLmFnZW50LnYxLldyaXRlQmluYXJ5RmlsZVJlc3BvbnNlEm4KF0dldFdvcmtzcGFjZUNoYW5nZXNIYXNoEiguYWdlbnQudjEuR2V0V29ya3NwYWNlQ2hhbmdlc0hhc2hSZXF1ZXN0GikuYWdlbnQudjEuR2V0V29ya3NwYWNlQ2hhbmdlc0hhc2hSZXNwb25zZRJxChhSZWZyZXNoR2l0aHViQWNjZXNzVG9rZW4SKS5hZ2VudC52MS5SZWZyZXNoR2l0aHViQWNjZXNzVG9rZW5SZXF1ZXN0GiouYWdlbnQudjEuUmVmcmVzaEdpdGh1YkFjY2Vzc1Rva2VuUmVzcG9uc2USawoWV2FybVJlbW90ZUFjY2Vzc1NlcnZlchInLmFnZW50LnYxLldhcm1SZW1vdGVBY2Nlc3NTZXJ2ZXJSZXF1ZXN0GiguYWdlbnQudjEuV2FybVJlbW90ZUFjY2Vzc1NlcnZlclJlc3BvbnNlElAKDUxpc3RBcnRpZmFjdHMSHi5hZ2VudC52MS5MaXN0QXJ0aWZhY3RzUmVxdWVzdBofLmFnZW50LnYxLkxpc3RBcnRpZmFjdHNSZXNwb25zZRJWCg9VcGxvYWRBcnRpZmFjdHMSIC5hZ2VudC52MS5VcGxvYWRBcnRpZmFjdHNSZXF1ZXN0GiEuYWdlbnQudjEuVXBsb2FkQXJ0aWZhY3RzUmVzcG9uc2USYgoTR2V0TWNwUmVmcmVzaFRva2VucxIkLmFnZW50LnYxLkdldE1jcFJlZnJlc2hUb2tlbnNSZXF1ZXN0GiUuYWdlbnQudjEuR2V0TWNwUmVmcmVzaFRva2Vuc1Jlc3BvbnNlEncKGlVwZGF0ZUVudmlyb25tZW50VmFyaWFibGVzEisuYWdlbnQudjEuVXBkYXRlRW52aXJvbm1lbnRWYXJpYWJsZXNSZXF1ZXN0GiwuYWdlbnQudjEuVXBkYXRlRW52aXJvbm1lbnRWYXJpYWJsZXNSZXNwb25zZTINCgtFeGVjU2VydmljZTJRCiJQcml2YXRlV29ya2VyQnJpZGdlRXh0ZXJuYWxTZXJ2aWNlEisKB0Nvbm5lY3QSDy5hZ2VudC52MS5GcmFtZRoPLmFnZW50LnYxLkZyYW1lMngKEExpZmVjeWNsZVNlcnZpY2USMQoNUmVzZXRJbnN0YW5jZRIPLmFnZW50LnYxLkVtcHR5Gg8uYWdlbnQudjEuRW1wdHkSMQoNUmVuZXdJbnN0YW5jZRIPLmFnZW50LnYxLkVtcHR5Gg8uYWdlbnQudjEuRW1wdHliBnByb3RvMw"); -/** - * Describes the message agent.v1.GlobToolResult. - * Use `create(GlobToolResultSchema)` to create a new message. - */ -export const GlobToolResultSchema = /*@__PURE__*/ messageDesc(file_agent, 0); -/** - * Describes the message agent.v1.GlobToolError. - * Use `create(GlobToolErrorSchema)` to create a new message. - */ -export const GlobToolErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 1); -/** - * Describes the message agent.v1.GlobToolSuccess. - * Use `create(GlobToolSuccessSchema)` to create a new message. - */ -export const GlobToolSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 2); -/** - * Describes the message agent.v1.GlobToolCall. - * Use `create(GlobToolCallSchema)` to create a new message. - */ -export const GlobToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 3); -/** - * Describes the message agent.v1.ReadLintsToolCall. - * Use `create(ReadLintsToolCallSchema)` to create a new message. - */ -export const ReadLintsToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 4); -/** - * Describes the message agent.v1.ReadLintsToolArgs. - * Use `create(ReadLintsToolArgsSchema)` to create a new message. - */ -export const ReadLintsToolArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 5); -/** - * Describes the message agent.v1.ReadLintsToolResult. - * Use `create(ReadLintsToolResultSchema)` to create a new message. - */ -export const ReadLintsToolResultSchema = /*@__PURE__*/ messageDesc(file_agent, 6); -/** - * Describes the message agent.v1.ReadLintsToolSuccess. - * Use `create(ReadLintsToolSuccessSchema)` to create a new message. - */ -export const ReadLintsToolSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 7); -/** - * Describes the message agent.v1.FileDiagnostics. - * Use `create(FileDiagnosticsSchema)` to create a new message. - */ -export const FileDiagnosticsSchema = /*@__PURE__*/ messageDesc(file_agent, 8); -/** - * Describes the message agent.v1.DiagnosticItem. - * Use `create(DiagnosticItemSchema)` to create a new message. - */ -export const DiagnosticItemSchema = /*@__PURE__*/ messageDesc(file_agent, 9); -/** - * Describes the message agent.v1.DiagnosticRange. - * Use `create(DiagnosticRangeSchema)` to create a new message. - */ -export const DiagnosticRangeSchema = /*@__PURE__*/ messageDesc(file_agent, 10); -/** - * Describes the message agent.v1.ReadLintsToolError. - * Use `create(ReadLintsToolErrorSchema)` to create a new message. - */ -export const ReadLintsToolErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 11); -/** - * Describes the message agent.v1.McpToolError. - * Use `create(McpToolErrorSchema)` to create a new message. - */ -export const McpToolErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 12); -/** - * Describes the message agent.v1.McpToolResult. - * Use `create(McpToolResultSchema)` to create a new message. - */ -export const McpToolResultSchema = /*@__PURE__*/ messageDesc(file_agent, 13); -/** - * Describes the message agent.v1.McpToolCall. - * Use `create(McpToolCallSchema)` to create a new message. - */ -export const McpToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 14); -/** - * Describes the message agent.v1.SemSearchToolCall. - * Use `create(SemSearchToolCallSchema)` to create a new message. - */ -export const SemSearchToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 15); -/** - * Describes the message agent.v1.SemSearchToolArgs. - * Use `create(SemSearchToolArgsSchema)` to create a new message. - */ -export const SemSearchToolArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 16); -/** - * Describes the message agent.v1.SemSearchToolResult. - * Use `create(SemSearchToolResultSchema)` to create a new message. - */ -export const SemSearchToolResultSchema = /*@__PURE__*/ messageDesc(file_agent, 17); -/** - * Describes the message agent.v1.SemSearchToolSuccess. - * Use `create(SemSearchToolSuccessSchema)` to create a new message. - */ -export const SemSearchToolSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 18); -/** - * Describes the message agent.v1.SemSearchToolError. - * Use `create(SemSearchToolErrorSchema)` to create a new message. - */ -export const SemSearchToolErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 19); -/** - * Describes the message agent.v1.ListMcpResourcesToolCall. - * Use `create(ListMcpResourcesToolCallSchema)` to create a new message. - */ -export const ListMcpResourcesToolCallSchema = -/*@__PURE__*/ -messageDesc(file_agent, 20); -/** - * Describes the message agent.v1.ReadMcpResourceToolCall. - * Use `create(ReadMcpResourceToolCallSchema)` to create a new message. - */ -export const ReadMcpResourceToolCallSchema = -/*@__PURE__*/ -messageDesc(file_agent, 21); -/** - * Describes the message agent.v1.FetchToolCall. - * Use `create(FetchToolCallSchema)` to create a new message. - */ -export const FetchToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 22); -/** - * Describes the message agent.v1.RecordScreenToolCall. - * Use `create(RecordScreenToolCallSchema)` to create a new message. - */ -export const RecordScreenToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 23); -/** - * Describes the message agent.v1.WriteShellStdinToolCall. - * Use `create(WriteShellStdinToolCallSchema)` to create a new message. - */ -export const WriteShellStdinToolCallSchema = -/*@__PURE__*/ -messageDesc(file_agent, 24); -/** - * Describes the message agent.v1.ReflectArgs. - * Use `create(ReflectArgsSchema)` to create a new message. - */ -export const ReflectArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 25); -/** - * Describes the message agent.v1.ReflectResult. - * Use `create(ReflectResultSchema)` to create a new message. - */ -export const ReflectResultSchema = /*@__PURE__*/ messageDesc(file_agent, 26); -/** - * Describes the message agent.v1.ReflectSuccess. - * Use `create(ReflectSuccessSchema)` to create a new message. - */ -export const ReflectSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 27); -/** - * Describes the message agent.v1.ReflectError. - * Use `create(ReflectErrorSchema)` to create a new message. - */ -export const ReflectErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 28); -/** - * Describes the message agent.v1.ReflectToolCall. - * Use `create(ReflectToolCallSchema)` to create a new message. - */ -export const ReflectToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 29); -/** - * Describes the message agent.v1.StartGrindExecutionArgs. - * Use `create(StartGrindExecutionArgsSchema)` to create a new message. - */ -export const StartGrindExecutionArgsSchema = -/*@__PURE__*/ -messageDesc(file_agent, 30); -/** - * Describes the message agent.v1.StartGrindExecutionResult. - * Use `create(StartGrindExecutionResultSchema)` to create a new message. - */ -export const StartGrindExecutionResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 31); -/** - * Describes the message agent.v1.StartGrindExecutionSuccess. - * Use `create(StartGrindExecutionSuccessSchema)` to create a new message. - */ -export const StartGrindExecutionSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 32); -/** - * Describes the message agent.v1.StartGrindExecutionError. - * Use `create(StartGrindExecutionErrorSchema)` to create a new message. - */ -export const StartGrindExecutionErrorSchema = -/*@__PURE__*/ -messageDesc(file_agent, 33); -/** - * Describes the message agent.v1.StartGrindExecutionToolCall. - * Use `create(StartGrindExecutionToolCallSchema)` to create a new message. - */ -export const StartGrindExecutionToolCallSchema = -/*@__PURE__*/ -messageDesc(file_agent, 34); -/** - * Describes the message agent.v1.StartGrindPlanningArgs. - * Use `create(StartGrindPlanningArgsSchema)` to create a new message. - */ -export const StartGrindPlanningArgsSchema = -/*@__PURE__*/ -messageDesc(file_agent, 35); -/** - * Describes the message agent.v1.StartGrindPlanningResult. - * Use `create(StartGrindPlanningResultSchema)` to create a new message. - */ -export const StartGrindPlanningResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 36); -/** - * Describes the message agent.v1.StartGrindPlanningSuccess. - * Use `create(StartGrindPlanningSuccessSchema)` to create a new message. - */ -export const StartGrindPlanningSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 37); -/** - * Describes the message agent.v1.StartGrindPlanningError. - * Use `create(StartGrindPlanningErrorSchema)` to create a new message. - */ -export const StartGrindPlanningErrorSchema = -/*@__PURE__*/ -messageDesc(file_agent, 38); -/** - * Describes the message agent.v1.StartGrindPlanningToolCall. - * Use `create(StartGrindPlanningToolCallSchema)` to create a new message. - */ -export const StartGrindPlanningToolCallSchema = -/*@__PURE__*/ -messageDesc(file_agent, 39); -/** - * Describes the message agent.v1.TaskArgs. - * Use `create(TaskArgsSchema)` to create a new message. - */ -export const TaskArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 40); -/** - * Describes the message agent.v1.TaskSuccess. - * Use `create(TaskSuccessSchema)` to create a new message. - */ -export const TaskSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 41); -/** - * Describes the message agent.v1.TaskError. - * Use `create(TaskErrorSchema)` to create a new message. - */ -export const TaskErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 42); -/** - * Describes the message agent.v1.TaskResult. - * Use `create(TaskResultSchema)` to create a new message. - */ -export const TaskResultSchema = /*@__PURE__*/ messageDesc(file_agent, 43); -/** - * Describes the message agent.v1.TaskToolCall. - * Use `create(TaskToolCallSchema)` to create a new message. - */ -export const TaskToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 44); -/** - * Describes the message agent.v1.TaskToolCallDelta. - * Use `create(TaskToolCallDeltaSchema)` to create a new message. - */ -export const TaskToolCallDeltaSchema = /*@__PURE__*/ messageDesc(file_agent, 45); -/** - * Describes the message agent.v1.ToolCall. - * Use `create(ToolCallSchema)` to create a new message. - */ -export const ToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 46); -/** - * Describes the message agent.v1.TruncatedToolCallArgs. - * Use `create(TruncatedToolCallArgsSchema)` to create a new message. - */ -export const TruncatedToolCallArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 47); -/** - * Describes the message agent.v1.TruncatedToolCallSuccess. - * Use `create(TruncatedToolCallSuccessSchema)` to create a new message. - */ -export const TruncatedToolCallSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 48); -/** - * Describes the message agent.v1.TruncatedToolCallError. - * Use `create(TruncatedToolCallErrorSchema)` to create a new message. - */ -export const TruncatedToolCallErrorSchema = -/*@__PURE__*/ -messageDesc(file_agent, 49); -/** - * Describes the message agent.v1.TruncatedToolCallResult. - * Use `create(TruncatedToolCallResultSchema)` to create a new message. - */ -export const TruncatedToolCallResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 50); -/** - * Describes the message agent.v1.TruncatedToolCall. - * Use `create(TruncatedToolCallSchema)` to create a new message. - */ -export const TruncatedToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 51); -/** - * Describes the message agent.v1.ToolCallDelta. - * Use `create(ToolCallDeltaSchema)` to create a new message. - */ -export const ToolCallDeltaSchema = /*@__PURE__*/ messageDesc(file_agent, 52); -/** - * Describes the message agent.v1.ConversationStep. - * Use `create(ConversationStepSchema)` to create a new message. - */ -export const ConversationStepSchema = /*@__PURE__*/ messageDesc(file_agent, 53); -/** - * Describes the message agent.v1.ConversationAction. - * Use `create(ConversationActionSchema)` to create a new message. - */ -export const ConversationActionSchema = /*@__PURE__*/ messageDesc(file_agent, 54); -/** - * Describes the message agent.v1.UserMessageAction. - * Use `create(UserMessageActionSchema)` to create a new message. - */ -export const UserMessageActionSchema = /*@__PURE__*/ messageDesc(file_agent, 55); -/** - * Describes the message agent.v1.CancelAction. - * Use `create(CancelActionSchema)` to create a new message. - */ -export const CancelActionSchema = /*@__PURE__*/ messageDesc(file_agent, 56); -/** - * Describes the message agent.v1.ResumeAction. - * Use `create(ResumeActionSchema)` to create a new message. - */ -export const ResumeActionSchema = /*@__PURE__*/ messageDesc(file_agent, 57); -/** - * Describes the message agent.v1.AsyncAskQuestionCompletionAction. - * Use `create(AsyncAskQuestionCompletionActionSchema)` to create a new message. - */ -export const AsyncAskQuestionCompletionActionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 58); -/** - * Describes the message agent.v1.SummarizeAction. - * Use `create(SummarizeActionSchema)` to create a new message. - */ -export const SummarizeActionSchema = /*@__PURE__*/ messageDesc(file_agent, 59); -/** - * Describes the message agent.v1.ShellCommandAction. - * Use `create(ShellCommandActionSchema)` to create a new message. - */ -export const ShellCommandActionSchema = /*@__PURE__*/ messageDesc(file_agent, 60); -/** - * Describes the message agent.v1.StartPlanAction. - * Use `create(StartPlanActionSchema)` to create a new message. - */ -export const StartPlanActionSchema = /*@__PURE__*/ messageDesc(file_agent, 61); -/** - * Describes the message agent.v1.ExecutePlanAction. - * Use `create(ExecutePlanActionSchema)` to create a new message. - */ -export const ExecutePlanActionSchema = /*@__PURE__*/ messageDesc(file_agent, 62); -/** - * Describes the message agent.v1.UserMessage. - * Use `create(UserMessageSchema)` to create a new message. - */ -export const UserMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 63); -/** - * Describes the message agent.v1.AssistantMessage. - * Use `create(AssistantMessageSchema)` to create a new message. - */ -export const AssistantMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 64); -/** - * Describes the message agent.v1.ThinkingMessage. - * Use `create(ThinkingMessageSchema)` to create a new message. - */ -export const ThinkingMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 65); -/** - * Describes the message agent.v1.ShellCommand. - * Use `create(ShellCommandSchema)` to create a new message. - */ -export const ShellCommandSchema = /*@__PURE__*/ messageDesc(file_agent, 66); -/** - * Describes the message agent.v1.ShellOutput. - * Use `create(ShellOutputSchema)` to create a new message. - */ -export const ShellOutputSchema = /*@__PURE__*/ messageDesc(file_agent, 67); -/** - * Describes the message agent.v1.ConversationTurn. - * Use `create(ConversationTurnSchema)` to create a new message. - */ -export const ConversationTurnSchema = /*@__PURE__*/ messageDesc(file_agent, 68); -/** - * Describes the message agent.v1.ConversationPlan. - * Use `create(ConversationPlanSchema)` to create a new message. - */ -export const ConversationPlanSchema = /*@__PURE__*/ messageDesc(file_agent, 69); -/** - * Describes the message agent.v1.ConversationTurnStructure. - * Use `create(ConversationTurnStructureSchema)` to create a new message. - */ -export const ConversationTurnStructureSchema = -/*@__PURE__*/ -messageDesc(file_agent, 70); -/** - * Describes the message agent.v1.AgentConversationTurn. - * Use `create(AgentConversationTurnSchema)` to create a new message. - */ -export const AgentConversationTurnSchema = /*@__PURE__*/ messageDesc(file_agent, 71); -/** - * Describes the message agent.v1.AgentConversationTurnStructure. - * Use `create(AgentConversationTurnStructureSchema)` to create a new message. - */ -export const AgentConversationTurnStructureSchema = -/*@__PURE__*/ -messageDesc(file_agent, 72); -/** - * Describes the message agent.v1.ShellConversationTurn. - * Use `create(ShellConversationTurnSchema)` to create a new message. - */ -export const ShellConversationTurnSchema = /*@__PURE__*/ messageDesc(file_agent, 73); -/** - * Describes the message agent.v1.ShellConversationTurnStructure. - * Use `create(ShellConversationTurnStructureSchema)` to create a new message. - */ -export const ShellConversationTurnStructureSchema = -/*@__PURE__*/ -messageDesc(file_agent, 74); -/** - * Describes the message agent.v1.ConversationSummary. - * Use `create(ConversationSummarySchema)` to create a new message. - */ -export const ConversationSummarySchema = /*@__PURE__*/ messageDesc(file_agent, 75); -/** - * Describes the message agent.v1.ConversationSummaryArchive. - * Use `create(ConversationSummaryArchiveSchema)` to create a new message. - */ -export const ConversationSummaryArchiveSchema = -/*@__PURE__*/ -messageDesc(file_agent, 76); -/** - * Describes the message agent.v1.ConversationTokenDetails. - * Use `create(ConversationTokenDetailsSchema)` to create a new message. - */ -export const ConversationTokenDetailsSchema = -/*@__PURE__*/ -messageDesc(file_agent, 77); -/** - * Describes the message agent.v1.FileState. - * Use `create(FileStateSchema)` to create a new message. - */ -export const FileStateSchema = /*@__PURE__*/ messageDesc(file_agent, 78); -/** - * Describes the message agent.v1.FileStateStructure. - * Use `create(FileStateStructureSchema)` to create a new message. - */ -export const FileStateStructureSchema = /*@__PURE__*/ messageDesc(file_agent, 79); -/** - * Describes the message agent.v1.StepTiming. - * Use `create(StepTimingSchema)` to create a new message. - */ -export const StepTimingSchema = /*@__PURE__*/ messageDesc(file_agent, 80); -/** - * Describes the message agent.v1.ConversationState. - * Use `create(ConversationStateSchema)` to create a new message. - */ -export const ConversationStateSchema = /*@__PURE__*/ messageDesc(file_agent, 81); -/** - * Describes the message agent.v1.SubagentPersistedState. - * Use `create(SubagentPersistedStateSchema)` to create a new message. - */ -export const SubagentPersistedStateSchema = -/*@__PURE__*/ -messageDesc(file_agent, 82); -/** - * Describes the message agent.v1.ConversationStateStructure. - * Use `create(ConversationStateStructureSchema)` to create a new message. - */ -export const ConversationStateStructureSchema = -/*@__PURE__*/ -messageDesc(file_agent, 83); -/** - * Describes the message agent.v1.ThinkingDetails. - * Use `create(ThinkingDetailsSchema)` to create a new message. - */ -export const ThinkingDetailsSchema = /*@__PURE__*/ messageDesc(file_agent, 84); -/** - * Describes the message agent.v1.ApiKeyCredentials. - * Use `create(ApiKeyCredentialsSchema)` to create a new message. - */ -export const ApiKeyCredentialsSchema = /*@__PURE__*/ messageDesc(file_agent, 85); -/** - * Describes the message agent.v1.AzureCredentials. - * Use `create(AzureCredentialsSchema)` to create a new message. - */ -export const AzureCredentialsSchema = /*@__PURE__*/ messageDesc(file_agent, 86); -/** - * Describes the message agent.v1.BedrockCredentials. - * Use `create(BedrockCredentialsSchema)` to create a new message. - */ -export const BedrockCredentialsSchema = /*@__PURE__*/ messageDesc(file_agent, 87); -/** - * Describes the message agent.v1.ModelDetails. - * Use `create(ModelDetailsSchema)` to create a new message. - */ -export const ModelDetailsSchema = /*@__PURE__*/ messageDesc(file_agent, 88); -/** - * Describes the message agent.v1.RequestedModel. - * Use `create(RequestedModelSchema)` to create a new message. - */ -export const RequestedModelSchema = /*@__PURE__*/ messageDesc(file_agent, 89); -/** - * Describes the message agent.v1.RequestedModel_ModelParameterbytes. - * Use `create(RequestedModel_ModelParameterbytesSchema)` to create a new message. - */ -export const RequestedModel_ModelParameterbytesSchema = -/*@__PURE__*/ -messageDesc(file_agent, 90); -/** - * Describes the message agent.v1.AgentRunRequest. - * Use `create(AgentRunRequestSchema)` to create a new message. - */ -export const AgentRunRequestSchema = /*@__PURE__*/ messageDesc(file_agent, 91); -/** - * Describes the message agent.v1.TextDeltaUpdate. - * Use `create(TextDeltaUpdateSchema)` to create a new message. - */ -export const TextDeltaUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 92); -/** - * Describes the message agent.v1.ToolCallStartedUpdate. - * Use `create(ToolCallStartedUpdateSchema)` to create a new message. - */ -export const ToolCallStartedUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 93); -/** - * Describes the message agent.v1.ToolCallCompletedUpdate. - * Use `create(ToolCallCompletedUpdateSchema)` to create a new message. - */ -export const ToolCallCompletedUpdateSchema = -/*@__PURE__*/ -messageDesc(file_agent, 94); -/** - * Describes the message agent.v1.ToolCallDeltaUpdate. - * Use `create(ToolCallDeltaUpdateSchema)` to create a new message. - */ -export const ToolCallDeltaUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 95); -/** - * Describes the message agent.v1.PartialToolCallUpdate. - * Use `create(PartialToolCallUpdateSchema)` to create a new message. - */ -export const PartialToolCallUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 96); -/** - * Describes the message agent.v1.ThinkingDeltaUpdate. - * Use `create(ThinkingDeltaUpdateSchema)` to create a new message. - */ -export const ThinkingDeltaUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 97); -/** - * Describes the message agent.v1.ThinkingCompletedUpdate. - * Use `create(ThinkingCompletedUpdateSchema)` to create a new message. - */ -export const ThinkingCompletedUpdateSchema = -/*@__PURE__*/ -messageDesc(file_agent, 98); -/** - * Describes the message agent.v1.TokenDeltaUpdate. - * Use `create(TokenDeltaUpdateSchema)` to create a new message. - */ -export const TokenDeltaUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 99); -/** - * Describes the message agent.v1.SummaryUpdate. - * Use `create(SummaryUpdateSchema)` to create a new message. - */ -export const SummaryUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 100); -/** - * Describes the message agent.v1.SummaryStartedUpdate. - * Use `create(SummaryStartedUpdateSchema)` to create a new message. - */ -export const SummaryStartedUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 101); -/** - * Describes the message agent.v1.HeartbeatUpdate. - * Use `create(HeartbeatUpdateSchema)` to create a new message. - */ -export const HeartbeatUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 102); -/** - * Describes the message agent.v1.SummaryCompletedUpdate. - * Use `create(SummaryCompletedUpdateSchema)` to create a new message. - */ -export const SummaryCompletedUpdateSchema = -/*@__PURE__*/ -messageDesc(file_agent, 103); -/** - * Describes the message agent.v1.ShellOutputDeltaUpdate. - * Use `create(ShellOutputDeltaUpdateSchema)` to create a new message. - */ -export const ShellOutputDeltaUpdateSchema = -/*@__PURE__*/ -messageDesc(file_agent, 104); -/** - * Describes the message agent.v1.TurnEndedUpdate. - * Use `create(TurnEndedUpdateSchema)` to create a new message. - */ -export const TurnEndedUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 105); -/** - * Describes the message agent.v1.UserMessageAppendedUpdate. - * Use `create(UserMessageAppendedUpdateSchema)` to create a new message. - */ -export const UserMessageAppendedUpdateSchema = -/*@__PURE__*/ -messageDesc(file_agent, 106); -/** - * Describes the message agent.v1.StepStartedUpdate. - * Use `create(StepStartedUpdateSchema)` to create a new message. - */ -export const StepStartedUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 107); -/** - * Describes the message agent.v1.StepCompletedUpdate. - * Use `create(StepCompletedUpdateSchema)` to create a new message. - */ -export const StepCompletedUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 108); -/** - * Describes the message agent.v1.InteractionUpdate. - * Use `create(InteractionUpdateSchema)` to create a new message. - */ -export const InteractionUpdateSchema = /*@__PURE__*/ messageDesc(file_agent, 109); -/** - * Describes the message agent.v1.InteractionQuery. - * Use `create(InteractionQuerySchema)` to create a new message. - */ -export const InteractionQuerySchema = /*@__PURE__*/ messageDesc(file_agent, 110); -/** - * Describes the message agent.v1.InteractionResponse. - * Use `create(InteractionResponseSchema)` to create a new message. - */ -export const InteractionResponseSchema = /*@__PURE__*/ messageDesc(file_agent, 111); -/** - * Describes the message agent.v1.AskQuestionInteractionQuery. - * Use `create(AskQuestionInteractionQuerySchema)` to create a new message. - */ -export const AskQuestionInteractionQuerySchema = -/*@__PURE__*/ -messageDesc(file_agent, 112); -/** - * Describes the message agent.v1.AskQuestionInteractionResponse. - * Use `create(AskQuestionInteractionResponseSchema)` to create a new message. - */ -export const AskQuestionInteractionResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 113); -/** - * Describes the message agent.v1.ClientHeartbeat. - * Use `create(ClientHeartbeatSchema)` to create a new message. - */ -export const ClientHeartbeatSchema = /*@__PURE__*/ messageDesc(file_agent, 114); -/** - * Describes the message agent.v1.PrewarmRequest. - * Use `create(PrewarmRequestSchema)` to create a new message. - */ -export const PrewarmRequestSchema = /*@__PURE__*/ messageDesc(file_agent, 115); -/** - * Describes the message agent.v1.ExecServerAbort. - * Use `create(ExecServerAbortSchema)` to create a new message. - */ -export const ExecServerAbortSchema = /*@__PURE__*/ messageDesc(file_agent, 116); -/** - * Describes the message agent.v1.ExecServerControlMessage. - * Use `create(ExecServerControlMessageSchema)` to create a new message. - */ -export const ExecServerControlMessageSchema = -/*@__PURE__*/ -messageDesc(file_agent, 117); -/** - * Describes the message agent.v1.AgentClientMessage. - * Use `create(AgentClientMessageSchema)` to create a new message. - */ -export const AgentClientMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 118); -/** - * Describes the message agent.v1.AgentServerMessage. - * Use `create(AgentServerMessageSchema)` to create a new message. - */ -export const AgentServerMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 119); -/** - * Describes the message agent.v1.NameAgentRequest. - * Use `create(NameAgentRequestSchema)` to create a new message. - */ -export const NameAgentRequestSchema = /*@__PURE__*/ messageDesc(file_agent, 120); -/** - * Describes the message agent.v1.NameAgentResponse. - * Use `create(NameAgentResponseSchema)` to create a new message. - */ -export const NameAgentResponseSchema = /*@__PURE__*/ messageDesc(file_agent, 121); -/** - * Describes the message agent.v1.GetUsableModelsRequest. - * Use `create(GetUsableModelsRequestSchema)` to create a new message. - */ -export const GetUsableModelsRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 122); -/** - * Describes the message agent.v1.GetUsableModelsResponse. - * Use `create(GetUsableModelsResponseSchema)` to create a new message. - */ -export const GetUsableModelsResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 123); -/** - * Describes the message agent.v1.GetDefaultModelForCliRequest. - * Use `create(GetDefaultModelForCliRequestSchema)` to create a new message. - */ -export const GetDefaultModelForCliRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 124); -/** - * Describes the message agent.v1.GetDefaultModelForCliResponse. - * Use `create(GetDefaultModelForCliResponseSchema)` to create a new message. - */ -export const GetDefaultModelForCliResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 125); -/** - * Describes the message agent.v1.GetAllowedModelIntentsRequest. - * Use `create(GetAllowedModelIntentsRequestSchema)` to create a new message. - */ -export const GetAllowedModelIntentsRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 126); -/** - * Describes the message agent.v1.GetAllowedModelIntentsResponse. - * Use `create(GetAllowedModelIntentsResponseSchema)` to create a new message. - */ -export const GetAllowedModelIntentsResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 127); -/** - * Describes the message agent.v1.IdeEditorsStateFile. - * Use `create(IdeEditorsStateFileSchema)` to create a new message. - */ -export const IdeEditorsStateFileSchema = /*@__PURE__*/ messageDesc(file_agent, 128); -/** - * Describes the message agent.v1.IdeEditorsStateLite. - * Use `create(IdeEditorsStateLiteSchema)` to create a new message. - */ -export const IdeEditorsStateLiteSchema = /*@__PURE__*/ messageDesc(file_agent, 129); -/** - * Describes the message agent.v1.ApplyAgentDiffToolCall. - * Use `create(ApplyAgentDiffToolCallSchema)` to create a new message. - */ -export const ApplyAgentDiffToolCallSchema = -/*@__PURE__*/ -messageDesc(file_agent, 130); -/** - * Describes the message agent.v1.ApplyAgentDiffArgs. - * Use `create(ApplyAgentDiffArgsSchema)` to create a new message. - */ -export const ApplyAgentDiffArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 131); -/** - * Describes the message agent.v1.ApplyAgentDiffResult. - * Use `create(ApplyAgentDiffResultSchema)` to create a new message. - */ -export const ApplyAgentDiffResultSchema = /*@__PURE__*/ messageDesc(file_agent, 132); -/** - * Describes the message agent.v1.ApplyAgentDiffSuccess. - * Use `create(ApplyAgentDiffSuccessSchema)` to create a new message. - */ -export const ApplyAgentDiffSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 133); -/** - * Describes the message agent.v1.AppliedAgentChange. - * Use `create(AppliedAgentChangeSchema)` to create a new message. - */ -export const AppliedAgentChangeSchema = /*@__PURE__*/ messageDesc(file_agent, 134); -/** - * Describes the message agent.v1.ApplyAgentDiffError. - * Use `create(ApplyAgentDiffErrorSchema)` to create a new message. - */ -export const ApplyAgentDiffErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 135); -/** - * Describes the message agent.v1.AskQuestionToolCall. - * Use `create(AskQuestionToolCallSchema)` to create a new message. - */ -export const AskQuestionToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 136); -/** - * Describes the message agent.v1.AskQuestionArgs. - * Use `create(AskQuestionArgsSchema)` to create a new message. - */ -export const AskQuestionArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 137); -/** - * Describes the message agent.v1.AskQuestionArgs_Question. - * Use `create(AskQuestionArgs_QuestionSchema)` to create a new message. - */ -export const AskQuestionArgs_QuestionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 138); -/** - * Describes the message agent.v1.AskQuestionArgs_Option. - * Use `create(AskQuestionArgs_OptionSchema)` to create a new message. - */ -export const AskQuestionArgs_OptionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 139); -/** - * Describes the message agent.v1.AskQuestionAsync. - * Use `create(AskQuestionAsyncSchema)` to create a new message. - */ -export const AskQuestionAsyncSchema = /*@__PURE__*/ messageDesc(file_agent, 140); -/** - * Describes the message agent.v1.AskQuestionResult. - * Use `create(AskQuestionResultSchema)` to create a new message. - */ -export const AskQuestionResultSchema = /*@__PURE__*/ messageDesc(file_agent, 141); -/** - * Describes the message agent.v1.AskQuestionSuccess. - * Use `create(AskQuestionSuccessSchema)` to create a new message. - */ -export const AskQuestionSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 142); -/** - * Describes the message agent.v1.AskQuestionSuccess_Answer. - * Use `create(AskQuestionSuccess_AnswerSchema)` to create a new message. - */ -export const AskQuestionSuccess_AnswerSchema = -/*@__PURE__*/ -messageDesc(file_agent, 143); -/** - * Describes the message agent.v1.AskQuestionError. - * Use `create(AskQuestionErrorSchema)` to create a new message. - */ -export const AskQuestionErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 144); -/** - * Describes the message agent.v1.AskQuestionRejected. - * Use `create(AskQuestionRejectedSchema)` to create a new message. - */ -export const AskQuestionRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 145); -/** - * Describes the message agent.v1.BackgroundShellSpawnArgs. - * Use `create(BackgroundShellSpawnArgsSchema)` to create a new message. - */ -export const BackgroundShellSpawnArgsSchema = -/*@__PURE__*/ -messageDesc(file_agent, 146); -/** - * Describes the message agent.v1.BackgroundShellSpawnResult. - * Use `create(BackgroundShellSpawnResultSchema)` to create a new message. - */ -export const BackgroundShellSpawnResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 147); -/** - * Describes the message agent.v1.BackgroundShellSpawnSuccess. - * Use `create(BackgroundShellSpawnSuccessSchema)` to create a new message. - */ -export const BackgroundShellSpawnSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 148); -/** - * Describes the message agent.v1.BackgroundShellSpawnError. - * Use `create(BackgroundShellSpawnErrorSchema)` to create a new message. - */ -export const BackgroundShellSpawnErrorSchema = -/*@__PURE__*/ -messageDesc(file_agent, 149); -/** - * Describes the message agent.v1.WriteShellStdinArgs. - * Use `create(WriteShellStdinArgsSchema)` to create a new message. - */ -export const WriteShellStdinArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 150); -/** - * Describes the message agent.v1.WriteShellStdinResult. - * Use `create(WriteShellStdinResultSchema)` to create a new message. - */ -export const WriteShellStdinResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 151); -/** - * Describes the message agent.v1.WriteShellStdinSuccess. - * Use `create(WriteShellStdinSuccessSchema)` to create a new message. - */ -export const WriteShellStdinSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 152); -/** - * Describes the message agent.v1.WriteShellStdinError. - * Use `create(WriteShellStdinErrorSchema)` to create a new message. - */ -export const WriteShellStdinErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 153); -/** - * Describes the message agent.v1.Coordinate. - * Use `create(CoordinateSchema)` to create a new message. - */ -export const CoordinateSchema = /*@__PURE__*/ messageDesc(file_agent, 154); -/** - * Describes the message agent.v1.ComputerUseArgs. - * Use `create(ComputerUseArgsSchema)` to create a new message. - */ -export const ComputerUseArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 155); -/** - * Describes the message agent.v1.ComputerUseAction. - * Use `create(ComputerUseActionSchema)` to create a new message. - */ -export const ComputerUseActionSchema = /*@__PURE__*/ messageDesc(file_agent, 156); -/** - * Describes the message agent.v1.MouseMoveAction. - * Use `create(MouseMoveActionSchema)` to create a new message. - */ -export const MouseMoveActionSchema = /*@__PURE__*/ messageDesc(file_agent, 157); -/** - * Describes the message agent.v1.ClickAction. - * Use `create(ClickActionSchema)` to create a new message. - */ -export const ClickActionSchema = /*@__PURE__*/ messageDesc(file_agent, 158); -/** - * Describes the message agent.v1.MouseDownAction. - * Use `create(MouseDownActionSchema)` to create a new message. - */ -export const MouseDownActionSchema = /*@__PURE__*/ messageDesc(file_agent, 159); -/** - * Describes the message agent.v1.MouseUpAction. - * Use `create(MouseUpActionSchema)` to create a new message. - */ -export const MouseUpActionSchema = /*@__PURE__*/ messageDesc(file_agent, 160); -/** - * Describes the message agent.v1.DragAction. - * Use `create(DragActionSchema)` to create a new message. - */ -export const DragActionSchema = /*@__PURE__*/ messageDesc(file_agent, 161); -/** - * Describes the message agent.v1.ScrollAction. - * Use `create(ScrollActionSchema)` to create a new message. - */ -export const ScrollActionSchema = /*@__PURE__*/ messageDesc(file_agent, 162); -/** - * Describes the message agent.v1.TypeAction. - * Use `create(TypeActionSchema)` to create a new message. - */ -export const TypeActionSchema = /*@__PURE__*/ messageDesc(file_agent, 163); -/** - * Describes the message agent.v1.KeyAction. - * Use `create(KeyActionSchema)` to create a new message. - */ -export const KeyActionSchema = /*@__PURE__*/ messageDesc(file_agent, 164); -/** - * Describes the message agent.v1.WaitAction. - * Use `create(WaitActionSchema)` to create a new message. - */ -export const WaitActionSchema = /*@__PURE__*/ messageDesc(file_agent, 165); -/** - * Describes the message agent.v1.ScreenshotAction. - * Use `create(ScreenshotActionSchema)` to create a new message. - */ -export const ScreenshotActionSchema = /*@__PURE__*/ messageDesc(file_agent, 166); -/** - * Describes the message agent.v1.CursorPositionAction. - * Use `create(CursorPositionActionSchema)` to create a new message. - */ -export const CursorPositionActionSchema = /*@__PURE__*/ messageDesc(file_agent, 167); -/** - * Describes the message agent.v1.ComputerUseResult. - * Use `create(ComputerUseResultSchema)` to create a new message. - */ -export const ComputerUseResultSchema = /*@__PURE__*/ messageDesc(file_agent, 168); -/** - * Describes the message agent.v1.ComputerUseSuccess. - * Use `create(ComputerUseSuccessSchema)` to create a new message. - */ -export const ComputerUseSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 169); -/** - * Describes the message agent.v1.ComputerUseError. - * Use `create(ComputerUseErrorSchema)` to create a new message. - */ -export const ComputerUseErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 170); -/** - * Describes the message agent.v1.ComputerUseToolCall. - * Use `create(ComputerUseToolCallSchema)` to create a new message. - */ -export const ComputerUseToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 171); -/** - * Describes the message agent.v1.CreatePlanToolCall. - * Use `create(CreatePlanToolCallSchema)` to create a new message. - */ -export const CreatePlanToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 172); -/** - * Describes the message agent.v1.Phase. - * Use `create(PhaseSchema)` to create a new message. - */ -export const PhaseSchema = /*@__PURE__*/ messageDesc(file_agent, 173); -/** - * Describes the message agent.v1.CreatePlanArgs. - * Use `create(CreatePlanArgsSchema)` to create a new message. - */ -export const CreatePlanArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 174); -/** - * Describes the message agent.v1.CreatePlanResult. - * Use `create(CreatePlanResultSchema)` to create a new message. - */ -export const CreatePlanResultSchema = /*@__PURE__*/ messageDesc(file_agent, 175); -/** - * Describes the message agent.v1.CreatePlanSuccess. - * Use `create(CreatePlanSuccessSchema)` to create a new message. - */ -export const CreatePlanSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 176); -/** - * Describes the message agent.v1.CreatePlanError. - * Use `create(CreatePlanErrorSchema)` to create a new message. - */ -export const CreatePlanErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 177); -/** - * Describes the message agent.v1.CreatePlanRequestQuery. - * Use `create(CreatePlanRequestQuerySchema)` to create a new message. - */ -export const CreatePlanRequestQuerySchema = -/*@__PURE__*/ -messageDesc(file_agent, 178); -/** - * Describes the message agent.v1.CreatePlanRequestResponse. - * Use `create(CreatePlanRequestResponseSchema)` to create a new message. - */ -export const CreatePlanRequestResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 179); -/** - * Describes the message agent.v1.CursorRuleTypeGlobal. - * Use `create(CursorRuleTypeGlobalSchema)` to create a new message. - */ -export const CursorRuleTypeGlobalSchema = /*@__PURE__*/ messageDesc(file_agent, 180); -/** - * Describes the message agent.v1.CursorRuleTypeFileGlobs. - * Use `create(CursorRuleTypeFileGlobsSchema)` to create a new message. - */ -export const CursorRuleTypeFileGlobsSchema = -/*@__PURE__*/ -messageDesc(file_agent, 181); -/** - * Describes the message agent.v1.CursorRuleTypeAgentFetched. - * Use `create(CursorRuleTypeAgentFetchedSchema)` to create a new message. - */ -export const CursorRuleTypeAgentFetchedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 182); -/** - * Describes the message agent.v1.CursorRuleTypeManuallyAttached. - * Use `create(CursorRuleTypeManuallyAttachedSchema)` to create a new message. - */ -export const CursorRuleTypeManuallyAttachedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 183); -/** - * Describes the message agent.v1.CursorRuleType. - * Use `create(CursorRuleTypeSchema)` to create a new message. - */ -export const CursorRuleTypeSchema = /*@__PURE__*/ messageDesc(file_agent, 184); -/** - * Describes the message agent.v1.CursorRule. - * Use `create(CursorRuleSchema)` to create a new message. - */ -export const CursorRuleSchema = /*@__PURE__*/ messageDesc(file_agent, 185); -/** - * Describes the message agent.v1.DeleteArgs. - * Use `create(DeleteArgsSchema)` to create a new message. - */ -export const DeleteArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 186); -/** - * Describes the message agent.v1.DeleteResult. - * Use `create(DeleteResultSchema)` to create a new message. - */ -export const DeleteResultSchema = /*@__PURE__*/ messageDesc(file_agent, 187); -/** - * Describes the message agent.v1.DeleteSuccess. - * Use `create(DeleteSuccessSchema)` to create a new message. - */ -export const DeleteSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 188); -/** - * Describes the message agent.v1.DeleteFileNotFound. - * Use `create(DeleteFileNotFoundSchema)` to create a new message. - */ -export const DeleteFileNotFoundSchema = /*@__PURE__*/ messageDesc(file_agent, 189); -/** - * Describes the message agent.v1.DeleteNotFile. - * Use `create(DeleteNotFileSchema)` to create a new message. - */ -export const DeleteNotFileSchema = /*@__PURE__*/ messageDesc(file_agent, 190); -/** - * Describes the message agent.v1.DeletePermissionDenied. - * Use `create(DeletePermissionDeniedSchema)` to create a new message. - */ -export const DeletePermissionDeniedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 191); -/** - * Describes the message agent.v1.DeleteFileBusy. - * Use `create(DeleteFileBusySchema)` to create a new message. - */ -export const DeleteFileBusySchema = /*@__PURE__*/ messageDesc(file_agent, 192); -/** - * Describes the message agent.v1.DeleteRejected. - * Use `create(DeleteRejectedSchema)` to create a new message. - */ -export const DeleteRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 193); -/** - * Describes the message agent.v1.DeleteError. - * Use `create(DeleteErrorSchema)` to create a new message. - */ -export const DeleteErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 194); -/** - * Describes the message agent.v1.DeleteToolCall. - * Use `create(DeleteToolCallSchema)` to create a new message. - */ -export const DeleteToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 195); -/** - * Describes the message agent.v1.DiagnosticsArgs. - * Use `create(DiagnosticsArgsSchema)` to create a new message. - */ -export const DiagnosticsArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 196); -/** - * Describes the message agent.v1.DiagnosticsResult. - * Use `create(DiagnosticsResultSchema)` to create a new message. - */ -export const DiagnosticsResultSchema = /*@__PURE__*/ messageDesc(file_agent, 197); -/** - * Describes the message agent.v1.DiagnosticsSuccess. - * Use `create(DiagnosticsSuccessSchema)` to create a new message. - */ -export const DiagnosticsSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 198); -/** - * Describes the message agent.v1.Diagnostic. - * Use `create(DiagnosticSchema)` to create a new message. - */ -export const DiagnosticSchema = /*@__PURE__*/ messageDesc(file_agent, 199); -/** - * Describes the message agent.v1.DiagnosticsError. - * Use `create(DiagnosticsErrorSchema)` to create a new message. - */ -export const DiagnosticsErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 200); -/** - * Describes the message agent.v1.DiagnosticsRejected. - * Use `create(DiagnosticsRejectedSchema)` to create a new message. - */ -export const DiagnosticsRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 201); -/** - * Describes the message agent.v1.DiagnosticsFileNotFound. - * Use `create(DiagnosticsFileNotFoundSchema)` to create a new message. - */ -export const DiagnosticsFileNotFoundSchema = -/*@__PURE__*/ -messageDesc(file_agent, 202); -/** - * Describes the message agent.v1.DiagnosticsPermissionDenied. - * Use `create(DiagnosticsPermissionDeniedSchema)` to create a new message. - */ -export const DiagnosticsPermissionDeniedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 203); -/** - * Describes the message agent.v1.EditArgs. - * Use `create(EditArgsSchema)` to create a new message. - */ -export const EditArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 204); -/** - * Describes the message agent.v1.EditResult. - * Use `create(EditResultSchema)` to create a new message. - */ -export const EditResultSchema = /*@__PURE__*/ messageDesc(file_agent, 205); -/** - * Describes the message agent.v1.EditSuccess. - * Use `create(EditSuccessSchema)` to create a new message. - */ -export const EditSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 206); -/** - * Describes the message agent.v1.EditFileNotFound. - * Use `create(EditFileNotFoundSchema)` to create a new message. - */ -export const EditFileNotFoundSchema = /*@__PURE__*/ messageDesc(file_agent, 207); -/** - * Describes the message agent.v1.EditReadPermissionDenied. - * Use `create(EditReadPermissionDeniedSchema)` to create a new message. - */ -export const EditReadPermissionDeniedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 208); -/** - * Describes the message agent.v1.EditWritePermissionDenied. - * Use `create(EditWritePermissionDeniedSchema)` to create a new message. - */ -export const EditWritePermissionDeniedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 209); -/** - * Describes the message agent.v1.EditRejected. - * Use `create(EditRejectedSchema)` to create a new message. - */ -export const EditRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 210); -/** - * Describes the message agent.v1.EditError. - * Use `create(EditErrorSchema)` to create a new message. - */ -export const EditErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 211); -/** - * Describes the message agent.v1.EditToolCall. - * Use `create(EditToolCallSchema)` to create a new message. - */ -export const EditToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 212); -/** - * Describes the message agent.v1.EditToolCallDelta. - * Use `create(EditToolCallDeltaSchema)` to create a new message. - */ -export const EditToolCallDeltaSchema = /*@__PURE__*/ messageDesc(file_agent, 213); -/** - * Describes the message agent.v1.ExaFetchArgs. - * Use `create(ExaFetchArgsSchema)` to create a new message. - */ -export const ExaFetchArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 214); -/** - * Describes the message agent.v1.ExaFetchResult. - * Use `create(ExaFetchResultSchema)` to create a new message. - */ -export const ExaFetchResultSchema = /*@__PURE__*/ messageDesc(file_agent, 215); -/** - * Describes the message agent.v1.ExaFetchSuccess. - * Use `create(ExaFetchSuccessSchema)` to create a new message. - */ -export const ExaFetchSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 216); -/** - * Describes the message agent.v1.ExaFetchError. - * Use `create(ExaFetchErrorSchema)` to create a new message. - */ -export const ExaFetchErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 217); -/** - * Describes the message agent.v1.ExaFetchRejected. - * Use `create(ExaFetchRejectedSchema)` to create a new message. - */ -export const ExaFetchRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 218); -/** - * Describes the message agent.v1.ExaFetchContent. - * Use `create(ExaFetchContentSchema)` to create a new message. - */ -export const ExaFetchContentSchema = /*@__PURE__*/ messageDesc(file_agent, 219); -/** - * Describes the message agent.v1.ExaFetchToolCall. - * Use `create(ExaFetchToolCallSchema)` to create a new message. - */ -export const ExaFetchToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 220); -/** - * Describes the message agent.v1.ExaFetchRequestQuery. - * Use `create(ExaFetchRequestQuerySchema)` to create a new message. - */ -export const ExaFetchRequestQuerySchema = /*@__PURE__*/ messageDesc(file_agent, 221); -/** - * Describes the message agent.v1.ExaFetchRequestResponse. - * Use `create(ExaFetchRequestResponseSchema)` to create a new message. - */ -export const ExaFetchRequestResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 222); -/** - * Describes the message agent.v1.ExaFetchRequestResponse_Approved. - * Use `create(ExaFetchRequestResponse_ApprovedSchema)` to create a new message. - */ -export const ExaFetchRequestResponse_ApprovedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 223); -/** - * Describes the message agent.v1.ExaFetchRequestResponse_Rejected. - * Use `create(ExaFetchRequestResponse_RejectedSchema)` to create a new message. - */ -export const ExaFetchRequestResponse_RejectedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 224); -/** - * Describes the message agent.v1.ExaSearchArgs. - * Use `create(ExaSearchArgsSchema)` to create a new message. - */ -export const ExaSearchArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 225); -/** - * Describes the message agent.v1.ExaSearchResult. - * Use `create(ExaSearchResultSchema)` to create a new message. - */ -export const ExaSearchResultSchema = /*@__PURE__*/ messageDesc(file_agent, 226); -/** - * Describes the message agent.v1.ExaSearchSuccess. - * Use `create(ExaSearchSuccessSchema)` to create a new message. - */ -export const ExaSearchSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 227); -/** - * Describes the message agent.v1.ExaSearchError. - * Use `create(ExaSearchErrorSchema)` to create a new message. - */ -export const ExaSearchErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 228); -/** - * Describes the message agent.v1.ExaSearchRejected. - * Use `create(ExaSearchRejectedSchema)` to create a new message. - */ -export const ExaSearchRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 229); -/** - * Describes the message agent.v1.ExaSearchReference. - * Use `create(ExaSearchReferenceSchema)` to create a new message. - */ -export const ExaSearchReferenceSchema = /*@__PURE__*/ messageDesc(file_agent, 230); -/** - * Describes the message agent.v1.ExaSearchToolCall. - * Use `create(ExaSearchToolCallSchema)` to create a new message. - */ -export const ExaSearchToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 231); -/** - * Describes the message agent.v1.ExaSearchRequestQuery. - * Use `create(ExaSearchRequestQuerySchema)` to create a new message. - */ -export const ExaSearchRequestQuerySchema = -/*@__PURE__*/ -messageDesc(file_agent, 232); -/** - * Describes the message agent.v1.ExaSearchRequestResponse. - * Use `create(ExaSearchRequestResponseSchema)` to create a new message. - */ -export const ExaSearchRequestResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 233); -/** - * Describes the message agent.v1.ExaSearchRequestResponse_Approved. - * Use `create(ExaSearchRequestResponse_ApprovedSchema)` to create a new message. - */ -export const ExaSearchRequestResponse_ApprovedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 234); -/** - * Describes the message agent.v1.ExaSearchRequestResponse_Rejected. - * Use `create(ExaSearchRequestResponse_RejectedSchema)` to create a new message. - */ -export const ExaSearchRequestResponse_RejectedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 235); -/** - * Describes the message agent.v1.ExecClientStreamClose. - * Use `create(ExecClientStreamCloseSchema)` to create a new message. - */ -export const ExecClientStreamCloseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 236); -/** - * Describes the message agent.v1.ExecClientThrow. - * Use `create(ExecClientThrowSchema)` to create a new message. - */ -export const ExecClientThrowSchema = /*@__PURE__*/ messageDesc(file_agent, 237); -/** - * Describes the message agent.v1.ExecClientHeartbeat. - * Use `create(ExecClientHeartbeatSchema)` to create a new message. - */ -export const ExecClientHeartbeatSchema = /*@__PURE__*/ messageDesc(file_agent, 238); -/** - * Describes the message agent.v1.ExecClientControlMessage. - * Use `create(ExecClientControlMessageSchema)` to create a new message. - */ -export const ExecClientControlMessageSchema = -/*@__PURE__*/ -messageDesc(file_agent, 239); -/** - * Describes the message agent.v1.SpanContext. - * Use `create(SpanContextSchema)` to create a new message. - */ -export const SpanContextSchema = /*@__PURE__*/ messageDesc(file_agent, 240); -/** - * Describes the message agent.v1.AbortArgs. - * Use `create(AbortArgsSchema)` to create a new message. - */ -export const AbortArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 241); -/** - * Describes the message agent.v1.AbortResult. - * Use `create(AbortResultSchema)` to create a new message. - */ -export const AbortResultSchema = /*@__PURE__*/ messageDesc(file_agent, 242); -/** - * Describes the message agent.v1.ExecServerMessage. - * Use `create(ExecServerMessageSchema)` to create a new message. - */ -export const ExecServerMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 243); -/** - * Describes the message agent.v1.ExecClientMessage. - * Use `create(ExecClientMessageSchema)` to create a new message. - */ -export const ExecClientMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 244); -/** - * Describes the message agent.v1.FetchArgs. - * Use `create(FetchArgsSchema)` to create a new message. - */ -export const FetchArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 245); -/** - * Describes the message agent.v1.FetchResult. - * Use `create(FetchResultSchema)` to create a new message. - */ -export const FetchResultSchema = /*@__PURE__*/ messageDesc(file_agent, 246); -/** - * Describes the message agent.v1.FetchSuccess. - * Use `create(FetchSuccessSchema)` to create a new message. - */ -export const FetchSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 247); -/** - * Describes the message agent.v1.FetchError. - * Use `create(FetchErrorSchema)` to create a new message. - */ -export const FetchErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 248); -/** - * Describes the message agent.v1.GenerateImageArgs. - * Use `create(GenerateImageArgsSchema)` to create a new message. - */ -export const GenerateImageArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 249); -/** - * Describes the message agent.v1.GenerateImageResult. - * Use `create(GenerateImageResultSchema)` to create a new message. - */ -export const GenerateImageResultSchema = /*@__PURE__*/ messageDesc(file_agent, 250); -/** - * Describes the message agent.v1.GenerateImageSuccess. - * Use `create(GenerateImageSuccessSchema)` to create a new message. - */ -export const GenerateImageSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 251); -/** - * Describes the message agent.v1.GenerateImageError. - * Use `create(GenerateImageErrorSchema)` to create a new message. - */ -export const GenerateImageErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 252); -/** - * Describes the message agent.v1.GenerateImageToolCall. - * Use `create(GenerateImageToolCallSchema)` to create a new message. - */ -export const GenerateImageToolCallSchema = -/*@__PURE__*/ -messageDesc(file_agent, 253); -/** - * Describes the message agent.v1.GrepArgs. - * Use `create(GrepArgsSchema)` to create a new message. - */ -export const GrepArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 254); -/** - * Describes the message agent.v1.GrepResult. - * Use `create(GrepResultSchema)` to create a new message. - */ -export const GrepResultSchema = /*@__PURE__*/ messageDesc(file_agent, 255); -/** - * Describes the message agent.v1.GrepError. - * Use `create(GrepErrorSchema)` to create a new message. - */ -export const GrepErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 256); -/** - * Describes the message agent.v1.GrepSuccess. - * Use `create(GrepSuccessSchema)` to create a new message. - */ -export const GrepSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 257); -/** - * Describes the message agent.v1.GrepUnionResult. - * Use `create(GrepUnionResultSchema)` to create a new message. - */ -export const GrepUnionResultSchema = /*@__PURE__*/ messageDesc(file_agent, 258); -/** - * Describes the message agent.v1.GrepCountResult. - * Use `create(GrepCountResultSchema)` to create a new message. - */ -export const GrepCountResultSchema = /*@__PURE__*/ messageDesc(file_agent, 259); -/** - * Describes the message agent.v1.GrepFileCount. - * Use `create(GrepFileCountSchema)` to create a new message. - */ -export const GrepFileCountSchema = /*@__PURE__*/ messageDesc(file_agent, 260); -/** - * Describes the message agent.v1.GrepFilesResult. - * Use `create(GrepFilesResultSchema)` to create a new message. - */ -export const GrepFilesResultSchema = /*@__PURE__*/ messageDesc(file_agent, 261); -/** - * Describes the message agent.v1.GrepContentResult. - * Use `create(GrepContentResultSchema)` to create a new message. - */ -export const GrepContentResultSchema = /*@__PURE__*/ messageDesc(file_agent, 262); -/** - * Describes the message agent.v1.GrepFileMatch. - * Use `create(GrepFileMatchSchema)` to create a new message. - */ -export const GrepFileMatchSchema = /*@__PURE__*/ messageDesc(file_agent, 263); -/** - * Describes the message agent.v1.GrepContentMatch. - * Use `create(GrepContentMatchSchema)` to create a new message. - */ -export const GrepContentMatchSchema = /*@__PURE__*/ messageDesc(file_agent, 264); -/** - * Describes the message agent.v1.GrepStream. - * Use `create(GrepStreamSchema)` to create a new message. - */ -export const GrepStreamSchema = /*@__PURE__*/ messageDesc(file_agent, 265); -/** - * Describes the message agent.v1.GrepToolCall. - * Use `create(GrepToolCallSchema)` to create a new message. - */ -export const GrepToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 266); -/** - * Describes the message agent.v1.GetBlobArgs. - * Use `create(GetBlobArgsSchema)` to create a new message. - */ -export const GetBlobArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 267); -/** - * Describes the message agent.v1.GetBlobResult. - * Use `create(GetBlobResultSchema)` to create a new message. - */ -export const GetBlobResultSchema = /*@__PURE__*/ messageDesc(file_agent, 268); -/** - * Describes the message agent.v1.SetBlobArgs. - * Use `create(SetBlobArgsSchema)` to create a new message. - */ -export const SetBlobArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 269); -/** - * Describes the message agent.v1.SetBlobResult. - * Use `create(SetBlobResultSchema)` to create a new message. - */ -export const SetBlobResultSchema = /*@__PURE__*/ messageDesc(file_agent, 270); -/** - * Describes the message agent.v1.KvServerMessage. - * Use `create(KvServerMessageSchema)` to create a new message. - */ -export const KvServerMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 271); -/** - * Describes the message agent.v1.KvClientMessage. - * Use `create(KvClientMessageSchema)` to create a new message. - */ -export const KvClientMessageSchema = /*@__PURE__*/ messageDesc(file_agent, 272); -/** - * Describes the message agent.v1.LsArgs. - * Use `create(LsArgsSchema)` to create a new message. - */ -export const LsArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 273); -/** - * Describes the message agent.v1.LsResult. - * Use `create(LsResultSchema)` to create a new message. - */ -export const LsResultSchema = /*@__PURE__*/ messageDesc(file_agent, 274); -/** - * Describes the message agent.v1.LsSuccess. - * Use `create(LsSuccessSchema)` to create a new message. - */ -export const LsSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 275); -/** - * Describes the message agent.v1.LsDirectoryTreeNode. - * Use `create(LsDirectoryTreeNodeSchema)` to create a new message. - */ -export const LsDirectoryTreeNodeSchema = /*@__PURE__*/ messageDesc(file_agent, 276); -/** - * Describes the message agent.v1.LsDirectoryTreeNode_File. - * Use `create(LsDirectoryTreeNode_FileSchema)` to create a new message. - */ -export const LsDirectoryTreeNode_FileSchema = -/*@__PURE__*/ -messageDesc(file_agent, 277); -/** - * Describes the message agent.v1.LsError. - * Use `create(LsErrorSchema)` to create a new message. - */ -export const LsErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 278); -/** - * Describes the message agent.v1.LsRejected. - * Use `create(LsRejectedSchema)` to create a new message. - */ -export const LsRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 279); -/** - * Describes the message agent.v1.LsTimeout. - * Use `create(LsTimeoutSchema)` to create a new message. - */ -export const LsTimeoutSchema = /*@__PURE__*/ messageDesc(file_agent, 280); -/** - * Describes the message agent.v1.TerminalMetadata. - * Use `create(TerminalMetadataSchema)` to create a new message. - */ -export const TerminalMetadataSchema = /*@__PURE__*/ messageDesc(file_agent, 281); -/** - * Describes the message agent.v1.TerminalMetadata_Command. - * Use `create(TerminalMetadata_CommandSchema)` to create a new message. - */ -export const TerminalMetadata_CommandSchema = -/*@__PURE__*/ -messageDesc(file_agent, 282); -/** - * Describes the message agent.v1.LsToolCall. - * Use `create(LsToolCallSchema)` to create a new message. - */ -export const LsToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 283); -/** - * Describes the message agent.v1.McpArgs. - * Use `create(McpArgsSchema)` to create a new message. - */ -export const McpArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 284); -/** - * Describes the message agent.v1.McpResult. - * Use `create(McpResultSchema)` to create a new message. - */ -export const McpResultSchema = /*@__PURE__*/ messageDesc(file_agent, 285); -/** - * Describes the message agent.v1.McpToolNotFound. - * Use `create(McpToolNotFoundSchema)` to create a new message. - */ -export const McpToolNotFoundSchema = /*@__PURE__*/ messageDesc(file_agent, 286); -/** - * Describes the message agent.v1.McpTextContent. - * Use `create(McpTextContentSchema)` to create a new message. - */ -export const McpTextContentSchema = /*@__PURE__*/ messageDesc(file_agent, 287); -/** - * Describes the message agent.v1.McpImageContent. - * Use `create(McpImageContentSchema)` to create a new message. - */ -export const McpImageContentSchema = /*@__PURE__*/ messageDesc(file_agent, 288); -/** - * Describes the message agent.v1.McpToolResultContentItem. - * Use `create(McpToolResultContentItemSchema)` to create a new message. - */ -export const McpToolResultContentItemSchema = -/*@__PURE__*/ -messageDesc(file_agent, 289); -/** - * Describes the message agent.v1.McpSuccess. - * Use `create(McpSuccessSchema)` to create a new message. - */ -export const McpSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 290); -/** - * Describes the message agent.v1.McpError. - * Use `create(McpErrorSchema)` to create a new message. - */ -export const McpErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 291); -/** - * Describes the message agent.v1.McpRejected. - * Use `create(McpRejectedSchema)` to create a new message. - */ -export const McpRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 292); -/** - * Describes the message agent.v1.McpPermissionDenied. - * Use `create(McpPermissionDeniedSchema)` to create a new message. - */ -export const McpPermissionDeniedSchema = /*@__PURE__*/ messageDesc(file_agent, 293); -/** - * Describes the message agent.v1.ListMcpResourcesExecArgs. - * Use `create(ListMcpResourcesExecArgsSchema)` to create a new message. - */ -export const ListMcpResourcesExecArgsSchema = -/*@__PURE__*/ -messageDesc(file_agent, 294); -/** - * Describes the message agent.v1.ListMcpResourcesExecResult. - * Use `create(ListMcpResourcesExecResultSchema)` to create a new message. - */ -export const ListMcpResourcesExecResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 295); -/** - * Describes the message agent.v1.ListMcpResourcesExecResult_McpResource. - * Use `create(ListMcpResourcesExecResult_McpResourceSchema)` to create a new message. - */ -export const ListMcpResourcesExecResult_McpResourceSchema = -/*@__PURE__*/ -messageDesc(file_agent, 296); -/** - * Describes the message agent.v1.ListMcpResourcesSuccess. - * Use `create(ListMcpResourcesSuccessSchema)` to create a new message. - */ -export const ListMcpResourcesSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 297); -/** - * Describes the message agent.v1.ListMcpResourcesError. - * Use `create(ListMcpResourcesErrorSchema)` to create a new message. - */ -export const ListMcpResourcesErrorSchema = -/*@__PURE__*/ -messageDesc(file_agent, 298); -/** - * Describes the message agent.v1.ListMcpResourcesRejected. - * Use `create(ListMcpResourcesRejectedSchema)` to create a new message. - */ -export const ListMcpResourcesRejectedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 299); -/** - * Describes the message agent.v1.ReadMcpResourceExecArgs. - * Use `create(ReadMcpResourceExecArgsSchema)` to create a new message. - */ -export const ReadMcpResourceExecArgsSchema = -/*@__PURE__*/ -messageDesc(file_agent, 300); -/** - * Describes the message agent.v1.ReadMcpResourceExecResult. - * Use `create(ReadMcpResourceExecResultSchema)` to create a new message. - */ -export const ReadMcpResourceExecResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 301); -/** - * Describes the message agent.v1.ReadMcpResourceSuccess. - * Use `create(ReadMcpResourceSuccessSchema)` to create a new message. - */ -export const ReadMcpResourceSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 302); -/** - * Describes the message agent.v1.ReadMcpResourceError. - * Use `create(ReadMcpResourceErrorSchema)` to create a new message. - */ -export const ReadMcpResourceErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 303); -/** - * Describes the message agent.v1.ReadMcpResourceRejected. - * Use `create(ReadMcpResourceRejectedSchema)` to create a new message. - */ -export const ReadMcpResourceRejectedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 304); -/** - * Describes the message agent.v1.ReadMcpResourceNotFound. - * Use `create(ReadMcpResourceNotFoundSchema)` to create a new message. - */ -export const ReadMcpResourceNotFoundSchema = -/*@__PURE__*/ -messageDesc(file_agent, 305); -/** - * Describes the message agent.v1.McpToolDefinition. - * Use `create(McpToolDefinitionSchema)` to create a new message. - */ -export const McpToolDefinitionSchema = /*@__PURE__*/ messageDesc(file_agent, 306); -/** - * Describes the message agent.v1.McpTools. - * Use `create(McpToolsSchema)` to create a new message. - */ -export const McpToolsSchema = /*@__PURE__*/ messageDesc(file_agent, 307); -/** - * Describes the message agent.v1.McpInstructions. - * Use `create(McpInstructionsSchema)` to create a new message. - */ -export const McpInstructionsSchema = /*@__PURE__*/ messageDesc(file_agent, 308); -/** - * Describes the message agent.v1.McpDescriptor. - * Use `create(McpDescriptorSchema)` to create a new message. - */ -export const McpDescriptorSchema = /*@__PURE__*/ messageDesc(file_agent, 309); -/** - * Describes the message agent.v1.McpToolDescriptor. - * Use `create(McpToolDescriptorSchema)` to create a new message. - */ -export const McpToolDescriptorSchema = /*@__PURE__*/ messageDesc(file_agent, 310); -/** - * Describes the message agent.v1.McpFileSystemOptions. - * Use `create(McpFileSystemOptionsSchema)` to create a new message. - */ -export const McpFileSystemOptionsSchema = /*@__PURE__*/ messageDesc(file_agent, 311); -/** - * Describes the message agent.v1.ReadArgs. - * Use `create(ReadArgsSchema)` to create a new message. - */ -export const ReadArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 312); -/** - * Describes the message agent.v1.ReadResult. - * Use `create(ReadResultSchema)` to create a new message. - */ -export const ReadResultSchema = /*@__PURE__*/ messageDesc(file_agent, 313); -/** - * Describes the message agent.v1.ReadSuccess. - * Use `create(ReadSuccessSchema)` to create a new message. - */ -export const ReadSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 314); -/** - * Describes the message agent.v1.ReadError. - * Use `create(ReadErrorSchema)` to create a new message. - */ -export const ReadErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 315); -/** - * Describes the message agent.v1.ReadRejected. - * Use `create(ReadRejectedSchema)` to create a new message. - */ -export const ReadRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 316); -/** - * Describes the message agent.v1.ReadFileNotFound. - * Use `create(ReadFileNotFoundSchema)` to create a new message. - */ -export const ReadFileNotFoundSchema = /*@__PURE__*/ messageDesc(file_agent, 317); -/** - * Describes the message agent.v1.ReadPermissionDenied. - * Use `create(ReadPermissionDeniedSchema)` to create a new message. - */ -export const ReadPermissionDeniedSchema = /*@__PURE__*/ messageDesc(file_agent, 318); -/** - * Describes the message agent.v1.ReadInvalidFile. - * Use `create(ReadInvalidFileSchema)` to create a new message. - */ -export const ReadInvalidFileSchema = /*@__PURE__*/ messageDesc(file_agent, 319); -/** - * Describes the message agent.v1.ReadToolCall. - * Use `create(ReadToolCallSchema)` to create a new message. - */ -export const ReadToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 320); -/** - * Describes the message agent.v1.ReadToolArgs. - * Use `create(ReadToolArgsSchema)` to create a new message. - */ -export const ReadToolArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 321); -/** - * Describes the message agent.v1.ReadToolResult. - * Use `create(ReadToolResultSchema)` to create a new message. - */ -export const ReadToolResultSchema = /*@__PURE__*/ messageDesc(file_agent, 322); -/** - * Describes the message agent.v1.ReadRange. - * Use `create(ReadRangeSchema)` to create a new message. - */ -export const ReadRangeSchema = /*@__PURE__*/ messageDesc(file_agent, 323); -/** - * Describes the message agent.v1.ReadToolSuccess. - * Use `create(ReadToolSuccessSchema)` to create a new message. - */ -export const ReadToolSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 324); -/** - * Describes the message agent.v1.ReadToolError. - * Use `create(ReadToolErrorSchema)` to create a new message. - */ -export const ReadToolErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 325); -/** - * Describes the message agent.v1.RecordScreenArgs. - * Use `create(RecordScreenArgsSchema)` to create a new message. - */ -export const RecordScreenArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 326); -/** - * Describes the message agent.v1.RecordScreenResult. - * Use `create(RecordScreenResultSchema)` to create a new message. - */ -export const RecordScreenResultSchema = /*@__PURE__*/ messageDesc(file_agent, 327); -/** - * Describes the message agent.v1.RecordScreenStartSuccess. - * Use `create(RecordScreenStartSuccessSchema)` to create a new message. - */ -export const RecordScreenStartSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 328); -/** - * Describes the message agent.v1.RecordScreenSaveSuccess. - * Use `create(RecordScreenSaveSuccessSchema)` to create a new message. - */ -export const RecordScreenSaveSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 329); -/** - * Describes the message agent.v1.RecordScreenDiscardSuccess. - * Use `create(RecordScreenDiscardSuccessSchema)` to create a new message. - */ -export const RecordScreenDiscardSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 330); -/** - * Describes the message agent.v1.RecordScreenFailure. - * Use `create(RecordScreenFailureSchema)` to create a new message. - */ -export const RecordScreenFailureSchema = /*@__PURE__*/ messageDesc(file_agent, 331); -/** - * Describes the message agent.v1.CursorPackagePrompt. - * Use `create(CursorPackagePromptSchema)` to create a new message. - */ -export const CursorPackagePromptSchema = /*@__PURE__*/ messageDesc(file_agent, 332); -/** - * Describes the message agent.v1.CursorPackage. - * Use `create(CursorPackageSchema)` to create a new message. - */ -export const CursorPackageSchema = /*@__PURE__*/ messageDesc(file_agent, 333); -/** - * Describes the message agent.v1.RepositoryIndexingInfo. - * Use `create(RepositoryIndexingInfoSchema)` to create a new message. - */ -export const RepositoryIndexingInfoSchema = -/*@__PURE__*/ -messageDesc(file_agent, 334); -/** - * Describes the message agent.v1.RequestContextArgs. - * Use `create(RequestContextArgsSchema)` to create a new message. - */ -export const RequestContextArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 335); -/** - * Describes the message agent.v1.RequestContextResult. - * Use `create(RequestContextResultSchema)` to create a new message. - */ -export const RequestContextResultSchema = /*@__PURE__*/ messageDesc(file_agent, 336); -/** - * Describes the message agent.v1.RequestContextSuccess. - * Use `create(RequestContextSuccessSchema)` to create a new message. - */ -export const RequestContextSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 337); -/** - * Describes the message agent.v1.RequestContextError. - * Use `create(RequestContextErrorSchema)` to create a new message. - */ -export const RequestContextErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 338); -/** - * Describes the message agent.v1.RequestContextRejected. - * Use `create(RequestContextRejectedSchema)` to create a new message. - */ -export const RequestContextRejectedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 339); -/** - * Describes the message agent.v1.ImageProto. - * Use `create(ImageProtoSchema)` to create a new message. - */ -export const ImageProtoSchema = /*@__PURE__*/ messageDesc(file_agent, 340); -/** - * Describes the message agent.v1.ImageProto_Dimension. - * Use `create(ImageProto_DimensionSchema)` to create a new message. - */ -export const ImageProto_DimensionSchema = /*@__PURE__*/ messageDesc(file_agent, 341); -/** - * Describes the message agent.v1.GitRepoInfo. - * Use `create(GitRepoInfoSchema)` to create a new message. - */ -export const GitRepoInfoSchema = /*@__PURE__*/ messageDesc(file_agent, 342); -/** - * Describes the message agent.v1.RequestContextEnv. - * Use `create(RequestContextEnvSchema)` to create a new message. - */ -export const RequestContextEnvSchema = /*@__PURE__*/ messageDesc(file_agent, 343); -/** - * Describes the message agent.v1.DebugModeConfig. - * Use `create(DebugModeConfigSchema)` to create a new message. - */ -export const DebugModeConfigSchema = /*@__PURE__*/ messageDesc(file_agent, 344); -/** - * Describes the message agent.v1.SkillDescriptor. - * Use `create(SkillDescriptorSchema)` to create a new message. - */ -export const SkillDescriptorSchema = /*@__PURE__*/ messageDesc(file_agent, 345); -/** - * Describes the message agent.v1.SkillOptions. - * Use `create(SkillOptionsSchema)` to create a new message. - */ -export const SkillOptionsSchema = /*@__PURE__*/ messageDesc(file_agent, 346); -/** - * Describes the message agent.v1.RequestContext. - * Use `create(RequestContextSchema)` to create a new message. - */ -export const RequestContextSchema = /*@__PURE__*/ messageDesc(file_agent, 347); -/** - * Describes the message agent.v1.SandboxPolicy. - * Use `create(SandboxPolicySchema)` to create a new message. - */ -export const SandboxPolicySchema = /*@__PURE__*/ messageDesc(file_agent, 348); -/** - * Describes the message agent.v1.SelectedImage. - * Use `create(SelectedImageSchema)` to create a new message. - */ -export const SelectedImageSchema = /*@__PURE__*/ messageDesc(file_agent, 349); -/** - * Describes the message agent.v1.SelectedImage_BlobIdWithData. - * Use `create(SelectedImage_BlobIdWithDataSchema)` to create a new message. - */ -export const SelectedImage_BlobIdWithDataSchema = -/*@__PURE__*/ -messageDesc(file_agent, 350); -/** - * Describes the message agent.v1.SelectedImage_Dimension. - * Use `create(SelectedImage_DimensionSchema)` to create a new message. - */ -export const SelectedImage_DimensionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 351); -/** - * Describes the message agent.v1.ExtraContextEntry. - * Use `create(ExtraContextEntrySchema)` to create a new message. - */ -export const ExtraContextEntrySchema = /*@__PURE__*/ messageDesc(file_agent, 352); -/** - * Describes the message agent.v1.SelectedFile. - * Use `create(SelectedFileSchema)` to create a new message. - */ -export const SelectedFileSchema = /*@__PURE__*/ messageDesc(file_agent, 353); -/** - * Describes the message agent.v1.SelectedCodeSelection. - * Use `create(SelectedCodeSelectionSchema)` to create a new message. - */ -export const SelectedCodeSelectionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 354); -/** - * Describes the message agent.v1.SelectedTerminal. - * Use `create(SelectedTerminalSchema)` to create a new message. - */ -export const SelectedTerminalSchema = /*@__PURE__*/ messageDesc(file_agent, 355); -/** - * Describes the message agent.v1.SelectedTerminalSelection. - * Use `create(SelectedTerminalSelectionSchema)` to create a new message. - */ -export const SelectedTerminalSelectionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 356); -/** - * Describes the message agent.v1.SelectedFolder. - * Use `create(SelectedFolderSchema)` to create a new message. - */ -export const SelectedFolderSchema = /*@__PURE__*/ messageDesc(file_agent, 357); -/** - * Describes the message agent.v1.SelectedExternalLink. - * Use `create(SelectedExternalLinkSchema)` to create a new message. - */ -export const SelectedExternalLinkSchema = /*@__PURE__*/ messageDesc(file_agent, 358); -/** - * Describes the message agent.v1.SelectedCursorRule. - * Use `create(SelectedCursorRuleSchema)` to create a new message. - */ -export const SelectedCursorRuleSchema = /*@__PURE__*/ messageDesc(file_agent, 359); -/** - * Describes the message agent.v1.SelectedGitDiff. - * Use `create(SelectedGitDiffSchema)` to create a new message. - */ -export const SelectedGitDiffSchema = /*@__PURE__*/ messageDesc(file_agent, 360); -/** - * Describes the message agent.v1.SelectedGitDiffFromBranchToMain. - * Use `create(SelectedGitDiffFromBranchToMainSchema)` to create a new message. - */ -export const SelectedGitDiffFromBranchToMainSchema = -/*@__PURE__*/ -messageDesc(file_agent, 361); -/** - * Describes the message agent.v1.SelectedGitCommit. - * Use `create(SelectedGitCommitSchema)` to create a new message. - */ -export const SelectedGitCommitSchema = /*@__PURE__*/ messageDesc(file_agent, 362); -/** - * Describes the message agent.v1.SelectedPullRequest. - * Use `create(SelectedPullRequestSchema)` to create a new message. - */ -export const SelectedPullRequestSchema = /*@__PURE__*/ messageDesc(file_agent, 363); -/** - * Describes the message agent.v1.SelectedGitPRDiffSelection. - * Use `create(SelectedGitPRDiffSelectionSchema)` to create a new message. - */ -export const SelectedGitPRDiffSelectionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 364); -/** - * Describes the message agent.v1.SelectedCursorCommand. - * Use `create(SelectedCursorCommandSchema)` to create a new message. - */ -export const SelectedCursorCommandSchema = -/*@__PURE__*/ -messageDesc(file_agent, 365); -/** - * Describes the message agent.v1.SelectedDocumentation. - * Use `create(SelectedDocumentationSchema)` to create a new message. - */ -export const SelectedDocumentationSchema = -/*@__PURE__*/ -messageDesc(file_agent, 366); -/** - * Describes the message agent.v1.SelectedPastChat. - * Use `create(SelectedPastChatSchema)` to create a new message. - */ -export const SelectedPastChatSchema = /*@__PURE__*/ messageDesc(file_agent, 367); -/** - * Describes the message agent.v1.CallFrame. - * Use `create(CallFrameSchema)` to create a new message. - */ -export const CallFrameSchema = /*@__PURE__*/ messageDesc(file_agent, 368); -/** - * Describes the message agent.v1.StackTrace. - * Use `create(StackTraceSchema)` to create a new message. - */ -export const StackTraceSchema = /*@__PURE__*/ messageDesc(file_agent, 369); -/** - * Describes the message agent.v1.SelectedConsoleLog. - * Use `create(SelectedConsoleLogSchema)` to create a new message. - */ -export const SelectedConsoleLogSchema = /*@__PURE__*/ messageDesc(file_agent, 370); -/** - * Describes the message agent.v1.SelectedUIElement. - * Use `create(SelectedUIElementSchema)` to create a new message. - */ -export const SelectedUIElementSchema = /*@__PURE__*/ messageDesc(file_agent, 371); -/** - * Describes the message agent.v1.SelectedSubagent. - * Use `create(SelectedSubagentSchema)` to create a new message. - */ -export const SelectedSubagentSchema = /*@__PURE__*/ messageDesc(file_agent, 372); -/** - * Describes the message agent.v1.SelectedContext. - * Use `create(SelectedContextSchema)` to create a new message. - */ -export const SelectedContextSchema = /*@__PURE__*/ messageDesc(file_agent, 373); -/** - * Describes the message agent.v1.InvocationContext. - * Use `create(InvocationContextSchema)` to create a new message. - */ -export const InvocationContextSchema = /*@__PURE__*/ messageDesc(file_agent, 374); -/** - * Describes the message agent.v1.InvocationContext_SlackThread. - * Use `create(InvocationContext_SlackThreadSchema)` to create a new message. - */ -export const InvocationContext_SlackThreadSchema = -/*@__PURE__*/ -messageDesc(file_agent, 375); -/** - * Describes the message agent.v1.InvocationContext_GithubPR. - * Use `create(InvocationContext_GithubPRSchema)` to create a new message. - */ -export const InvocationContext_GithubPRSchema = -/*@__PURE__*/ -messageDesc(file_agent, 376); -/** - * Describes the message agent.v1.InvocationContext_IdeState. - * Use `create(InvocationContext_IdeStateSchema)` to create a new message. - */ -export const InvocationContext_IdeStateSchema = -/*@__PURE__*/ -messageDesc(file_agent, 377); -/** - * Describes the message agent.v1.InvocationContext_IdeState_File. - * Use `create(InvocationContext_IdeState_FileSchema)` to create a new message. - */ -export const InvocationContext_IdeState_FileSchema = -/*@__PURE__*/ -messageDesc(file_agent, 378); -/** - * Describes the message agent.v1.InvocationContext_IdeState_File_CursorPosition. - * Use `create(InvocationContext_IdeState_File_CursorPositionSchema)` to create a new message. - */ -export const InvocationContext_IdeState_File_CursorPositionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 379); -/** - * Describes the message agent.v1.InvocationContext_IdeState_ViewedPullRequest. - * Use `create(InvocationContext_IdeState_ViewedPullRequestSchema)` to create a new message. - */ -export const InvocationContext_IdeState_ViewedPullRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 380); -/** - * Describes the message agent.v1.SetupVmEnvironmentArgs. - * Use `create(SetupVmEnvironmentArgsSchema)` to create a new message. - */ -export const SetupVmEnvironmentArgsSchema = -/*@__PURE__*/ -messageDesc(file_agent, 381); -/** - * Describes the message agent.v1.SetupVmEnvironmentResult. - * Use `create(SetupVmEnvironmentResultSchema)` to create a new message. - */ -export const SetupVmEnvironmentResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 382); -/** - * Describes the message agent.v1.SetupVmEnvironmentSuccess. - * Use `create(SetupVmEnvironmentSuccessSchema)` to create a new message. - */ -export const SetupVmEnvironmentSuccessSchema = -/*@__PURE__*/ -messageDesc(file_agent, 383); -/** - * Describes the message agent.v1.SetupVmEnvironmentToolCall. - * Use `create(SetupVmEnvironmentToolCallSchema)` to create a new message. - */ -export const SetupVmEnvironmentToolCallSchema = -/*@__PURE__*/ -messageDesc(file_agent, 384); -/** - * Describes the message agent.v1.ShellCommandParsingResult. - * Use `create(ShellCommandParsingResultSchema)` to create a new message. - */ -export const ShellCommandParsingResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 385); -/** - * Describes the message agent.v1.ShellCommandParsingResult_ExecutableCommandArg. - * Use `create(ShellCommandParsingResult_ExecutableCommandArgSchema)` to create a new message. - */ -export const ShellCommandParsingResult_ExecutableCommandArgSchema = -/*@__PURE__*/ -messageDesc(file_agent, 386); -/** - * Describes the message agent.v1.ShellCommandParsingResult_ExecutableCommand. - * Use `create(ShellCommandParsingResult_ExecutableCommandSchema)` to create a new message. - */ -export const ShellCommandParsingResult_ExecutableCommandSchema = -/*@__PURE__*/ -messageDesc(file_agent, 387); -/** - * Describes the message agent.v1.ShellArgs. - * Use `create(ShellArgsSchema)` to create a new message. - */ -export const ShellArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 388); -/** - * Describes the message agent.v1.ShellResult. - * Use `create(ShellResultSchema)` to create a new message. - */ -export const ShellResultSchema = /*@__PURE__*/ messageDesc(file_agent, 389); -/** - * Describes the message agent.v1.ShellStreamStdout. - * Use `create(ShellStreamStdoutSchema)` to create a new message. - */ -export const ShellStreamStdoutSchema = /*@__PURE__*/ messageDesc(file_agent, 390); -/** - * Describes the message agent.v1.ShellStreamStderr. - * Use `create(ShellStreamStderrSchema)` to create a new message. - */ -export const ShellStreamStderrSchema = /*@__PURE__*/ messageDesc(file_agent, 391); -/** - * Describes the message agent.v1.ShellStreamExit. - * Use `create(ShellStreamExitSchema)` to create a new message. - */ -export const ShellStreamExitSchema = /*@__PURE__*/ messageDesc(file_agent, 392); -/** - * Describes the message agent.v1.ShellStreamStart. - * Use `create(ShellStreamStartSchema)` to create a new message. - */ -export const ShellStreamStartSchema = /*@__PURE__*/ messageDesc(file_agent, 393); -/** - * Describes the message agent.v1.ShellStreamBackgrounded. - * Use `create(ShellStreamBackgroundedSchema)` to create a new message. - */ -export const ShellStreamBackgroundedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 394); -/** - * Describes the message agent.v1.ShellStream. - * Use `create(ShellStreamSchema)` to create a new message. - */ -export const ShellStreamSchema = /*@__PURE__*/ messageDesc(file_agent, 395); -/** - * Describes the message agent.v1.OutputLocation. - * Use `create(OutputLocationSchema)` to create a new message. - */ -export const OutputLocationSchema = /*@__PURE__*/ messageDesc(file_agent, 396); -/** - * Describes the message agent.v1.ShellSuccess. - * Use `create(ShellSuccessSchema)` to create a new message. - */ -export const ShellSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 397); -/** - * Describes the message agent.v1.ShellFailure. - * Use `create(ShellFailureSchema)` to create a new message. - */ -export const ShellFailureSchema = /*@__PURE__*/ messageDesc(file_agent, 398); -/** - * Describes the message agent.v1.ShellTimeout. - * Use `create(ShellTimeoutSchema)` to create a new message. - */ -export const ShellTimeoutSchema = /*@__PURE__*/ messageDesc(file_agent, 399); -/** - * Describes the message agent.v1.ShellRejected. - * Use `create(ShellRejectedSchema)` to create a new message. - */ -export const ShellRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 400); -/** - * Describes the message agent.v1.ShellPermissionDenied. - * Use `create(ShellPermissionDeniedSchema)` to create a new message. - */ -export const ShellPermissionDeniedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 401); -/** - * Describes the message agent.v1.ShellSpawnError. - * Use `create(ShellSpawnErrorSchema)` to create a new message. - */ -export const ShellSpawnErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 402); -/** - * Describes the message agent.v1.ShellPartialResult. - * Use `create(ShellPartialResultSchema)` to create a new message. - */ -export const ShellPartialResultSchema = /*@__PURE__*/ messageDesc(file_agent, 403); -/** - * Describes the message agent.v1.ShellToolCall. - * Use `create(ShellToolCallSchema)` to create a new message. - */ -export const ShellToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 404); -/** - * Describes the message agent.v1.ShellToolCallStdoutDelta. - * Use `create(ShellToolCallStdoutDeltaSchema)` to create a new message. - */ -export const ShellToolCallStdoutDeltaSchema = -/*@__PURE__*/ -messageDesc(file_agent, 405); -/** - * Describes the message agent.v1.ShellToolCallStderrDelta. - * Use `create(ShellToolCallStderrDeltaSchema)` to create a new message. - */ -export const ShellToolCallStderrDeltaSchema = -/*@__PURE__*/ -messageDesc(file_agent, 406); -/** - * Describes the message agent.v1.ShellToolCallDelta. - * Use `create(ShellToolCallDeltaSchema)` to create a new message. - */ -export const ShellToolCallDeltaSchema = /*@__PURE__*/ messageDesc(file_agent, 407); -/** - * Describes the message agent.v1.SubagentType. - * Use `create(SubagentTypeSchema)` to create a new message. - */ -export const SubagentTypeSchema = /*@__PURE__*/ messageDesc(file_agent, 408); -/** - * Describes the message agent.v1.SubagentTypeUnspecified. - * Use `create(SubagentTypeUnspecifiedSchema)` to create a new message. - */ -export const SubagentTypeUnspecifiedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 409); -/** - * Describes the message agent.v1.SubagentTypeComputerUse. - * Use `create(SubagentTypeComputerUseSchema)` to create a new message. - */ -export const SubagentTypeComputerUseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 410); -/** - * Describes the message agent.v1.SubagentTypeExplore. - * Use `create(SubagentTypeExploreSchema)` to create a new message. - */ -export const SubagentTypeExploreSchema = /*@__PURE__*/ messageDesc(file_agent, 411); -/** - * Describes the message agent.v1.SubagentTypeCustom. - * Use `create(SubagentTypeCustomSchema)` to create a new message. - */ -export const SubagentTypeCustomSchema = /*@__PURE__*/ messageDesc(file_agent, 412); -/** - * Describes the message agent.v1.CustomSubagent. - * Use `create(CustomSubagentSchema)` to create a new message. - */ -export const CustomSubagentSchema = /*@__PURE__*/ messageDesc(file_agent, 413); -/** - * Describes the message agent.v1.SwitchModeArgs. - * Use `create(SwitchModeArgsSchema)` to create a new message. - */ -export const SwitchModeArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 414); -/** - * Describes the message agent.v1.SwitchModeResult. - * Use `create(SwitchModeResultSchema)` to create a new message. - */ -export const SwitchModeResultSchema = /*@__PURE__*/ messageDesc(file_agent, 415); -/** - * Describes the message agent.v1.SwitchModeSuccess. - * Use `create(SwitchModeSuccessSchema)` to create a new message. - */ -export const SwitchModeSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 416); -/** - * Describes the message agent.v1.SwitchModeError. - * Use `create(SwitchModeErrorSchema)` to create a new message. - */ -export const SwitchModeErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 417); -/** - * Describes the message agent.v1.SwitchModeRejected. - * Use `create(SwitchModeRejectedSchema)` to create a new message. - */ -export const SwitchModeRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 418); -/** - * Describes the message agent.v1.SwitchModeToolCall. - * Use `create(SwitchModeToolCallSchema)` to create a new message. - */ -export const SwitchModeToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 419); -/** - * Describes the message agent.v1.SwitchModeRequestQuery. - * Use `create(SwitchModeRequestQuerySchema)` to create a new message. - */ -export const SwitchModeRequestQuerySchema = -/*@__PURE__*/ -messageDesc(file_agent, 420); -/** - * Describes the message agent.v1.SwitchModeRequestResponse. - * Use `create(SwitchModeRequestResponseSchema)` to create a new message. - */ -export const SwitchModeRequestResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 421); -/** - * Describes the message agent.v1.SwitchModeRequestResponse_Approved. - * Use `create(SwitchModeRequestResponse_ApprovedSchema)` to create a new message. - */ -export const SwitchModeRequestResponse_ApprovedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 422); -/** - * Describes the message agent.v1.SwitchModeRequestResponse_Rejected. - * Use `create(SwitchModeRequestResponse_RejectedSchema)` to create a new message. - */ -export const SwitchModeRequestResponse_RejectedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 423); -/** - * Describes the message agent.v1.TodoItem. - * Use `create(TodoItemSchema)` to create a new message. - */ -export const TodoItemSchema = /*@__PURE__*/ messageDesc(file_agent, 424); -/** - * Describes the message agent.v1.UpdateTodosToolCall. - * Use `create(UpdateTodosToolCallSchema)` to create a new message. - */ -export const UpdateTodosToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 425); -/** - * Describes the message agent.v1.UpdateTodosArgs. - * Use `create(UpdateTodosArgsSchema)` to create a new message. - */ -export const UpdateTodosArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 426); -/** - * Describes the message agent.v1.UpdateTodosResult. - * Use `create(UpdateTodosResultSchema)` to create a new message. - */ -export const UpdateTodosResultSchema = /*@__PURE__*/ messageDesc(file_agent, 427); -/** - * Describes the message agent.v1.UpdateTodosSuccess. - * Use `create(UpdateTodosSuccessSchema)` to create a new message. - */ -export const UpdateTodosSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 428); -/** - * Describes the message agent.v1.UpdateTodosError. - * Use `create(UpdateTodosErrorSchema)` to create a new message. - */ -export const UpdateTodosErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 429); -/** - * Describes the message agent.v1.ReadTodosToolCall. - * Use `create(ReadTodosToolCallSchema)` to create a new message. - */ -export const ReadTodosToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 430); -/** - * Describes the message agent.v1.ReadTodosArgs. - * Use `create(ReadTodosArgsSchema)` to create a new message. - */ -export const ReadTodosArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 431); -/** - * Describes the message agent.v1.ReadTodosResult. - * Use `create(ReadTodosResultSchema)` to create a new message. - */ -export const ReadTodosResultSchema = /*@__PURE__*/ messageDesc(file_agent, 432); -/** - * Describes the message agent.v1.ReadTodosSuccess. - * Use `create(ReadTodosSuccessSchema)` to create a new message. - */ -export const ReadTodosSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 433); -/** - * Describes the message agent.v1.ReadTodosError. - * Use `create(ReadTodosErrorSchema)` to create a new message. - */ -export const ReadTodosErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 434); -/** - * Describes the message agent.v1.Range. - * Use `create(RangeSchema)` to create a new message. - */ -export const RangeSchema = /*@__PURE__*/ messageDesc(file_agent, 435); -/** - * Describes the message agent.v1.Position. - * Use `create(PositionSchema)` to create a new message. - */ -export const PositionSchema = /*@__PURE__*/ messageDesc(file_agent, 436); -/** - * Describes the message agent.v1.Error. - * Use `create(ErrorSchema)` to create a new message. - */ -export const ErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 437); -/** - * Describes the message agent.v1.WebSearchArgs. - * Use `create(WebSearchArgsSchema)` to create a new message. - */ -export const WebSearchArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 438); -/** - * Describes the message agent.v1.WebSearchResult. - * Use `create(WebSearchResultSchema)` to create a new message. - */ -export const WebSearchResultSchema = /*@__PURE__*/ messageDesc(file_agent, 439); -/** - * Describes the message agent.v1.WebSearchSuccess. - * Use `create(WebSearchSuccessSchema)` to create a new message. - */ -export const WebSearchSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 440); -/** - * Describes the message agent.v1.WebSearchError. - * Use `create(WebSearchErrorSchema)` to create a new message. - */ -export const WebSearchErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 441); -/** - * Describes the message agent.v1.WebSearchRejected. - * Use `create(WebSearchRejectedSchema)` to create a new message. - */ -export const WebSearchRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 442); -/** - * Describes the message agent.v1.WebSearchReference. - * Use `create(WebSearchReferenceSchema)` to create a new message. - */ -export const WebSearchReferenceSchema = /*@__PURE__*/ messageDesc(file_agent, 443); -/** - * Describes the message agent.v1.WebSearchToolCall. - * Use `create(WebSearchToolCallSchema)` to create a new message. - */ -export const WebSearchToolCallSchema = /*@__PURE__*/ messageDesc(file_agent, 444); -/** - * Describes the message agent.v1.WebSearchRequestQuery. - * Use `create(WebSearchRequestQuerySchema)` to create a new message. - */ -export const WebSearchRequestQuerySchema = -/*@__PURE__*/ -messageDesc(file_agent, 445); -/** - * Describes the message agent.v1.WebSearchRequestResponse. - * Use `create(WebSearchRequestResponseSchema)` to create a new message. - */ -export const WebSearchRequestResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 446); -/** - * Describes the message agent.v1.WebSearchRequestResponse_Approved. - * Use `create(WebSearchRequestResponse_ApprovedSchema)` to create a new message. - */ -export const WebSearchRequestResponse_ApprovedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 447); -/** - * Describes the message agent.v1.WebSearchRequestResponse_Rejected. - * Use `create(WebSearchRequestResponse_RejectedSchema)` to create a new message. - */ -export const WebSearchRequestResponse_RejectedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 448); -/** - * Describes the message agent.v1.WriteArgs. - * Use `create(WriteArgsSchema)` to create a new message. - */ -export const WriteArgsSchema = /*@__PURE__*/ messageDesc(file_agent, 449); -/** - * Describes the message agent.v1.WriteResult. - * Use `create(WriteResultSchema)` to create a new message. - */ -export const WriteResultSchema = /*@__PURE__*/ messageDesc(file_agent, 450); -/** - * Describes the message agent.v1.WriteSuccess. - * Use `create(WriteSuccessSchema)` to create a new message. - */ -export const WriteSuccessSchema = /*@__PURE__*/ messageDesc(file_agent, 451); -/** - * Describes the message agent.v1.WritePermissionDenied. - * Use `create(WritePermissionDeniedSchema)` to create a new message. - */ -export const WritePermissionDeniedSchema = -/*@__PURE__*/ -messageDesc(file_agent, 452); -/** - * Describes the message agent.v1.WriteNoSpace. - * Use `create(WriteNoSpaceSchema)` to create a new message. - */ -export const WriteNoSpaceSchema = /*@__PURE__*/ messageDesc(file_agent, 453); -/** - * Describes the message agent.v1.WriteError. - * Use `create(WriteErrorSchema)` to create a new message. - */ -export const WriteErrorSchema = /*@__PURE__*/ messageDesc(file_agent, 454); -/** - * Describes the message agent.v1.WriteRejected. - * Use `create(WriteRejectedSchema)` to create a new message. - */ -export const WriteRejectedSchema = /*@__PURE__*/ messageDesc(file_agent, 455); -/** - * Describes the message agent.v1.BootstrapStatsigRequest. - * Use `create(BootstrapStatsigRequestSchema)` to create a new message. - */ -export const BootstrapStatsigRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 456); -/** - * Describes the message agent.v1.PingResponse. - * Use `create(PingResponseSchema)` to create a new message. - */ -export const PingResponseSchema = /*@__PURE__*/ messageDesc(file_agent, 457); -/** - * Describes the message agent.v1.ExecRequest. - * Use `create(ExecRequestSchema)` to create a new message. - */ -export const ExecRequestSchema = /*@__PURE__*/ messageDesc(file_agent, 458); -/** - * Describes the message agent.v1.ExecResponse. - * Use `create(ExecResponseSchema)` to create a new message. - */ -export const ExecResponseSchema = /*@__PURE__*/ messageDesc(file_agent, 459); -/** - * Describes the message agent.v1.StdoutEvent. - * Use `create(StdoutEventSchema)` to create a new message. - */ -export const StdoutEventSchema = /*@__PURE__*/ messageDesc(file_agent, 460); -/** - * Describes the message agent.v1.StderrEvent. - * Use `create(StderrEventSchema)` to create a new message. - */ -export const StderrEventSchema = /*@__PURE__*/ messageDesc(file_agent, 461); -/** - * Describes the message agent.v1.ExitEvent. - * Use `create(ExitEventSchema)` to create a new message. - */ -export const ExitEventSchema = /*@__PURE__*/ messageDesc(file_agent, 462); -/** - * Describes the message agent.v1.ReadTextFileRequest. - * Use `create(ReadTextFileRequestSchema)` to create a new message. - */ -export const ReadTextFileRequestSchema = /*@__PURE__*/ messageDesc(file_agent, 463); -/** - * Describes the message agent.v1.ReadTextFileResponse. - * Use `create(ReadTextFileResponseSchema)` to create a new message. - */ -export const ReadTextFileResponseSchema = /*@__PURE__*/ messageDesc(file_agent, 464); -/** - * Describes the message agent.v1.WriteTextFileRequest. - * Use `create(WriteTextFileRequestSchema)` to create a new message. - */ -export const WriteTextFileRequestSchema = /*@__PURE__*/ messageDesc(file_agent, 465); -/** - * Describes the message agent.v1.WriteTextFileResponse. - * Use `create(WriteTextFileResponseSchema)` to create a new message. - */ -export const WriteTextFileResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 466); -/** - * Describes the message agent.v1.ReadBinaryFileRequest. - * Use `create(ReadBinaryFileRequestSchema)` to create a new message. - */ -export const ReadBinaryFileRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 467); -/** - * Describes the message agent.v1.ReadBinaryFileResponse. - * Use `create(ReadBinaryFileResponseSchema)` to create a new message. - */ -export const ReadBinaryFileResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 468); -/** - * Describes the message agent.v1.WriteBinaryFileRequest. - * Use `create(WriteBinaryFileRequestSchema)` to create a new message. - */ -export const WriteBinaryFileRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 469); -/** - * Describes the message agent.v1.WriteBinaryFileResponse. - * Use `create(WriteBinaryFileResponseSchema)` to create a new message. - */ -export const WriteBinaryFileResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 470); -/** - * Describes the message agent.v1.GetWorkspaceChangesHashRequest. - * Use `create(GetWorkspaceChangesHashRequestSchema)` to create a new message. - */ -export const GetWorkspaceChangesHashRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 471); -/** - * Describes the message agent.v1.GetWorkspaceChangesHashResponse. - * Use `create(GetWorkspaceChangesHashResponseSchema)` to create a new message. - */ -export const GetWorkspaceChangesHashResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 472); -/** - * Describes the message agent.v1.RefreshGithubAccessTokenRequest. - * Use `create(RefreshGithubAccessTokenRequestSchema)` to create a new message. - */ -export const RefreshGithubAccessTokenRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 473); -/** - * Describes the message agent.v1.RefreshGithubAccessTokenResponse. - * Use `create(RefreshGithubAccessTokenResponseSchema)` to create a new message. - */ -export const RefreshGithubAccessTokenResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 474); -/** - * Describes the message agent.v1.WarmRemoteAccessServerRequest. - * Use `create(WarmRemoteAccessServerRequestSchema)` to create a new message. - */ -export const WarmRemoteAccessServerRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 475); -/** - * Describes the message agent.v1.WarmRemoteAccessServerResponse. - * Use `create(WarmRemoteAccessServerResponseSchema)` to create a new message. - */ -export const WarmRemoteAccessServerResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 476); -/** - * Describes the message agent.v1.ListArtifactsRequest. - * Use `create(ListArtifactsRequestSchema)` to create a new message. - */ -export const ListArtifactsRequestSchema = /*@__PURE__*/ messageDesc(file_agent, 477); -/** - * Describes the message agent.v1.ArtifactUploadMetadata. - * Use `create(ArtifactUploadMetadataSchema)` to create a new message. - */ -export const ArtifactUploadMetadataSchema = -/*@__PURE__*/ -messageDesc(file_agent, 478); -/** - * Describes the message agent.v1.ListArtifactsResponse. - * Use `create(ListArtifactsResponseSchema)` to create a new message. - */ -export const ListArtifactsResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 479); -/** - * Describes the message agent.v1.UploadArtifactsRequest. - * Use `create(UploadArtifactsRequestSchema)` to create a new message. - */ -export const UploadArtifactsRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 480); -/** - * Describes the message agent.v1.ArtifactUploadInstruction. - * Use `create(ArtifactUploadInstructionSchema)` to create a new message. - */ -export const ArtifactUploadInstructionSchema = -/*@__PURE__*/ -messageDesc(file_agent, 481); -/** - * Describes the message agent.v1.ArtifactUploadDispatchResult. - * Use `create(ArtifactUploadDispatchResultSchema)` to create a new message. - */ -export const ArtifactUploadDispatchResultSchema = -/*@__PURE__*/ -messageDesc(file_agent, 482); -/** - * Describes the message agent.v1.UploadArtifactsResponse. - * Use `create(UploadArtifactsResponseSchema)` to create a new message. - */ -export const UploadArtifactsResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 483); -/** - * Describes the message agent.v1.GetMcpRefreshTokensRequest. - * Use `create(GetMcpRefreshTokensRequestSchema)` to create a new message. - */ -export const GetMcpRefreshTokensRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 484); -/** - * Describes the message agent.v1.GetMcpRefreshTokensResponse. - * Use `create(GetMcpRefreshTokensResponseSchema)` to create a new message. - */ -export const GetMcpRefreshTokensResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 485); -/** - * Describes the message agent.v1.UpdateEnvironmentVariablesRequest. - * Use `create(UpdateEnvironmentVariablesRequestSchema)` to create a new message. - */ -export const UpdateEnvironmentVariablesRequestSchema = -/*@__PURE__*/ -messageDesc(file_agent, 486); -/** - * Describes the message agent.v1.UpdateEnvironmentVariablesResponse. - * Use `create(UpdateEnvironmentVariablesResponseSchema)` to create a new message. - */ -export const UpdateEnvironmentVariablesResponseSchema = -/*@__PURE__*/ -messageDesc(file_agent, 487); -/** - * Describes the message agent.v1.McpOAuthStoredData. - * Use `create(McpOAuthStoredDataSchema)` to create a new message. - */ -export const McpOAuthStoredDataSchema = /*@__PURE__*/ messageDesc(file_agent, 488); -/** - * Describes the message agent.v1.Frame. - * Use `create(FrameSchema)` to create a new message. - */ -export const FrameSchema = /*@__PURE__*/ messageDesc(file_agent, 489); -/** - * Describes the message agent.v1.Empty. - * Use `create(EmptySchema)` to create a new message. - */ -export const EmptySchema = /*@__PURE__*/ messageDesc(file_agent, 490); -/** - * Describes the message agent.v1.BidiRequestId. - * Use `create(BidiRequestIdSchema)` to create a new message. - */ -export const BidiRequestIdSchema = /*@__PURE__*/ messageDesc(file_agent, 491); -/** - * @generated from enum agent.v1.AppliedAgentChange_ChangeType - */ -export var AppliedAgentChange_ChangeType; -(function (AppliedAgentChange_ChangeType) { - /** - * @generated from enum value: CHANGE_TYPE_UNSPECIFIED = 0; - */ - AppliedAgentChange_ChangeType[AppliedAgentChange_ChangeType["CHANGE_TYPE_UNSPECIFIED"] = 0] = "CHANGE_TYPE_UNSPECIFIED"; - /** - * @generated from enum value: CHANGE_TYPE_CREATED = 1; - */ - AppliedAgentChange_ChangeType[AppliedAgentChange_ChangeType["CHANGE_TYPE_CREATED"] = 1] = "CHANGE_TYPE_CREATED"; - /** - * @generated from enum value: CHANGE_TYPE_MODIFIED = 2; - */ - AppliedAgentChange_ChangeType[AppliedAgentChange_ChangeType["CHANGE_TYPE_MODIFIED"] = 2] = "CHANGE_TYPE_MODIFIED"; - /** - * @generated from enum value: CHANGE_TYPE_DELETED = 3; - */ - AppliedAgentChange_ChangeType[AppliedAgentChange_ChangeType["CHANGE_TYPE_DELETED"] = 3] = "CHANGE_TYPE_DELETED"; -})(AppliedAgentChange_ChangeType || (AppliedAgentChange_ChangeType = {})); -/** - * Describes the enum agent.v1.AppliedAgentChange_ChangeType. - */ -export const AppliedAgentChange_ChangeTypeSchema = -/*@__PURE__*/ -enumDesc(file_agent, 0); -/** - * @generated from enum agent.v1.MouseButton - */ -export var MouseButton; -(function (MouseButton) { - /** - * @generated from enum value: MOUSE_BUTTON_UNSPECIFIED = 0; - */ - MouseButton[MouseButton["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: MOUSE_BUTTON_LEFT = 1; - */ - MouseButton[MouseButton["LEFT"] = 1] = "LEFT"; - /** - * @generated from enum value: MOUSE_BUTTON_RIGHT = 2; - */ - MouseButton[MouseButton["RIGHT"] = 2] = "RIGHT"; - /** - * @generated from enum value: MOUSE_BUTTON_MIDDLE = 3; - */ - MouseButton[MouseButton["MIDDLE"] = 3] = "MIDDLE"; - /** - * @generated from enum value: MOUSE_BUTTON_BACK = 4; - */ - MouseButton[MouseButton["BACK"] = 4] = "BACK"; - /** - * @generated from enum value: MOUSE_BUTTON_FORWARD = 5; - */ - MouseButton[MouseButton["FORWARD"] = 5] = "FORWARD"; -})(MouseButton || (MouseButton = {})); -/** - * Describes the enum agent.v1.MouseButton. - */ -export const MouseButtonSchema = /*@__PURE__*/ enumDesc(file_agent, 1); -/** - * @generated from enum agent.v1.ScrollDirection - */ -export var ScrollDirection; -(function (ScrollDirection) { - /** - * @generated from enum value: SCROLL_DIRECTION_UNSPECIFIED = 0; - */ - ScrollDirection[ScrollDirection["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: SCROLL_DIRECTION_UP = 1; - */ - ScrollDirection[ScrollDirection["UP"] = 1] = "UP"; - /** - * @generated from enum value: SCROLL_DIRECTION_DOWN = 2; - */ - ScrollDirection[ScrollDirection["DOWN"] = 2] = "DOWN"; - /** - * @generated from enum value: SCROLL_DIRECTION_LEFT = 3; - */ - ScrollDirection[ScrollDirection["LEFT"] = 3] = "LEFT"; - /** - * @generated from enum value: SCROLL_DIRECTION_RIGHT = 4; - */ - ScrollDirection[ScrollDirection["RIGHT"] = 4] = "RIGHT"; -})(ScrollDirection || (ScrollDirection = {})); -/** - * Describes the enum agent.v1.ScrollDirection. - */ -export const ScrollDirectionSchema = /*@__PURE__*/ enumDesc(file_agent, 2); -/** - * @generated from enum agent.v1.CursorRuleSource - */ -export var CursorRuleSource; -(function (CursorRuleSource) { - /** - * @generated from enum value: CURSOR_RULE_SOURCE_UNSPECIFIED = 0; - */ - CursorRuleSource[CursorRuleSource["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: CURSOR_RULE_SOURCE_TEAM = 1; - */ - CursorRuleSource[CursorRuleSource["TEAM"] = 1] = "TEAM"; - /** - * @generated from enum value: CURSOR_RULE_SOURCE_USER = 2; - */ - CursorRuleSource[CursorRuleSource["USER"] = 2] = "USER"; -})(CursorRuleSource || (CursorRuleSource = {})); -/** - * Describes the enum agent.v1.CursorRuleSource. - */ -export const CursorRuleSourceSchema = /*@__PURE__*/ enumDesc(file_agent, 3); -/** - * @generated from enum agent.v1.DiagnosticSeverity - */ -export var DiagnosticSeverity; -(function (DiagnosticSeverity) { - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_UNSPECIFIED = 0; - */ - DiagnosticSeverity[DiagnosticSeverity["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_ERROR = 1; - */ - DiagnosticSeverity[DiagnosticSeverity["ERROR"] = 1] = "ERROR"; - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_WARNING = 2; - */ - DiagnosticSeverity[DiagnosticSeverity["WARNING"] = 2] = "WARNING"; - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_INFORMATION = 3; - */ - DiagnosticSeverity[DiagnosticSeverity["INFORMATION"] = 3] = "INFORMATION"; - /** - * @generated from enum value: DIAGNOSTIC_SEVERITY_HINT = 4; - */ - DiagnosticSeverity[DiagnosticSeverity["HINT"] = 4] = "HINT"; -})(DiagnosticSeverity || (DiagnosticSeverity = {})); -/** - * Describes the enum agent.v1.DiagnosticSeverity. - */ -export const DiagnosticSeveritySchema = /*@__PURE__*/ enumDesc(file_agent, 4); -/** - * @generated from enum agent.v1.RecordingMode - */ -export var RecordingMode; -(function (RecordingMode) { - /** - * @generated from enum value: RECORDING_MODE_UNSPECIFIED = 0; - */ - RecordingMode[RecordingMode["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: RECORDING_MODE_START_RECORDING = 1; - */ - RecordingMode[RecordingMode["START_RECORDING"] = 1] = "START_RECORDING"; - /** - * @generated from enum value: RECORDING_MODE_SAVE_RECORDING = 2; - */ - RecordingMode[RecordingMode["SAVE_RECORDING"] = 2] = "SAVE_RECORDING"; - /** - * @generated from enum value: RECORDING_MODE_DISCARD_RECORDING = 3; - */ - RecordingMode[RecordingMode["DISCARD_RECORDING"] = 3] = "DISCARD_RECORDING"; -})(RecordingMode || (RecordingMode = {})); -/** - * Describes the enum agent.v1.RecordingMode. - */ -export const RecordingModeSchema = /*@__PURE__*/ enumDesc(file_agent, 5); -/** - * @generated from enum agent.v1.RequestedFilePathRejectedReason - */ -export var RequestedFilePathRejectedReason; -(function (RequestedFilePathRejectedReason) { - /** - * @generated from enum value: REQUESTED_FILE_PATH_REJECTED_REASON_UNSPECIFIED = 0; - */ - RequestedFilePathRejectedReason[RequestedFilePathRejectedReason["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: REQUESTED_FILE_PATH_REJECTED_REASON_SLASHES_NOT_ALLOWED = 1; - */ - RequestedFilePathRejectedReason[RequestedFilePathRejectedReason["SLASHES_NOT_ALLOWED"] = 1] = "SLASHES_NOT_ALLOWED"; -})(RequestedFilePathRejectedReason || (RequestedFilePathRejectedReason = {})); -/** - * Describes the enum agent.v1.RequestedFilePathRejectedReason. - */ -export const RequestedFilePathRejectedReasonSchema = -/*@__PURE__*/ -enumDesc(file_agent, 6); -/** - * @generated from enum agent.v1.PackageType - */ -export var PackageType; -(function (PackageType) { - /** - * @generated from enum value: PACKAGE_TYPE_UNSPECIFIED = 0; - */ - PackageType[PackageType["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: PACKAGE_TYPE_CURSOR_PROJECT = 1; - */ - PackageType[PackageType["CURSOR_PROJECT"] = 1] = "CURSOR_PROJECT"; - /** - * @generated from enum value: PACKAGE_TYPE_CURSOR_PERSONAL = 2; - */ - PackageType[PackageType["CURSOR_PERSONAL"] = 2] = "CURSOR_PERSONAL"; - /** - * @generated from enum value: PACKAGE_TYPE_CLAUDE_SKILL = 3; - */ - PackageType[PackageType["CLAUDE_SKILL"] = 3] = "CLAUDE_SKILL"; - /** - * @generated from enum value: PACKAGE_TYPE_CLAUDE_PLUGIN = 4; - */ - PackageType[PackageType["CLAUDE_PLUGIN"] = 4] = "CLAUDE_PLUGIN"; -})(PackageType || (PackageType = {})); -/** - * Describes the enum agent.v1.PackageType. - */ -export const PackageTypeSchema = /*@__PURE__*/ enumDesc(file_agent, 7); -/** - * @generated from enum agent.v1.SandboxPolicy_Type - */ -export var SandboxPolicy_Type; -(function (SandboxPolicy_Type) { - /** - * @generated from enum value: TYPE_UNSPECIFIED = 0; - */ - SandboxPolicy_Type[SandboxPolicy_Type["TYPE_UNSPECIFIED"] = 0] = "TYPE_UNSPECIFIED"; - /** - * @generated from enum value: TYPE_INSECURE_NONE = 1; - */ - SandboxPolicy_Type[SandboxPolicy_Type["TYPE_INSECURE_NONE"] = 1] = "TYPE_INSECURE_NONE"; - /** - * @generated from enum value: TYPE_WORKSPACE_READWRITE = 2; - */ - SandboxPolicy_Type[SandboxPolicy_Type["TYPE_WORKSPACE_READWRITE"] = 2] = "TYPE_WORKSPACE_READWRITE"; - /** - * @generated from enum value: TYPE_WORKSPACE_READONLY = 3; - */ - SandboxPolicy_Type[SandboxPolicy_Type["TYPE_WORKSPACE_READONLY"] = 3] = "TYPE_WORKSPACE_READONLY"; -})(SandboxPolicy_Type || (SandboxPolicy_Type = {})); -/** - * Describes the enum agent.v1.SandboxPolicy_Type. - */ -export const SandboxPolicy_TypeSchema = /*@__PURE__*/ enumDesc(file_agent, 8); -/** - * @generated from enum agent.v1.TimeoutBehavior - */ -export var TimeoutBehavior; -(function (TimeoutBehavior) { - /** - * @generated from enum value: TIMEOUT_BEHAVIOR_UNSPECIFIED = 0; - */ - TimeoutBehavior[TimeoutBehavior["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: TIMEOUT_BEHAVIOR_CANCEL = 1; - */ - TimeoutBehavior[TimeoutBehavior["CANCEL"] = 1] = "CANCEL"; - /** - * @generated from enum value: TIMEOUT_BEHAVIOR_BACKGROUND = 2; - */ - TimeoutBehavior[TimeoutBehavior["BACKGROUND"] = 2] = "BACKGROUND"; -})(TimeoutBehavior || (TimeoutBehavior = {})); -/** - * Describes the enum agent.v1.TimeoutBehavior. - */ -export const TimeoutBehaviorSchema = /*@__PURE__*/ enumDesc(file_agent, 9); -/** - * @generated from enum agent.v1.ShellAbortReason - */ -export var ShellAbortReason; -(function (ShellAbortReason) { - /** - * @generated from enum value: SHELL_ABORT_REASON_UNSPECIFIED = 0; - */ - ShellAbortReason[ShellAbortReason["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: SHELL_ABORT_REASON_USER_ABORT = 1; - */ - ShellAbortReason[ShellAbortReason["USER_ABORT"] = 1] = "USER_ABORT"; - /** - * @generated from enum value: SHELL_ABORT_REASON_TIMEOUT = 2; - */ - ShellAbortReason[ShellAbortReason["TIMEOUT"] = 2] = "TIMEOUT"; -})(ShellAbortReason || (ShellAbortReason = {})); -/** - * Describes the enum agent.v1.ShellAbortReason. - */ -export const ShellAbortReasonSchema = /*@__PURE__*/ enumDesc(file_agent, 10); -/** - * @generated from enum agent.v1.CustomSubagentPermissionMode - */ -export var CustomSubagentPermissionMode; -(function (CustomSubagentPermissionMode) { - /** - * @generated from enum value: CUSTOM_SUBAGENT_PERMISSION_MODE_UNSPECIFIED = 0; - */ - CustomSubagentPermissionMode[CustomSubagentPermissionMode["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: CUSTOM_SUBAGENT_PERMISSION_MODE_DEFAULT = 1; - */ - CustomSubagentPermissionMode[CustomSubagentPermissionMode["DEFAULT"] = 1] = "DEFAULT"; - /** - * @generated from enum value: CUSTOM_SUBAGENT_PERMISSION_MODE_READONLY = 2; - */ - CustomSubagentPermissionMode[CustomSubagentPermissionMode["READONLY"] = 2] = "READONLY"; -})(CustomSubagentPermissionMode || (CustomSubagentPermissionMode = {})); -/** - * Describes the enum agent.v1.CustomSubagentPermissionMode. - */ -export const CustomSubagentPermissionModeSchema = -/*@__PURE__*/ -enumDesc(file_agent, 11); -/** - * @generated from enum agent.v1.TodoStatus - */ -export var TodoStatus; -(function (TodoStatus) { - /** - * @generated from enum value: TODO_STATUS_UNSPECIFIED = 0; - */ - TodoStatus[TodoStatus["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: TODO_STATUS_PENDING = 1; - */ - TodoStatus[TodoStatus["PENDING"] = 1] = "PENDING"; - /** - * @generated from enum value: TODO_STATUS_IN_PROGRESS = 2; - */ - TodoStatus[TodoStatus["IN_PROGRESS"] = 2] = "IN_PROGRESS"; - /** - * @generated from enum value: TODO_STATUS_COMPLETED = 3; - */ - TodoStatus[TodoStatus["COMPLETED"] = 3] = "COMPLETED"; - /** - * @generated from enum value: TODO_STATUS_CANCELLED = 4; - */ - TodoStatus[TodoStatus["CANCELLED"] = 4] = "CANCELLED"; -})(TodoStatus || (TodoStatus = {})); -/** - * Describes the enum agent.v1.TodoStatus. - */ -export const TodoStatusSchema = /*@__PURE__*/ enumDesc(file_agent, 12); -/** - * @generated from enum agent.v1.ClientOS - */ -export var ClientOS; -(function (ClientOS) { - /** - * @generated from enum value: CLIENT_OS_UNSPECIFIED = 0; - */ - ClientOS[ClientOS["CLIENT_OS_UNSPECIFIED"] = 0] = "CLIENT_OS_UNSPECIFIED"; - /** - * @generated from enum value: CLIENT_OS_WINDOWS = 1; - */ - ClientOS[ClientOS["CLIENT_OS_WINDOWS"] = 1] = "CLIENT_OS_WINDOWS"; - /** - * @generated from enum value: CLIENT_OS_MACOS = 2; - */ - ClientOS[ClientOS["CLIENT_OS_MACOS"] = 2] = "CLIENT_OS_MACOS"; - /** - * @generated from enum value: CLIENT_OS_LINUX = 3; - */ - ClientOS[ClientOS["CLIENT_OS_LINUX"] = 3] = "CLIENT_OS_LINUX"; -})(ClientOS || (ClientOS = {})); -/** - * Describes the enum agent.v1.ClientOS. - */ -export const ClientOSSchema = /*@__PURE__*/ enumDesc(file_agent, 13); -/** - * @generated from enum agent.v1.ArtifactUploadDispatchStatus - */ -export var ArtifactUploadDispatchStatus; -(function (ArtifactUploadDispatchStatus) { - /** - * @generated from enum value: ARTIFACT_UPLOAD_DISPATCH_STATUS_UNSPECIFIED = 0; - */ - ArtifactUploadDispatchStatus[ArtifactUploadDispatchStatus["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: ARTIFACT_UPLOAD_DISPATCH_STATUS_ACCEPTED = 1; - */ - ArtifactUploadDispatchStatus[ArtifactUploadDispatchStatus["ACCEPTED"] = 1] = "ACCEPTED"; - /** - * @generated from enum value: ARTIFACT_UPLOAD_DISPATCH_STATUS_REJECTED = 2; - */ - ArtifactUploadDispatchStatus[ArtifactUploadDispatchStatus["REJECTED"] = 2] = "REJECTED"; - /** - * @generated from enum value: ARTIFACT_UPLOAD_DISPATCH_STATUS_SKIPPED_ALREADY_IN_PROGRESS = 3; - */ - ArtifactUploadDispatchStatus[ArtifactUploadDispatchStatus["SKIPPED_ALREADY_IN_PROGRESS"] = 3] = "SKIPPED_ALREADY_IN_PROGRESS"; -})(ArtifactUploadDispatchStatus || (ArtifactUploadDispatchStatus = {})); -/** - * Describes the enum agent.v1.ArtifactUploadDispatchStatus. - */ -export const ArtifactUploadDispatchStatusSchema = -/*@__PURE__*/ -enumDesc(file_agent, 14); -/** - * @generated from enum agent.v1.Frame_Kind - */ -export var Frame_Kind; -(function (Frame_Kind) { - /** - * @generated from enum value: KIND_UNSPECIFIED = 0; - */ - Frame_Kind[Frame_Kind["KIND_UNSPECIFIED"] = 0] = "KIND_UNSPECIFIED"; - /** - * @generated from enum value: KIND_REQUEST = 1; - */ - Frame_Kind[Frame_Kind["KIND_REQUEST"] = 1] = "KIND_REQUEST"; - /** - * @generated from enum value: KIND_RESPONSE = 2; - */ - Frame_Kind[Frame_Kind["KIND_RESPONSE"] = 2] = "KIND_RESPONSE"; - /** - * @generated from enum value: KIND_ERROR = 3; - */ - Frame_Kind[Frame_Kind["KIND_ERROR"] = 3] = "KIND_ERROR"; -})(Frame_Kind || (Frame_Kind = {})); -/** - * Describes the enum agent.v1.Frame_Kind. - */ -export const Frame_KindSchema = /*@__PURE__*/ enumDesc(file_agent, 15); -/** - * @generated from enum agent.v1.BugbotDeeplinkEventKind - */ -export var BugbotDeeplinkEventKind; -(function (BugbotDeeplinkEventKind) { - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_UNSPECIFIED = 0; - */ - BugbotDeeplinkEventKind[BugbotDeeplinkEventKind["UNSPECIFIED"] = 0] = "UNSPECIFIED"; - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_CLICKED = 1; - */ - BugbotDeeplinkEventKind[BugbotDeeplinkEventKind["CLICKED"] = 1] = "CLICKED"; - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_HANDLED_DIALOG_SHOWN = 2; - */ - BugbotDeeplinkEventKind[BugbotDeeplinkEventKind["HANDLED_DIALOG_SHOWN"] = 2] = "HANDLED_DIALOG_SHOWN"; - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_HANDLED_CHAT_CREATED = 3; - */ - BugbotDeeplinkEventKind[BugbotDeeplinkEventKind["HANDLED_CHAT_CREATED"] = 3] = "HANDLED_CHAT_CREATED"; - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_ERROR = 4; - */ - BugbotDeeplinkEventKind[BugbotDeeplinkEventKind["ERROR"] = 4] = "ERROR"; - /** - * @generated from enum value: BUGBOT_DEEPLINK_EVENT_KIND_HANDLED_FIX_IN_WEB = 5; - */ - BugbotDeeplinkEventKind[BugbotDeeplinkEventKind["HANDLED_FIX_IN_WEB"] = 5] = "HANDLED_FIX_IN_WEB"; -})(BugbotDeeplinkEventKind || (BugbotDeeplinkEventKind = {})); -/** - * Describes the enum agent.v1.BugbotDeeplinkEventKind. - */ -export const BugbotDeeplinkEventKindSchema = /*@__PURE__*/ enumDesc(file_agent, 16); -/** - * Agent Service with bidirectional streaming - * - * @generated from service agent.v1.AgentService - */ -export const AgentService = /*@__PURE__*/ serviceDesc(file_agent, 0); -/** - * @generated from service agent.v1.ControlService - */ -export const ControlService = /*@__PURE__*/ serviceDesc(file_agent, 1); -/** - * Agent Service with unary RPC - * - * @generated from service agent.v1.ExecService - */ -export const ExecService = /*@__PURE__*/ serviceDesc(file_agent, 2); -/** - * @generated from service agent.v1.PrivateWorkerBridgeExternalService - */ -export const PrivateWorkerBridgeExternalService = /*@__PURE__*/ serviceDesc(file_agent, 3); -/** - * LifecycleService is exposed by the bridge *client*, in addition to ExecService (tool calls) and ControlService (control operations "within the daemon"). It operates at a similar abstraction level as AnyrunService: it represents operations similar to creating a VM, checking out a repository, etc. - * - * @generated from service agent.v1.LifecycleService - */ -export const LifecycleService = /*@__PURE__*/ serviceDesc(file_agent, 4); diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/proxy.d.ts b/references/upstream-plugins/opencode-cursor-oauth/dist/proxy.d.ts deleted file mode 100644 index e356f7ad..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/proxy.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -interface CursorUnaryRpcOptions { - accessToken: string; - rpcPath: string; - requestBody: Uint8Array; - url?: string; - timeoutMs?: number; -} -export declare function callCursorUnaryRpc(options: CursorUnaryRpcOptions): Promise<{ - body: Uint8Array; - exitCode: number; - timedOut: boolean; -}>; -export declare function getProxyPort(): number | undefined; -export declare function startProxy(getAccessToken: () => Promise, models?: ReadonlyArray<{ - id: string; - name: string; -}>): Promise; -export declare function stopProxy(): void; -export {}; diff --git a/references/upstream-plugins/opencode-cursor-oauth/dist/proxy.js b/references/upstream-plugins/opencode-cursor-oauth/dist/proxy.js deleted file mode 100644 index 2d40dd63..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/dist/proxy.js +++ /dev/null @@ -1,1128 +0,0 @@ -/** - * Local OpenAI-compatible proxy that translates requests to Cursor's gRPC protocol. - * - * Accepts POST /v1/chat/completions in OpenAI format, translates to Cursor's - * protobuf/HTTP2 Connect protocol, and streams back OpenAI-format SSE. - * - * Tool calling uses Cursor's native MCP tool protocol: - * - OpenAI tool defs → McpToolDefinition in RequestContext - * - Cursor toolCallStarted/Delta/Completed → OpenAI tool_calls SSE chunks - * - mcpArgs exec → pause stream, return tool_calls to caller - * - Follow-up request with tool results → resume bridge with mcpResult - * - * HTTP/2 transport is delegated to a Node child process (h2-bridge.mjs) - * because Bun's node:http2 module is broken. - */ -import { create, fromBinary, fromJson, toBinary, toJson } from "@bufbuild/protobuf"; -import { ValueSchema } from "@bufbuild/protobuf/wkt"; -import { AgentClientMessageSchema, AgentRunRequestSchema, AgentServerMessageSchema, ClientHeartbeatSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, BackgroundShellSpawnResultSchema, DeleteResultSchema, DeleteRejectedSchema, DiagnosticsResultSchema, ExecClientMessageSchema, FetchErrorSchema, FetchResultSchema, GetBlobResultSchema, GrepErrorSchema, GrepResultSchema, KvClientMessageSchema, LsRejectedSchema, LsResultSchema, McpErrorSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolDefinitionSchema, McpToolResultContentItemSchema, ModelDetailsSchema, ReadRejectedSchema, ReadResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, SetBlobResultSchema, ShellRejectedSchema, ShellResultSchema, UserMessageActionSchema, UserMessageSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, } from "./proto/agent_pb"; -import { createHash } from "node:crypto"; -import { resolve as pathResolve } from "node:path"; -const CURSOR_API_URL = process.env.CURSOR_API_URL ?? "https://api2.cursor.sh"; -const CONNECT_END_STREAM_FLAG = 0b00000010; -const BRIDGE_PATH = pathResolve(import.meta.dir, "h2-bridge.mjs"); -const SSE_HEADERS = { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", -}; -// Active bridges keyed by a session token (derived from conversation state). -// When tool_calls are returned, the bridge stays alive. The next request -// with tool results looks up the bridge and sends mcpResult messages. -const activeBridges = new Map(); -const conversationStates = new Map(); -const CONVERSATION_TTL_MS = 30 * 60 * 1000; // 30 minutes -function evictStaleConversations() { - const now = Date.now(); - for (const [key, stored] of conversationStates) { - if (now - stored.lastAccessMs > CONVERSATION_TTL_MS) { - conversationStates.delete(key); - } - } -} -/** Length-prefix a message: [4-byte BE length][payload] */ -function lpEncode(data) { - const buf = Buffer.alloc(4 + data.length); - buf.writeUInt32BE(data.length, 0); - buf.set(data, 4); - return buf; -} -/** Connect protocol frame: [1-byte flags][4-byte BE length][payload] */ -function frameConnectMessage(data, flags = 0) { - const frame = Buffer.alloc(5 + data.length); - frame[0] = flags; - frame.writeUInt32BE(data.length, 1); - frame.set(data, 5); - return frame; -} -function spawnBridge(options) { - const proc = Bun.spawn(["node", BRIDGE_PATH], { - stdin: "pipe", - stdout: "pipe", - stderr: "ignore", - }); - const config = JSON.stringify({ - accessToken: options.accessToken, - url: options.url ?? CURSOR_API_URL, - path: options.rpcPath, - unary: options.unary ?? false, - }); - proc.stdin.write(lpEncode(new TextEncoder().encode(config))); - const cbs = { - data: null, - close: null, - }; - // Track exit state so late onClose registrations fire immediately. - let exited = false; - let exitCode = 1; - (async () => { - const reader = proc.stdout.getReader(); - let pending = Buffer.alloc(0); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) - break; - pending = Buffer.concat([pending, Buffer.from(value)]); - while (pending.length >= 4) { - const len = pending.readUInt32BE(0); - if (pending.length < 4 + len) - break; - const payload = pending.subarray(4, 4 + len); - pending = pending.subarray(4 + len); - cbs.data?.(Buffer.from(payload)); - } - } - } - catch { - // Stream ended - } - const code = await proc.exited ?? 1; - exited = true; - exitCode = code; - cbs.close?.(code); - })(); - return { - proc, - get alive() { return !exited; }, - write(data) { - try { - proc.stdin.write(lpEncode(data)); - } - catch { } - }, - end() { - try { - proc.stdin.write(lpEncode(new Uint8Array(0))); - proc.stdin.end(); - } - catch { } - }, - onData(cb) { cbs.data = cb; }, - onClose(cb) { - if (exited) { - // Process already exited — invoke immediately so streams don't hang. - queueMicrotask(() => cb(exitCode)); - } - else { - cbs.close = cb; - } - }, - }; -} -export async function callCursorUnaryRpc(options) { - const bridge = spawnBridge({ - accessToken: options.accessToken, - rpcPath: options.rpcPath, - url: options.url, - unary: true, - }); - const chunks = []; - const { promise, resolve } = Promise.withResolvers(); - let timedOut = false; - const timeoutMs = options.timeoutMs ?? 5_000; - const timeout = timeoutMs > 0 - ? setTimeout(() => { - timedOut = true; - try { - bridge.proc.kill(); - } - catch { } - }, timeoutMs) - : undefined; - bridge.onData((chunk) => { - chunks.push(Buffer.from(chunk)); - }); - bridge.onClose((exitCode) => { - if (timeout) - clearTimeout(timeout); - resolve({ - body: Buffer.concat(chunks), - exitCode, - timedOut, - }); - }); - // Unary: send raw protobuf body (no Connect framing) - bridge.write(options.requestBody); - bridge.end(); - return promise; -} -let proxyServer; -let proxyPort; -let proxyAccessTokenProvider; -let proxyModels = []; -function buildOpenAIModelList(models) { - return models.map((model) => ({ - id: model.id, - object: "model", - created: 0, - owned_by: "cursor", - })); -} -export function getProxyPort() { - return proxyPort; -} -export async function startProxy(getAccessToken, models = []) { - proxyAccessTokenProvider = getAccessToken; - proxyModels = models.map((model) => ({ - id: model.id, - name: model.name, - })); - if (proxyServer && proxyPort) - return proxyPort; - proxyServer = Bun.serve({ - port: 0, - idleTimeout: 255, // max — Cursor responses can take 30s+ - async fetch(req) { - const url = new URL(req.url); - if (req.method === "GET" && url.pathname === "/v1/models") { - return new Response(JSON.stringify({ - object: "list", - data: buildOpenAIModelList(proxyModels), - }), { headers: { "Content-Type": "application/json" } }); - } - if (req.method === "POST" && url.pathname === "/v1/chat/completions") { - try { - const body = (await req.json()); - if (!proxyAccessTokenProvider) { - throw new Error("Cursor proxy access token provider not configured"); - } - const accessToken = await proxyAccessTokenProvider(); - return handleChatCompletion(body, accessToken); - } - catch (err) { - const message = err instanceof Error ? err.message : String(err); - return new Response(JSON.stringify({ - error: { message, type: "server_error", code: "internal_error" }, - }), { status: 500, headers: { "Content-Type": "application/json" } }); - } - } - return new Response("Not Found", { status: 404 }); - }, - }); - proxyPort = proxyServer.port; - if (!proxyPort) - throw new Error("Failed to bind proxy to a port"); - return proxyPort; -} -export function stopProxy() { - if (proxyServer) { - proxyServer.stop(); - proxyServer = undefined; - proxyPort = undefined; - proxyAccessTokenProvider = undefined; - proxyModels = []; - } - // Clean up any lingering bridges - for (const active of activeBridges.values()) { - clearInterval(active.heartbeatTimer); - active.bridge.end(); - } - activeBridges.clear(); - conversationStates.clear(); -} -function handleChatCompletion(body, accessToken) { - const { systemPrompt, userText, turns, toolResults } = parseMessages(body.messages); - const modelId = body.model; - const tools = body.tools ?? []; - if (!userText && toolResults.length === 0) { - return new Response(JSON.stringify({ - error: { - message: "No user message found", - type: "invalid_request_error", - }, - }), { status: 400, headers: { "Content-Type": "application/json" } }); - } - // bridgeKey: model-specific, for active tool-call bridges - // convKey: model-independent, for conversation state that survives model switches - const bridgeKey = deriveBridgeKey(modelId, body.messages); - const convKey = deriveConversationKey(body.messages); - const activeBridge = activeBridges.get(bridgeKey); - if (activeBridge && toolResults.length > 0) { - activeBridges.delete(bridgeKey); - if (activeBridge.bridge.alive) { - // Resume the live bridge with tool results - return handleToolResultResume(activeBridge, toolResults, modelId, bridgeKey, convKey); - } - // Bridge died (timeout, server disconnect, etc.). - // Clean up and fall through to start a fresh bridge. - clearInterval(activeBridge.heartbeatTimer); - activeBridge.bridge.end(); - } - // Clean up stale bridge if present - if (activeBridge && activeBridges.has(bridgeKey)) { - clearInterval(activeBridge.heartbeatTimer); - activeBridge.bridge.end(); - activeBridges.delete(bridgeKey); - } - let stored = conversationStates.get(convKey); - if (!stored) { - stored = { - conversationId: deterministicConversationId(convKey), - checkpoint: null, - blobStore: new Map(), - lastAccessMs: Date.now(), - }; - conversationStates.set(convKey, stored); - } - stored.lastAccessMs = Date.now(); - evictStaleConversations(); - // Build the request. When tool results are present but the bridge died, - // we must still include the last user text so Cursor has context. - const mcpTools = buildMcpToolDefinitions(tools); - const effectiveUserText = userText || (toolResults.length > 0 - ? toolResults.map((r) => r.content).join("\n") - : ""); - const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, turns, stored.conversationId, stored.checkpoint, stored.blobStore); - payload.mcpTools = mcpTools; - if (body.stream === false) { - return handleNonStreamingResponse(payload, accessToken, modelId, convKey); - } - return handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey); -} -/** Normalize OpenAI message content to a plain string. */ -function textContent(content) { - if (content == null) - return ""; - if (typeof content === "string") - return content; - return content - .filter((p) => p.type === "text" && p.text) - .map((p) => p.text) - .join("\n"); -} -function parseMessages(messages) { - let systemPrompt = "You are a helpful assistant."; - const pairs = []; - const toolResults = []; - // Collect system messages - const systemParts = messages - .filter((m) => m.role === "system") - .map((m) => textContent(m.content)); - if (systemParts.length > 0) { - systemPrompt = systemParts.join("\n"); - } - // Separate tool results from conversation turns - const nonSystem = messages.filter((m) => m.role !== "system"); - let pendingUser = ""; - for (const msg of nonSystem) { - if (msg.role === "tool") { - toolResults.push({ - toolCallId: msg.tool_call_id ?? "", - content: textContent(msg.content), - }); - } - else if (msg.role === "user") { - if (pendingUser) { - pairs.push({ userText: pendingUser, assistantText: "" }); - } - pendingUser = textContent(msg.content); - } - else if (msg.role === "assistant") { - // Skip assistant messages that are just tool_calls with no text - const text = textContent(msg.content); - if (pendingUser) { - pairs.push({ userText: pendingUser, assistantText: text }); - pendingUser = ""; - } - } - } - let lastUserText = ""; - if (pendingUser) { - lastUserText = pendingUser; - } - else if (pairs.length > 0 && toolResults.length === 0) { - const last = pairs.pop(); - lastUserText = last.userText; - } - return { systemPrompt, userText: lastUserText, turns: pairs, toolResults }; -} -/** Convert OpenAI tool definitions to Cursor's MCP tool protobuf format. */ -function buildMcpToolDefinitions(tools) { - return tools.map((t) => { - const fn = t.function; - const jsonSchema = fn.parameters && typeof fn.parameters === "object" - ? fn.parameters - : { type: "object", properties: {}, required: [] }; - const inputSchema = toBinary(ValueSchema, fromJson(ValueSchema, jsonSchema)); - return create(McpToolDefinitionSchema, { - name: fn.name, - description: fn.description || "", - providerIdentifier: "opencode", - toolName: fn.name, - inputSchema, - }); - }); -} -/** Decode a Cursor MCP arg value (protobuf Value bytes) to a JS value. */ -function decodeMcpArgValue(value) { - try { - const parsed = fromBinary(ValueSchema, value); - return toJson(ValueSchema, parsed); - } - catch { } - return new TextDecoder().decode(value); -} -/** Decode a map of MCP arg values. */ -function decodeMcpArgsMap(args) { - const decoded = {}; - for (const [key, value] of Object.entries(args)) { - decoded[key] = decodeMcpArgValue(value); - } - return decoded; -} -function buildCursorRequest(modelId, systemPrompt, userText, turns, conversationId, checkpoint, existingBlobStore) { - const blobStore = new Map(existingBlobStore ?? []); - // System prompt → blob store (Cursor requests it back via KV handshake) - const systemJson = JSON.stringify({ role: "system", content: systemPrompt }); - const systemBytes = new TextEncoder().encode(systemJson); - const systemBlobId = new Uint8Array(createHash("sha256").update(systemBytes).digest()); - blobStore.set(Buffer.from(systemBlobId).toString("hex"), systemBytes); - let conversationState; - if (checkpoint) { - conversationState = fromBinary(ConversationStateStructureSchema, checkpoint); - } - else { - const turnBytes = []; - for (const turn of turns) { - const userMsg = create(UserMessageSchema, { - text: turn.userText, - messageId: crypto.randomUUID(), - }); - const userMsgBytes = toBinary(UserMessageSchema, userMsg); - const stepBytes = []; - if (turn.assistantText) { - const step = create(ConversationStepSchema, { - message: { - case: "assistantMessage", - value: create(AssistantMessageSchema, { text: turn.assistantText }), - }, - }); - stepBytes.push(toBinary(ConversationStepSchema, step)); - } - const agentTurn = create(AgentConversationTurnStructureSchema, { - userMessage: userMsgBytes, - steps: stepBytes, - }); - const turnStructure = create(ConversationTurnStructureSchema, { - turn: { case: "agentConversationTurn", value: agentTurn }, - }); - turnBytes.push(toBinary(ConversationTurnStructureSchema, turnStructure)); - } - conversationState = create(ConversationStateStructureSchema, { - rootPromptMessagesJson: [systemBlobId], - turns: turnBytes, - todos: [], - pendingToolCalls: [], - previousWorkspaceUris: [], - fileStates: {}, - fileStatesV2: {}, - summaryArchives: [], - turnTimings: [], - subagentStates: {}, - selfSummaryCount: 0, - readPaths: [], - }); - } - const userMessage = create(UserMessageSchema, { - text: userText, - messageId: crypto.randomUUID(), - }); - const action = create(ConversationActionSchema, { - action: { - case: "userMessageAction", - value: create(UserMessageActionSchema, { userMessage }), - }, - }); - const modelDetails = create(ModelDetailsSchema, { - modelId, - displayModelId: modelId, - displayName: modelId, - }); - const runRequest = create(AgentRunRequestSchema, { - conversationState, - action, - modelDetails, - conversationId, - }); - const clientMessage = create(AgentClientMessageSchema, { - message: { case: "runRequest", value: runRequest }, - }); - return { - requestBytes: toBinary(AgentClientMessageSchema, clientMessage), - blobStore, - mcpTools: [], - }; -} -function parseConnectEndStream(data) { - try { - const payload = JSON.parse(new TextDecoder().decode(data)); - const error = payload?.error; - if (error) { - const code = error.code ?? "unknown"; - const message = error.message ?? "Unknown error"; - return new Error(`Connect error ${code}: ${message}`); - } - return null; - } - catch { - return new Error("Failed to parse Connect end stream"); - } -} -function makeHeartbeatBytes() { - const heartbeat = create(AgentClientMessageSchema, { - message: { - case: "clientHeartbeat", - value: create(ClientHeartbeatSchema, {}), - }, - }); - return frameConnectMessage(toBinary(AgentClientMessageSchema, heartbeat)); -} -/** - * Create a stateful parser for Connect protocol frames. - * Handles buffering partial data across chunks. - */ -function createConnectFrameParser(onMessage, onEndStream) { - let pending = Buffer.alloc(0); - return (incoming) => { - pending = Buffer.concat([pending, incoming]); - while (pending.length >= 5) { - const flags = pending[0]; - const msgLen = pending.readUInt32BE(1); - if (pending.length < 5 + msgLen) - break; - const messageBytes = pending.subarray(5, 5 + msgLen); - pending = pending.subarray(5 + msgLen); - if (flags & CONNECT_END_STREAM_FLAG) { - onEndStream(messageBytes); - } - else { - onMessage(messageBytes); - } - } - }; -} -const THINKING_TAG_NAMES = ['think', 'thinking', 'reasoning', 'thought', 'think_intent']; -const MAX_THINKING_TAG_LEN = 16; // is 15 chars -/** - * Strip thinking tags from streamed text, routing tagged content to reasoning. - * Buffers partial tags across chunk boundaries. - */ -function createThinkingTagFilter() { - let buffer = ''; - let inThinking = false; - return { - process(text) { - const input = buffer + text; - buffer = ''; - let content = ''; - let reasoning = ''; - let lastIdx = 0; - const re = new RegExp(`<(/?)(?:${THINKING_TAG_NAMES.join('|')})\\s*>`, 'gi'); - let match; - while ((match = re.exec(input)) !== null) { - const before = input.slice(lastIdx, match.index); - if (inThinking) - reasoning += before; - else - content += before; - inThinking = match[1] !== '/'; - lastIdx = re.lastIndex; - } - const rest = input.slice(lastIdx); - // Buffer a trailing '<' that could be the start of a thinking tag. - const ltPos = rest.lastIndexOf('<'); - if (ltPos >= 0 && rest.length - ltPos < MAX_THINKING_TAG_LEN && /^<\/?[a-z_]*$/i.test(rest.slice(ltPos))) { - buffer = rest.slice(ltPos); - const before = rest.slice(0, ltPos); - if (inThinking) - reasoning += before; - else - content += before; - } - else { - if (inThinking) - reasoning += rest; - else - content += rest; - } - return { content, reasoning }; - }, - flush() { - const b = buffer; - buffer = ''; - if (!b) - return { content: '', reasoning: '' }; - return inThinking ? { content: '', reasoning: b } : { content: b, reasoning: '' }; - }, - }; -} -function computeUsage(state) { - const completion_tokens = state.outputTokens; - const total_tokens = state.totalTokens || completion_tokens; - const prompt_tokens = Math.max(0, total_tokens - completion_tokens); - return { prompt_tokens, completion_tokens, total_tokens }; -} -function processServerMessage(msg, blobStore, mcpTools, sendFrame, state, onText, onMcpExec, onCheckpoint) { - const msgCase = msg.message.case; - if (msgCase === "interactionUpdate") { - handleInteractionUpdate(msg.message.value, state, onText); - } - else if (msgCase === "kvServerMessage") { - handleKvMessage(msg.message.value, blobStore, sendFrame); - } - else if (msgCase === "execServerMessage") { - handleExecMessage(msg.message.value, mcpTools, sendFrame, onMcpExec); - } - else if (msgCase === "conversationCheckpointUpdate") { - const stateStructure = msg.message.value; - if (stateStructure.tokenDetails) { - state.totalTokens = stateStructure.tokenDetails.usedTokens; - } - if (onCheckpoint) { - onCheckpoint(toBinary(ConversationStateStructureSchema, stateStructure)); - } - } -} -function handleInteractionUpdate(update, state, onText) { - const updateCase = update.message?.case; - if (updateCase === "textDelta") { - const delta = update.message.value.text || ""; - if (delta) - onText(delta, false); - } - else if (updateCase === "thinkingDelta") { - const delta = update.message.value.text || ""; - if (delta) - onText(delta, true); - } - else if (updateCase === "tokenDelta") { - state.outputTokens += update.message.value.tokens ?? 0; - } - // toolCallStarted, partialToolCall, toolCallDelta, toolCallCompleted - // are intentionally ignored. MCP tool calls flow through the exec - // message path (mcpArgs → mcpResult), not interaction updates. -} -/** Send a KV client response back to Cursor. */ -function sendKvResponse(kvMsg, messageCase, value, sendFrame) { - const response = create(KvClientMessageSchema, { - id: kvMsg.id, - message: { case: messageCase, value: value }, - }); - const clientMsg = create(AgentClientMessageSchema, { - message: { case: "kvClientMessage", value: response }, - }); - sendFrame(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMsg))); -} -function handleKvMessage(kvMsg, blobStore, sendFrame) { - const kvCase = kvMsg.message.case; - if (kvCase === "getBlobArgs") { - const blobId = kvMsg.message.value.blobId; - const blobIdKey = Buffer.from(blobId).toString("hex"); - const blobData = blobStore.get(blobIdKey); - sendKvResponse(kvMsg, "getBlobResult", create(GetBlobResultSchema, blobData ? { blobData } : {}), sendFrame); - } - else if (kvCase === "setBlobArgs") { - const { blobId, blobData } = kvMsg.message.value; - blobStore.set(Buffer.from(blobId).toString("hex"), blobData); - sendKvResponse(kvMsg, "setBlobResult", create(SetBlobResultSchema, {}), sendFrame); - } -} -function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec) { - const execCase = execMsg.message.case; - if (execCase === "requestContextArgs") { - const requestContext = create(RequestContextSchema, { - rules: [], - repositoryInfo: [], - tools: mcpTools, - gitRepos: [], - projectLayouts: [], - mcpInstructions: [], - fileContents: {}, - customSubagents: [], - }); - const result = create(RequestContextResultSchema, { - result: { - case: "success", - value: create(RequestContextSuccessSchema, { requestContext }), - }, - }); - sendExecResult(execMsg, "requestContextResult", result, sendFrame); - return; - } - if (execCase === "mcpArgs") { - const mcpArgs = execMsg.message.value; - const decoded = decodeMcpArgsMap(mcpArgs.args ?? {}); - onMcpExec({ - execId: execMsg.execId, - execMsgId: execMsg.id, - toolCallId: mcpArgs.toolCallId || crypto.randomUUID(), - toolName: mcpArgs.toolName || mcpArgs.name, - decodedArgs: JSON.stringify(decoded), - }); - return; - } - // --- Reject native Cursor tools --- - // The model tries these first. We must respond with rejection/error - // so it falls back to our MCP tools (registered via RequestContext). - const REJECT_REASON = "Tool not available in this environment. Use the MCP tools provided instead."; - if (execCase === "readArgs") { - const args = execMsg.message.value; - const result = create(ReadResultSchema, { - result: { case: "rejected", value: create(ReadRejectedSchema, { path: args.path, reason: REJECT_REASON }) }, - }); - sendExecResult(execMsg, "readResult", result, sendFrame); - return; - } - if (execCase === "lsArgs") { - const args = execMsg.message.value; - const result = create(LsResultSchema, { - result: { case: "rejected", value: create(LsRejectedSchema, { path: args.path, reason: REJECT_REASON }) }, - }); - sendExecResult(execMsg, "lsResult", result, sendFrame); - return; - } - if (execCase === "grepArgs") { - const result = create(GrepResultSchema, { - result: { case: "error", value: create(GrepErrorSchema, { error: REJECT_REASON }) }, - }); - sendExecResult(execMsg, "grepResult", result, sendFrame); - return; - } - if (execCase === "writeArgs") { - const args = execMsg.message.value; - const result = create(WriteResultSchema, { - result: { case: "rejected", value: create(WriteRejectedSchema, { path: args.path, reason: REJECT_REASON }) }, - }); - sendExecResult(execMsg, "writeResult", result, sendFrame); - return; - } - if (execCase === "deleteArgs") { - const args = execMsg.message.value; - const result = create(DeleteResultSchema, { - result: { case: "rejected", value: create(DeleteRejectedSchema, { path: args.path, reason: REJECT_REASON }) }, - }); - sendExecResult(execMsg, "deleteResult", result, sendFrame); - return; - } - if (execCase === "shellArgs" || execCase === "shellStreamArgs") { - const args = execMsg.message.value; - const result = create(ShellResultSchema, { - result: { - case: "rejected", - value: create(ShellRejectedSchema, { - command: args.command ?? "", - workingDirectory: args.workingDirectory ?? "", - reason: REJECT_REASON, - isReadonly: false, - }), - }, - }); - sendExecResult(execMsg, "shellResult", result, sendFrame); - return; - } - if (execCase === "backgroundShellSpawnArgs") { - const args = execMsg.message.value; - const result = create(BackgroundShellSpawnResultSchema, { - result: { - case: "rejected", - value: create(ShellRejectedSchema, { - command: args.command ?? "", - workingDirectory: args.workingDirectory ?? "", - reason: REJECT_REASON, - isReadonly: false, - }), - }, - }); - sendExecResult(execMsg, "backgroundShellSpawnResult", result, sendFrame); - return; - } - if (execCase === "writeShellStdinArgs") { - const result = create(WriteShellStdinResultSchema, { - result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: REJECT_REASON }) }, - }); - sendExecResult(execMsg, "writeShellStdinResult", result, sendFrame); - return; - } - if (execCase === "fetchArgs") { - const args = execMsg.message.value; - const result = create(FetchResultSchema, { - result: { case: "error", value: create(FetchErrorSchema, { url: args.url ?? "", error: REJECT_REASON }) }, - }); - sendExecResult(execMsg, "fetchResult", result, sendFrame); - return; - } - if (execCase === "diagnosticsArgs") { - const result = create(DiagnosticsResultSchema, {}); - sendExecResult(execMsg, "diagnosticsResult", result, sendFrame); - return; - } - // MCP resource/screen/computer exec types - const miscCaseMap = { - listMcpResourcesExecArgs: "listMcpResourcesExecResult", - readMcpResourceExecArgs: "readMcpResourceExecResult", - recordScreenArgs: "recordScreenResult", - computerUseArgs: "computerUseResult", - }; - const resultCase = miscCaseMap[execCase]; - if (resultCase) { - sendExecResult(execMsg, resultCase, create(McpResultSchema, {}), sendFrame); - return; - } - // Unknown exec type — log and ignore - console.error(`[proxy] unhandled exec: ${execCase}`); -} -/** Send an exec client message back to Cursor. */ -function sendExecResult(execMsg, messageCase, value, sendFrame) { - const execClientMessage = create(ExecClientMessageSchema, { - id: execMsg.id, - execId: execMsg.execId, - message: { case: messageCase, value: value }, - }); - const clientMessage = create(AgentClientMessageSchema, { - message: { case: "execClientMessage", value: execClientMessage }, - }); - sendFrame(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage))); -} -function normalizeConversationMessages(messages) { - return messages - .filter((m) => m.role !== "tool") - .map((m) => ({ - role: m.role, - content: textContent(m.content), - })) - .filter((m) => m.content || m.role === "user" || m.role === "system"); -} -/** Derive a key for active bridge lookup (tool-call continuations). Model-specific. */ -function deriveBridgeKey(modelId, messages) { - const normalizedMessages = normalizeConversationMessages(messages); - return createHash("sha256") - .update(JSON.stringify({ - modelId, - messages: normalizedMessages, - })) - .digest("hex") - .slice(0, 16); -} -/** Derive a key for conversation state. Model-independent so context survives model switches. */ -function deriveConversationKey(messages) { - const normalizedMessages = normalizeConversationMessages(messages); - return createHash("sha256") - .update(JSON.stringify({ - messages: normalizedMessages, - })) - .digest("hex") - .slice(0, 16); -} -/** Deterministic UUID derived from convKey so Cursor's server-side conversation - * persists across proxy restarts. Formats 16 bytes of SHA-256 as a v4-shaped UUID. */ -function deterministicConversationId(convKey) { - const hex = createHash("sha256") - .update(`cursor-conv-id:${convKey}`) - .digest("hex") - .slice(0, 32); - // Format as UUID: xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx - return [ - hex.slice(0, 8), - hex.slice(8, 12), - `4${hex.slice(13, 16)}`, - `${(0x8 | (parseInt(hex[16], 16) & 0x3)).toString(16)}${hex.slice(17, 20)}`, - hex.slice(20, 32), - ].join("-"); -} -/** Create an SSE streaming Response that reads from a live bridge. */ -function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey) { - const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`; - const created = Math.floor(Date.now() / 1000); - const stream = new ReadableStream({ - start(controller) { - const encoder = new TextEncoder(); - let closed = false; - const sendSSE = (data) => { - if (closed) - return; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); - }; - const sendDone = () => { - if (closed) - return; - controller.enqueue(encoder.encode("data: [DONE]\n\n")); - }; - const closeController = () => { - if (closed) - return; - closed = true; - controller.close(); - }; - const makeChunk = (delta, finishReason = null) => ({ - id: completionId, - object: "chat.completion.chunk", - created, - model: modelId, - choices: [{ index: 0, delta, finish_reason: finishReason }], - }); - const makeUsageChunk = () => { - const { prompt_tokens, completion_tokens, total_tokens } = computeUsage(state); - return { - id: completionId, - object: "chat.completion.chunk", - created, - model: modelId, - choices: [], - usage: { prompt_tokens, completion_tokens, total_tokens }, - }; - }; - const state = { - toolCallIndex: 0, - pendingExecs: [], - outputTokens: 0, - totalTokens: 0, - }; - const tagFilter = createThinkingTagFilter(); - let mcpExecReceived = false; - const processChunk = createConnectFrameParser((messageBytes) => { - try { - const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes); - processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => { - if (isThinking) { - sendSSE(makeChunk({ reasoning_content: text })); - } - else { - const { content, reasoning } = tagFilter.process(text); - if (reasoning) - sendSSE(makeChunk({ reasoning_content: reasoning })); - if (content) - sendSSE(makeChunk({ content })); - } - }, - // onMcpExec — the model wants to execute a tool. - (exec) => { - state.pendingExecs.push(exec); - mcpExecReceived = true; - const flushed = tagFilter.flush(); - if (flushed.reasoning) - sendSSE(makeChunk({ reasoning_content: flushed.reasoning })); - if (flushed.content) - sendSSE(makeChunk({ content: flushed.content })); - const toolCallIndex = state.toolCallIndex++; - sendSSE(makeChunk({ - tool_calls: [{ - index: toolCallIndex, - id: exec.toolCallId, - type: "function", - function: { - name: exec.toolName, - arguments: exec.decodedArgs, - }, - }], - })); - // Keep the bridge alive for tool result continuation. - activeBridges.set(bridgeKey, { - bridge, - heartbeatTimer, - blobStore, - mcpTools, - pendingExecs: state.pendingExecs, - }); - sendSSE(makeChunk({}, "tool_calls")); - sendDone(); - closeController(); - }, (checkpointBytes) => { - const stored = conversationStates.get(convKey); - if (stored) { - stored.checkpoint = checkpointBytes; - stored.lastAccessMs = Date.now(); - } - }); - } - catch { - // Skip unparseable messages - } - }, (endStreamBytes) => { - const endError = parseConnectEndStream(endStreamBytes); - if (endError) { - sendSSE(makeChunk({ content: `\n[Error: ${endError.message}]` })); - } - }); - bridge.onData(processChunk); - bridge.onClose((code) => { - clearInterval(heartbeatTimer); - const stored = conversationStates.get(convKey); - if (stored) { - for (const [k, v] of blobStore) - stored.blobStore.set(k, v); - stored.lastAccessMs = Date.now(); - } - if (!mcpExecReceived) { - const flushed = tagFilter.flush(); - if (flushed.reasoning) - sendSSE(makeChunk({ reasoning_content: flushed.reasoning })); - if (flushed.content) - sendSSE(makeChunk({ content: flushed.content })); - sendSSE(makeChunk({}, "stop")); - sendSSE(makeUsageChunk()); - sendDone(); - closeController(); - } - else if (code !== 0) { - // Bridge died while tool calls are pending (timeout, crash, etc.). - // Close the SSE stream so the client doesn't hang forever. - sendSSE(makeChunk({ content: "\n[Error: bridge connection lost]" })); - sendSSE(makeChunk({}, "stop")); - sendSSE(makeUsageChunk()); - sendDone(); - closeController(); - // Remove stale entry so the next request doesn't try to resume it. - activeBridges.delete(bridgeKey); - } - }); - }, - }); - return new Response(stream, { headers: SSE_HEADERS }); -} -/** Spawn a bridge, send the initial request frame, and start heartbeat. */ -function startBridge(accessToken, requestBytes) { - const bridge = spawnBridge({ - accessToken, - rpcPath: "/agent.v1.AgentService/Run", - }); - bridge.write(frameConnectMessage(requestBytes)); - const heartbeatTimer = setInterval(() => bridge.write(makeHeartbeatBytes()), 5_000); - return { bridge, heartbeatTimer }; -} -function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey) { - const { bridge, heartbeatTimer } = startBridge(accessToken, payload.requestBytes); - return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey); -} -/** Resume a paused bridge by sending MCP results and continuing to stream. */ -function handleToolResultResume(active, toolResults, modelId, bridgeKey, convKey) { - const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs } = active; - // Send mcpResult for each pending exec that has a matching tool result - for (const exec of pendingExecs) { - const result = toolResults.find((r) => r.toolCallId === exec.toolCallId); - const mcpResult = result - ? create(McpResultSchema, { - result: { - case: "success", - value: create(McpSuccessSchema, { - content: [ - create(McpToolResultContentItemSchema, { - content: { - case: "text", - value: create(McpTextContentSchema, { text: result.content }), - }, - }), - ], - isError: false, - }), - }, - }) - : create(McpResultSchema, { - result: { - case: "error", - value: create(McpErrorSchema, { error: "Tool result not provided" }), - }, - }); - const execClientMessage = create(ExecClientMessageSchema, { - id: exec.execMsgId, - execId: exec.execId, - message: { - case: "mcpResult", - value: mcpResult, - }, - }); - const clientMessage = create(AgentClientMessageSchema, { - message: { case: "execClientMessage", value: execClientMessage }, - }); - bridge.write(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage))); - } - return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey); -} -async function handleNonStreamingResponse(payload, accessToken, modelId, convKey) { - const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`; - const created = Math.floor(Date.now() / 1000); - const { text, usage } = await collectFullResponse(payload, accessToken, convKey); - return new Response(JSON.stringify({ - id: completionId, - object: "chat.completion", - created, - model: modelId, - choices: [ - { - index: 0, - message: { role: "assistant", content: text }, - finish_reason: "stop", - }, - ], - usage, - }), { headers: { "Content-Type": "application/json" } }); -} -async function collectFullResponse(payload, accessToken, convKey) { - const { promise, resolve } = Promise.withResolvers(); - let fullText = ""; - const { bridge, heartbeatTimer } = startBridge(accessToken, payload.requestBytes); - const state = { - toolCallIndex: 0, - pendingExecs: [], - outputTokens: 0, - totalTokens: 0, - }; - const tagFilter = createThinkingTagFilter(); - bridge.onData(createConnectFrameParser((messageBytes) => { - try { - const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes); - processServerMessage(serverMessage, payload.blobStore, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => { - if (isThinking) - return; - const { content } = tagFilter.process(text); - fullText += content; - }, () => { }, (checkpointBytes) => { - const stored = conversationStates.get(convKey); - if (stored) { - stored.checkpoint = checkpointBytes; - stored.lastAccessMs = Date.now(); - } - }); - } - catch { - // Skip - } - }, () => { })); - bridge.onClose(() => { - clearInterval(heartbeatTimer); - const stored = conversationStates.get(convKey); - if (stored) { - for (const [k, v] of payload.blobStore) - stored.blobStore.set(k, v); - stored.lastAccessMs = Date.now(); - } - const flushed = tagFilter.flush(); - fullText += flushed.content; - const usage = computeUsage(state); - resolve({ - text: fullText, - usage, - }); - }); - return promise; -} diff --git a/references/upstream-plugins/opencode-cursor-oauth/package.json b/references/upstream-plugins/opencode-cursor-oauth/package.json deleted file mode 100644 index 4279971b..00000000 --- a/references/upstream-plugins/opencode-cursor-oauth/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@playwo/opencode-cursor-oauth", - "version": "0.4.3", - "description": "OpenCode plugin that connects Cursor's API to OpenCode via OAuth, model discovery, and a local OpenAI-compatible proxy.", - "license": "MIT", - "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsc -p tsconfig.json && node scripts/copy-runtime.mjs", - "test": "bun test/smoke.ts", - "prepublishOnly": "npm run build" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/PoolPirate/opencode-cursor.git" - }, - "homepage": "https://github.com/PoolPirate/opencode-cursor#readme", - "bugs": { - "url": "https://github.com/PoolPirate/opencode-cursor/issues" - }, - "keywords": [ - "opencode", - "opencode-plugin", - "cursor", - "oauth", - "openai-compatible" - ], - "engines": { - "node": ">=18" - }, - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@opencode-ai/plugin": "^1.2.27", - "@bufbuild/protobuf": "^2.0.0", - "zod": "^3.24.0" - }, - "devDependencies": { - "@types/bun": "^1.3.11", - "typescript": "^5.9.3" - } -} diff --git a/scripts/check-upstream-plugin-updates.mjs b/scripts/check-upstream-plugin-updates.mjs index de0db7a4..4b3ce293 100644 --- a/scripts/check-upstream-plugin-updates.mjs +++ b/scripts/check-upstream-plugin-updates.mjs @@ -139,7 +139,7 @@ async function main() { ? "up-to-date" : "update available"; console.log( - `${spec.pluginId}: tracked ${tracked.version}, latest ${latest.version} (${status})`, + `${spec.pluginId}: tracked ${tracked.version}, npm ${spec.distTag} ${latest.version} (${status})`, ); } diff --git a/scripts/lib/upstream-plugin-registry.mjs b/scripts/lib/upstream-plugin-registry.mjs index 00a41a2e..7f613522 100644 --- a/scripts/lib/upstream-plugin-registry.mjs +++ b/scripts/lib/upstream-plugin-registry.mjs @@ -69,11 +69,11 @@ export function normalizeLatestPublishedPluginVersion(spec, packument) { typeof packument === "object" && packument["dist-tags"] && typeof packument["dist-tags"] === "object" - ? packument["dist-tags"].latest + ? packument["dist-tags"][spec.distTag] : ""; if (typeof latestVersion !== "string" || !latestVersion) { - throw new Error(`Package ${spec.packageName} is missing a latest dist-tag.`); + throw new Error(`Package ${spec.packageName} is missing the ${spec.distTag} dist-tag.`); } const versionEntry = diff --git a/scripts/lib/upstream-plugin-sanitization.mjs b/scripts/lib/upstream-plugin-sanitization.mjs index 462dfdd6..6d9a1bce 100644 --- a/scripts/lib/upstream-plugin-sanitization.mjs +++ b/scripts/lib/upstream-plugin-sanitization.mjs @@ -8,6 +8,12 @@ const AGY_UNREDACTED_CREDENTIAL_PATTERNS = Object.freeze([ /\b\d{10,}-[a-z0-9]+\.apps\.googleusercontent\.com\b/i, /GOCSPX-[A-Za-z0-9_-]+/, ]); +// cursor-opencode-provider ships no OAuth client secret (browser login is PKCE); these +// patterns fail the sync if a real Cursor API key or access token ever lands in the snapshot. +const CURSOR_UNREDACTED_CREDENTIAL_PATTERNS = Object.freeze([ + /crsr_[A-Za-z0-9]{20,}/, + /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}/, +]); const GEMINI_BUNDLE_REPLACEMENTS = Object.freeze([ { label: "GEMINI_CLIENT_ID", @@ -32,47 +38,6 @@ const GEMINI_SOURCE_MAP_REPLACEMENTS = Object.freeze([ replacement: `$1${REDACTED_GOOGLE_OAUTH_CLIENT_SECRET}$3`, }, ]); -const CURSOR_SAFE_MODELS_BLOCK = `export async function getCursorModels(apiKey) { - if (cachedModels) - return cachedModels; - const discovered = await fetchCursorUsableModels(apiKey); - if (discovered && discovered.length > 0) { - cachedModels = discovered; - return cachedModels; - } - return FALLBACK_MODELS; -}`; -const CURSOR_SAFE_PROXY_BLOCK = `function normalizeConversationMessages(messages) { - return messages - .filter((m) => m.role !== "tool") - .map((m) => ({ - role: m.role, - content: textContent(m.content), - })) - .filter((m) => m.content || m.role === "user" || m.role === "system"); -} -/** Derive a key for active bridge lookup (tool-call continuations). Model-specific. */ -function deriveBridgeKey(modelId, messages) { - const normalizedMessages = normalizeConversationMessages(messages); - return createHash("sha256") - .update(JSON.stringify({ - modelId, - messages: normalizedMessages, - })) - .digest("hex") - .slice(0, 16); -} -/** Derive a key for conversation state. Model-independent so context survives model switches. */ -function deriveConversationKey(messages) { - const normalizedMessages = normalizeConversationMessages(messages); - return createHash("sha256") - .update(JSON.stringify({ - messages: normalizedMessages, - })) - .digest("hex") - .slice(0, 16); -}`; - const SNAPSHOT_SANITIZERS = Object.freeze({ "opencode-agy-auth": Object.freeze([ { @@ -172,34 +137,6 @@ const SNAPSHOT_SANITIZERS = Object.freeze({ replacements: GEMINI_SOURCE_MAP_REPLACEMENTS, }, ]), - "opencode-cursor-oauth": Object.freeze([ - { - relativePath: "dist/models.js", - replacements: [ - { - label: "CURSOR_DISCOVERY_CACHE_FALLBACK", - alreadySanitizedPattern: - /if \(discovered && discovered\.length > 0\) {\s+cachedModels = discovered;\s+return cachedModels;\s+}\s+return FALLBACK_MODELS;/, - pattern: - /export async function getCursorModels\(apiKey\) {\s+if \(cachedModels\)\s+return cachedModels;\s+const discovered = await fetchCursorUsableModels\(apiKey\);\s+cachedModels = discovered && discovered\.length > 0 \? discovered : FALLBACK_MODELS;\s+return cachedModels;\s+}/, - replacement: CURSOR_SAFE_MODELS_BLOCK, - }, - ], - }, - { - relativePath: "dist/proxy.js", - replacements: [ - { - label: "CURSOR_TRANSCRIPT_BRIDGE_KEY", - alreadySanitizedPattern: - /function deriveBridgeKey\(modelId, messages\) {\s+const normalizedMessages = (?:normalizeConversationMessages\(messages\)|messages\s+\.filter\(\(m\) => m\.role !== "tool"\)[\s\S]+?\.filter\(\(m\) => m\.content \|\| m\.role === "user" \|\| m\.role === "system"\));\s+return createHash\("sha256"\)\s+\.update\(JSON\.stringify\({\s+modelId,\s+messages: normalizedMessages,\s+}\)\)\s+\.digest\("hex"\)\s+\.slice\(0, 16\);\s+}\s+\/\*\* Derive a key for conversation state\. Model-independent so context survives model switches\. \*\/\s+function deriveConversationKey\(messages\) {\s+const normalizedMessages = (?:normalizeConversationMessages\(messages\)|messages\s+\.filter\(\(m\) => m\.role !== "tool"\)[\s\S]+?\.filter\(\(m\) => m\.content \|\| m\.role === "user" \|\| m\.role === "system"\));\s+return createHash\("sha256"\)\s+\.update\(JSON\.stringify\({\s+messages: normalizedMessages,\s+}\)\)\s+\.digest\("hex"\)\s+\.slice\(0, 16\);\s+}/, - pattern: - /\/\*\* Derive a key for active bridge lookup \(tool-call continuations\)\. Model-specific\. \*\/\s+function deriveBridgeKey\(modelId, messages\) {\s+const firstUserMsg = messages\.find\(\(m\) => m\.role === "user"\);\s+const firstUserText = firstUserMsg \? textContent\(firstUserMsg\.content\) : "";\s+return createHash\("sha256"\)\s+\.update\(`bridge:\$\{modelId\}:\$\{firstUserText\.slice\(0, 200\)\}`\)\s+\.digest\("hex"\)\s+\.slice\(0, 16\);\s+}\s+\/\*\* Derive a key for conversation state\. Model-independent so context survives model switches\. \*\/\s+function deriveConversationKey\(messages\) {\s+const firstUserMsg = messages\.find\(\(m\) => m\.role === "user"\);\s+const firstUserText = firstUserMsg \? textContent\(firstUserMsg\.content\) : "";\s+return createHash\("sha256"\)\s+\.update\(`conv:\$\{firstUserText\.slice\(0, 200\)\}`\)\s+\.digest\("hex"\)\s+\.slice\(0, 16\);\s+}/, - replacement: CURSOR_SAFE_PROXY_BLOCK, - }, - ], - }, - ]), }); async function listSnapshotFiles(rootPath) { @@ -220,19 +157,24 @@ async function listSnapshotFiles(rootPath) { return files; } -async function verifyAgySnapshotSanitized(pluginRoot, capturedCredentialValues) { +async function verifySnapshotHasNoCredentials( + pluginRoot, + credentialLabel, + unredactedPatterns, + capturedCredentialValues, +) { for (const filePath of await listSnapshotFiles(pluginRoot)) { const content = await readFile(filePath); for (const value of capturedCredentialValues) { if (content.includes(value)) { - throw new Error(`Found unsanitized AGY OAuth credential in ${filePath}.`); + throw new Error(`Found unsanitized ${credentialLabel} in ${filePath}.`); } } const text = content.toString("utf8"); - if (AGY_UNREDACTED_CREDENTIAL_PATTERNS.some((pattern) => pattern.test(text))) { - throw new Error(`Found unsanitized AGY OAuth credential in ${filePath}.`); + if (unredactedPatterns.some((pattern) => pattern.test(text))) { + throw new Error(`Found unsanitized ${credentialLabel} in ${filePath}.`); } } } @@ -296,7 +238,21 @@ export async function sanitizeUpstreamPluginSnapshot(pluginId, pluginRoot) { } if (pluginId === "opencode-agy-auth") { - await verifyAgySnapshotSanitized(pluginRoot, capturedCredentialValues); + await verifySnapshotHasNoCredentials( + pluginRoot, + "AGY OAuth credential", + AGY_UNREDACTED_CREDENTIAL_PATTERNS, + capturedCredentialValues, + ); + } + + if (pluginId === "cursor-opencode-provider") { + await verifySnapshotHasNoCredentials( + pluginRoot, + "Cursor credential", + CURSOR_UNREDACTED_CREDENTIAL_PATTERNS, + capturedCredentialValues, + ); } if ( diff --git a/scripts/lib/upstream-plugin-specs.mjs b/scripts/lib/upstream-plugin-specs.mjs index 75d209e7..ffd4fc27 100644 --- a/scripts/lib/upstream-plugin-specs.mjs +++ b/scripts/lib/upstream-plugin-specs.mjs @@ -2,20 +2,24 @@ export const UPSTREAM_PLUGIN_REFERENCE_ROOT = "references/upstream-plugins"; const RAW_UPSTREAM_PLUGIN_SPECS = [ { - pluginId: "opencode-cursor-oauth", - packageName: "@playwo/opencode-cursor-oauth", - repoOwner: "PoolPirate", - repoName: "opencode-cursor", + pluginId: "cursor-opencode-provider", + packageName: "cursor-opencode-provider", + distTag: "latest", + repoOwner: "oakimov", + repoName: "cursor-opencode-provider", }, { pluginId: "opencode-gemini-auth", packageName: "opencode-gemini-auth", + distTag: "latest", repoOwner: "jenslys", repoName: "opencode-gemini-auth", }, { pluginId: "opencode-agy-auth", packageName: "@anthonyhaussman/opencode-agy-auth", + // The OpenCode 2 build ships only on the alpha dist-tag; `latest` is the OpenCode 1 build. + distTag: "alpha", repoOwner: "anthonyhaussman", repoName: "opencode-agy-auth", allowMissingRepositoryMetadata: true, diff --git a/tests/pricing-resolver.coverage.test.ts b/tests/pricing-resolver.coverage.test.ts index 7f74152e..80ed545a 100644 --- a/tests/pricing-resolver.coverage.test.ts +++ b/tests/pricing-resolver.coverage.test.ts @@ -14,28 +14,35 @@ import { } from "../src/lib/modelsdev-pricing.js"; import { resolvePricingKey } from "../src/lib/quota-stats.js"; -const CURSOR_UPSTREAM_MODELS_PATH = new URL( - "../references/upstream-plugins/opencode-cursor-oauth/dist/models.js", +const CURSOR_UPSTREAM_PRICING_PATH = new URL( + "../references/upstream-plugins/cursor-opencode-provider/dist/pricing-data.js", import.meta.url, ); -const CURSOR_UPSTREAM_INTENTIONALLY_UNKNOWN_MODELS = new Set(); - -function getCursorUpstreamFallbackModelIds(): string[] { - const source = readFileSync(CURSOR_UPSTREAM_MODELS_PATH, "utf8"); - const marker = "const FALLBACK_MODELS = ["; +// Price not confirmed, so these stay unknown: models.dev rates differ from Cursor's for the +// Gemini Flash ids, and muse-spark (meta) is not a pricing snapshot provider. +const CURSOR_UPSTREAM_INTENTIONALLY_UNKNOWN_MODELS = new Set([ + "gemini-3.6-flash", + "gemini-3.7-flash", + "gemini-3.8-flash", + "muse-spark-1.3", +]); + +function getCursorUpstreamPricedModelIds(): string[] { + const source = readFileSync(CURSOR_UPSTREAM_PRICING_PATH, "utf8"); + const marker = "export const CURSOR_MODEL_COSTS = "; const start = source.indexOf(marker); if (start === -1) { - throw new Error("Unable to locate Cursor upstream FALLBACK_MODELS in synced reference"); + throw new Error("Unable to locate Cursor upstream CURSOR_MODEL_COSTS in synced reference"); } - const bodyStart = source.indexOf("[", start); + const bodyStart = start + marker.length; let depth = 0; let bodyEnd = -1; for (let index = bodyStart; index < source.length; index += 1) { const char = source[index]; - if (char === "[") depth += 1; - if (char === "]") { + if (char === "{") depth += 1; + if (char === "}") { depth -= 1; if (depth === 0) { bodyEnd = index; @@ -44,13 +51,12 @@ function getCursorUpstreamFallbackModelIds(): string[] { } } - if (bodyStart === -1 || bodyEnd === -1) { - throw new Error("Unable to parse Cursor upstream FALLBACK_MODELS in synced reference"); + if (source[bodyStart] !== "{" || bodyEnd === -1) { + throw new Error("Unable to parse Cursor upstream CURSOR_MODEL_COSTS in synced reference"); } - return [...source.slice(bodyStart, bodyEnd + 1).matchAll(/\bid\s*:\s*"([^"]+)"/g)] - .map((match) => match[1]!) - .sort((a, b) => a.localeCompare(b)); + const costs = JSON.parse(source.slice(bodyStart, bodyEnd + 1)) as Record; + return Object.keys(costs).sort((a, b) => a.localeCompare(b)); } describe("resolvePricingKey snapshot coverage", () => { @@ -567,20 +573,20 @@ describe("resolvePricingKey snapshot coverage", () => { expect(failures).toEqual([]); }); - it("accounts for every synced upstream Cursor fallback model id", () => { - const fallbackModelIds = getCursorUpstreamFallbackModelIds(); + it("accounts for every synced upstream Cursor priced model id", () => { + const upstreamModelIds = getCursorUpstreamPricedModelIds(); const intentionallyUnknown = [...CURSOR_UPSTREAM_INTENTIONALLY_UNKNOWN_MODELS].sort((a, b) => a.localeCompare(b), ); expect( - intentionallyUnknown.filter((modelID) => !fallbackModelIds.includes(modelID)), - "Remove stale entries from CURSOR_UPSTREAM_INTENTIONALLY_UNKNOWN_MODELS when upstream fallback ids change.", + intentionallyUnknown.filter((modelID) => !upstreamModelIds.includes(modelID)), + "Remove stale entries from CURSOR_UPSTREAM_INTENTIONALLY_UNKNOWN_MODELS when upstream priced ids change.", ).toEqual([]); const failures: string[] = []; - for (const modelID of fallbackModelIds) { + for (const modelID of upstreamModelIds) { const resolvedModel = resolveCursorModel(`cursor/${modelID}`); const resolvedPricing = resolvePricingKey({ providerID: "cursor", diff --git a/tests/upstream-plugin-issues.test.ts b/tests/upstream-plugin-issues.test.ts index 0f9aa590..6948d1c5 100644 --- a/tests/upstream-plugin-issues.test.ts +++ b/tests/upstream-plugin-issues.test.ts @@ -40,18 +40,18 @@ describe("upstream-plugin-issues", () => { expect(body).toContain(""); }); - it("uses the canonical Cursor package and repo while keeping the internal tracked key stable", () => { - const cursorSpec = getUpstreamPluginSpec("opencode-cursor-oauth"); + it("uses the Cursor companion package and repo in issue details", () => { + const cursorSpec = getUpstreamPluginSpec("cursor-opencode-provider"); expect(cursorSpec).toBeTruthy(); if (!cursorSpec) return; const cursorTracked = { - npmUrl: "https://www.npmjs.com/package/%40playwo/opencode-cursor-oauth/v/0.4.3", - packageName: "@playwo/opencode-cursor-oauth", - publishedAt: "2026-04-08T14:04:58.057Z", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - repo: "PoolPirate/opencode-cursor", - version: "0.4.3", + npmUrl: "https://www.npmjs.com/package/cursor-opencode-provider/v/0.7.3", + packageName: "cursor-opencode-provider", + publishedAt: "2026-09-20T10:00:00.000Z", + referenceDir: "references/upstream-plugins/cursor-opencode-provider", + repo: "oakimov/cursor-opencode-provider", + version: "0.7.3", }; const body = buildUpstreamPluginIssueBody({ @@ -61,10 +61,12 @@ describe("upstream-plugin-issues", () => { tracked: cursorTracked, }); - expect(body).toContain("- Plugin: `opencode-cursor-oauth`"); - expect(body).toContain("- Package: `@playwo/opencode-cursor-oauth`"); - expect(body).toContain("- Repository: `PoolPirate/opencode-cursor`"); - expect(body).toContain("- Reference path: `references/upstream-plugins/opencode-cursor-oauth`"); + expect(body).toContain("- Plugin: `cursor-opencode-provider`"); + expect(body).toContain("- Package: `cursor-opencode-provider`"); + expect(body).toContain("- Repository: `oakimov/cursor-opencode-provider`"); + expect(body).toContain( + "- Reference path: `references/upstream-plugins/cursor-opencode-provider`", + ); }); it("uses the canonical scoped AGY package and repository in issue details", () => { @@ -94,26 +96,22 @@ describe("upstream-plugin-issues", () => { }); it("treats same-version Cursor metadata drift as an available update", () => { - const cursorSpec = getUpstreamPluginSpec("opencode-cursor-oauth"); + const cursorSpec = getUpstreamPluginSpec("cursor-opencode-provider"); expect(cursorSpec).toBeTruthy(); if (!cursorSpec) return; const trackedCursor = { - npmUrl: "https://www.npmjs.com/package/opencode-cursor-oauth/v/0.4.3", - packageName: "opencode-cursor-oauth", - publishedAt: "2026-04-08T14:04:58.057Z", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - repo: "ephraimduncan/opencode-cursor", - version: "0.4.3", + npmUrl: "https://www.npmjs.com/package/cursor-opencode-provider/v/0.7.3", + packageName: "cursor-opencode-provider", + publishedAt: "2026-09-20T10:00:00.000Z", + referenceDir: "references/upstream-plugins/cursor-opencode-provider", + repo: "old-owner/cursor-opencode-provider", + version: "0.7.3", }; const latestCursor = { - npmUrl: "https://www.npmjs.com/package/%40playwo/opencode-cursor-oauth/v/0.4.3", - packageName: "@playwo/opencode-cursor-oauth", - publishedAt: "2026-04-08T14:04:58.057Z", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - repo: "PoolPirate/opencode-cursor", - version: "0.4.3", + ...trackedCursor, + repo: "oakimov/cursor-opencode-provider", }; const plan = planUpstreamPluginIssueAction({ @@ -124,7 +122,7 @@ describe("upstream-plugin-issues", () => { }); expect(plan.create).toMatchObject({ - title: "[check] opencode-cursor-oauth had update", + title: "[check] cursor-opencode-provider had update", }); expect(plan.create?.body).toContain(""); }); diff --git a/tests/upstream-plugin-lock.test.ts b/tests/upstream-plugin-lock.test.ts index c5f38ddf..cf7dbdd1 100644 --- a/tests/upstream-plugin-lock.test.ts +++ b/tests/upstream-plugin-lock.test.ts @@ -60,16 +60,16 @@ describe("upstream-plugin-lock", () => { it("reads the committed lock when synchronized metadata remains uncommitted", async () => { const lockPath = path.join(testState.repoRoot, "references", "upstream-plugins", "lock.json"); const previousEntry = { - npmUrl: "https://www.npmjs.com/package/opencode-cursor-oauth/v/0.4.3", - packageName: "opencode-cursor-oauth", + npmUrl: "https://www.npmjs.com/package/%40old-scope/cursor-opencode-provider/v/0.4.3", + packageName: "@old-scope/cursor-opencode-provider", publishedAt: "2026-04-08T14:04:58.057Z", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - repo: "old-owner/opencode-cursor", + referenceDir: "references/upstream-plugins/cursor-opencode-provider", + repo: "old-owner/cursor-opencode-provider", version: "0.4.3", }; await writeFile( lockPath, - `${JSON.stringify({ plugins: { "opencode-cursor-oauth": previousEntry } }, null, 2)}\n`, + `${JSON.stringify({ plugins: { "cursor-opencode-provider": previousEntry } }, null, 2)}\n`, "utf8", ); await execFileAsync("git", ["init"], { cwd: testState.repoRoot }); @@ -94,13 +94,13 @@ describe("upstream-plugin-lock", () => { const currentEntry = { ...previousEntry, - npmUrl: "https://www.npmjs.com/package/%40playwo/opencode-cursor-oauth/v/0.4.3", - packageName: "@playwo/opencode-cursor-oauth", - repo: "PoolPirate/opencode-cursor", + npmUrl: "https://www.npmjs.com/package/cursor-opencode-provider/v/0.4.3", + packageName: "cursor-opencode-provider", + repo: "oakimov/cursor-opencode-provider", }; await writeFile( lockPath, - `${JSON.stringify({ plugins: { "opencode-cursor-oauth": currentEntry } }, null, 2)}\n`, + `${JSON.stringify({ plugins: { "cursor-opencode-provider": currentEntry } }, null, 2)}\n`, "utf8", ); @@ -121,7 +121,7 @@ describe("upstream-plugin-lock", () => { changeKind: "metadata", changedFields: ["packageName", "repo", "npmUrl"], currentVersion: "0.4.3", - pluginId: "opencode-cursor-oauth", + pluginId: "cursor-opencode-provider", previousVersion: "0.4.3", }, ]); @@ -132,12 +132,12 @@ describe("upstream-plugin-lock", () => { await writeUpstreamPluginLock({ plugins: { - "opencode-cursor-oauth": { - npmUrl: "https://www.npmjs.com/package/%40playwo/opencode-cursor-oauth/v/2.0.0", - packageName: "@playwo/opencode-cursor-oauth", + "cursor-opencode-provider": { + npmUrl: "https://www.npmjs.com/package/cursor-opencode-provider/v/2.0.0", + packageName: "cursor-opencode-provider", publishedAt: "2026-03-20T00:00:00.000Z", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - repo: "PoolPirate/opencode-cursor", + referenceDir: "references/upstream-plugins/cursor-opencode-provider", + repo: "oakimov/cursor-opencode-provider", version: "2.0.0", }, "opencode-gemini-auth": { @@ -154,7 +154,7 @@ describe("upstream-plugin-lock", () => { const lockPath = path.join(testState.repoRoot, "references", "upstream-plugins", "lock.json"); await expect(readFile(lockPath, "utf8")).resolves.toContain(`"version": "2.0.0"`); await expect(readFile(lockPath, "utf8")).resolves.toContain( - `"packageName": "@playwo/opencode-cursor-oauth"`, + `"packageName": "cursor-opencode-provider"`, ); const entries = await readdir(path.dirname(lockPath)); diff --git a/tests/upstream-plugin-registry.test.ts b/tests/upstream-plugin-registry.test.ts index 6433429a..7e3ce3c9 100644 --- a/tests/upstream-plugin-registry.test.ts +++ b/tests/upstream-plugin-registry.test.ts @@ -4,57 +4,56 @@ import { normalizeLatestPublishedPluginVersion } from "../scripts/lib/upstream-p import { getUpstreamPluginSpec } from "../scripts/lib/upstream-plugin-specs.mjs"; describe("upstream-plugin-registry", () => { - it("builds canonical npm metadata for the scoped Cursor package", () => { - const spec = getUpstreamPluginSpec("opencode-cursor-oauth"); + it("builds canonical npm metadata for the Cursor package", () => { + const spec = getUpstreamPluginSpec("cursor-opencode-provider"); expect(spec).toBeTruthy(); if (!spec) return; const latest = normalizeLatestPublishedPluginVersion(spec, { "dist-tags": { - latest: "0.4.3", + latest: "0.7.3", }, repository: { type: "git", - url: "git+https://github.com/PoolPirate/opencode-cursor.git", + url: "git+https://github.com/oakimov/cursor-opencode-provider.git", }, time: { - "0.4.3": "2026-04-08T14:04:58.057Z", + "0.7.3": "2026-09-20T10:00:00.000Z", }, versions: { - "0.4.3": { + "0.7.3": { dist: { tarball: - "https://example.test/@playwo/opencode-cursor-oauth/-/opencode-cursor-oauth-0.4.3.tgz", + "https://registry.npmjs.org/cursor-opencode-provider/-/cursor-opencode-provider-0.7.3.tgz", }, repository: { type: "git", - url: "git+https://github.com/PoolPirate/opencode-cursor.git", + url: "git+https://github.com/oakimov/cursor-opencode-provider.git", }, }, }, }); - expect(latest.packageName).toBe("@playwo/opencode-cursor-oauth"); - expect(latest.repo).toBe("PoolPirate/opencode-cursor"); - expect(latest.npmUrl).toBe( - "https://www.npmjs.com/package/%40playwo/opencode-cursor-oauth/v/0.4.3", - ); + expect(latest.packageName).toBe("cursor-opencode-provider"); + expect(latest.repo).toBe("oakimov/cursor-opencode-provider"); + expect(latest.version).toBe("0.7.3"); + expect(latest.npmUrl).toBe("https://www.npmjs.com/package/cursor-opencode-provider/v/0.7.3"); }); it("rejects missing repository metadata without an explicit spec exception", () => { - const spec = getUpstreamPluginSpec("opencode-cursor-oauth"); + const spec = getUpstreamPluginSpec("cursor-opencode-provider"); expect(spec).toBeTruthy(); if (!spec) return; expect(() => normalizeLatestPublishedPluginVersion(spec, { - "dist-tags": { latest: "0.4.3" }, - time: { "0.4.3": "2026-04-08T14:04:58.057Z" }, + "dist-tags": { latest: "0.7.3" }, + time: { "0.7.3": "2026-09-20T10:00:00.000Z" }, versions: { - "0.4.3": { + "0.7.3": { dist: { tarball: - "https://example.test/@playwo/opencode-cursor-oauth/-/opencode-cursor-oauth-0.4.3.tgz", + "https://registry.npmjs.org/cursor-opencode-provider/-/cursor-opencode-provider-0.7.3.tgz", }, }, }, @@ -62,13 +61,70 @@ describe("upstream-plugin-registry", () => { ).toThrow("is missing GitHub repository metadata"); }); + it("tracks the AGY alpha dist-tag, not latest", () => { + const spec = getUpstreamPluginSpec("opencode-agy-auth"); + expect(spec).toBeTruthy(); + if (!spec) return; + + const latest = normalizeLatestPublishedPluginVersion(spec, { + "dist-tags": { alpha: "1.2.11-alpha.0", latest: "1.2.11" }, + time: { + "1.2.11": "2026-09-25T06:33:34.440Z", + "1.2.11-alpha.0": "2026-09-25T06:34:38.610Z", + }, + versions: { + "1.2.11": { + dist: { + tarball: + "https://registry.npmjs.org/@anthonyhaussman/opencode-agy-auth/-/opencode-agy-auth-1.2.11.tgz", + }, + }, + "1.2.11-alpha.0": { + dist: { + tarball: + "https://registry.npmjs.org/@anthonyhaussman/opencode-agy-auth/-/opencode-agy-auth-1.2.11-alpha.0.tgz", + }, + }, + }, + }); + + expect(latest.version).toBe("1.2.11-alpha.0"); + expect(latest.tarballUrl).toBe( + "https://registry.npmjs.org/@anthonyhaussman/opencode-agy-auth/-/opencode-agy-auth-1.2.11-alpha.0.tgz", + ); + expect(latest.npmUrl).toBe( + "https://www.npmjs.com/package/%40anthonyhaussman/opencode-agy-auth/v/1.2.11-alpha.0", + ); + }); + + it("rejects a packument without the tracked dist-tag", () => { + const spec = getUpstreamPluginSpec("opencode-agy-auth"); + expect(spec).toBeTruthy(); + if (!spec) return; + + expect(() => + normalizeLatestPublishedPluginVersion(spec, { + "dist-tags": { latest: "1.2.11" }, + time: { "1.2.11": "2026-09-25T06:33:34.440Z" }, + versions: { + "1.2.11": { + dist: { + tarball: + "https://registry.npmjs.org/@anthonyhaussman/opencode-agy-auth/-/opencode-agy-auth-1.2.11.tgz", + }, + }, + }, + }), + ).toThrow("is missing the alpha dist-tag"); + }); + it("builds canonical npm metadata for scoped AGY when npm omits repository metadata", () => { const spec = getUpstreamPluginSpec("opencode-agy-auth"); expect(spec).toBeTruthy(); if (!spec) return; const latest = normalizeLatestPublishedPluginVersion(spec, { - "dist-tags": { latest: "1.1.4" }, + "dist-tags": { alpha: "1.1.4" }, time: { "1.1.4": "2026-07-18T08:36:49.202Z" }, versions: { "1.1.4": { @@ -94,7 +150,7 @@ describe("upstream-plugin-registry", () => { expect(() => normalizeLatestPublishedPluginVersion(spec, { - "dist-tags": { latest: "1.1.4" }, + "dist-tags": { alpha: "1.1.4" }, repository: "https://gitlab.com/anthonyhaussman/opencode-agy-auth", time: { "1.1.4": "2026-07-18T08:36:49.202Z" }, versions: { @@ -116,7 +172,7 @@ describe("upstream-plugin-registry", () => { expect(() => normalizeLatestPublishedPluginVersion(spec, { - "dist-tags": { latest: "1.1.4" }, + "dist-tags": { alpha: "1.1.4" }, repository: "github:someone-else/opencode-agy-auth", time: { "1.1.4": "2026-07-18T08:36:49.202Z" }, versions: { diff --git a/tests/upstream-plugin-review.test.ts b/tests/upstream-plugin-review.test.ts index 0c50b77a..835c8dc6 100644 --- a/tests/upstream-plugin-review.test.ts +++ b/tests/upstream-plugin-review.test.ts @@ -48,29 +48,29 @@ describe("upstream-plugin-review", () => { it("detects same-version metadata-only changes", () => { const previous = { version: "0.4.3", - packageName: "opencode-cursor-oauth", - repo: "old-owner/opencode-cursor", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - npmUrl: "https://www.npmjs.com/package/opencode-cursor-oauth/v/0.4.3", + packageName: "@old-scope/cursor-opencode-provider", + repo: "old-owner/cursor-opencode-provider", + referenceDir: "references/upstream-plugins/cursor-opencode-provider", + npmUrl: "https://www.npmjs.com/package/%40old-scope/cursor-opencode-provider/v/0.4.3", publishedAt: "2026-04-08T14:04:58.057Z", }; const current = { ...previous, - packageName: "@playwo/opencode-cursor-oauth", - repo: "PoolPirate/opencode-cursor", + packageName: "cursor-opencode-provider", + repo: "oakimov/cursor-opencode-provider", }; expect( buildChangedPluginSummaries( - { plugins: { "opencode-cursor-oauth": previous } }, - { plugins: { "opencode-cursor-oauth": current } }, + { plugins: { "cursor-opencode-provider": previous } }, + { plugins: { "cursor-opencode-provider": current } }, ), ).toEqual([ { changeKind: "metadata", changedFields: ["packageName", "repo"], currentVersion: "0.4.3", - pluginId: "opencode-cursor-oauth", + pluginId: "cursor-opencode-provider", previousVersion: "0.4.3", }, ]); @@ -79,7 +79,7 @@ describe("upstream-plugin-review", () => { it("keeps pre-synchronized reference-only changes in the review set", () => { const lock = { plugins: { - "opencode-cursor-oauth": { + "cursor-opencode-provider": { version: "0.4.3", }, }, @@ -91,8 +91,8 @@ describe("upstream-plugin-review", () => { lock, new Map([ [ - "opencode-cursor-oauth", - ["references/upstream-plugins/opencode-cursor-oauth/package.json"], + "cursor-opencode-provider", + ["references/upstream-plugins/cursor-opencode-provider/package.json"], ], ]), [], @@ -102,7 +102,7 @@ describe("upstream-plugin-review", () => { changeKind: "metadata", changedFields: ["reference contents"], currentVersion: "0.4.3", - pluginId: "opencode-cursor-oauth", + pluginId: "cursor-opencode-provider", previousVersion: "0.4.3", }, ]); @@ -116,7 +116,7 @@ describe("upstream-plugin-review", () => { changeKind: "metadata", changedFields: ["packageName"], currentVersion: "0.4.3", - pluginId: "opencode-cursor-oauth", + pluginId: "cursor-opencode-provider", previousVersion: "0.4.3", }, ], @@ -252,7 +252,7 @@ describe("upstream-plugin-review", () => { changeKind: "metadata", changedFields: ["packageName", "repo"], currentVersion: "0.4.3", - pluginId: "opencode-cursor-oauth", + pluginId: "cursor-opencode-provider", previousVersion: "0.4.3", }, ], @@ -265,7 +265,7 @@ describe("upstream-plugin-review", () => { output: "", }, }), - ).toContain("opencode-cursor-oauth: metadata changed at 0.4.3 (packageName, repo)"); + ).toContain("cursor-opencode-provider: metadata changed at 0.4.3 (packageName, repo)"); expect(prompt).toContain("references/upstream-plugins/opencode-gemini-auth/package.json"); expect(prompt).toContain('"version": "1.3.0"'); expect(prompt).toContain("`pnpm test`: passed"); diff --git a/tests/upstream-plugin-sanitization.test.ts b/tests/upstream-plugin-sanitization.test.ts index da77f8cb..eae55ddc 100644 --- a/tests/upstream-plugin-sanitization.test.ts +++ b/tests/upstream-plugin-sanitization.test.ts @@ -64,128 +64,18 @@ async function writeGeminiDistBundle(pluginRoot: string, clientId: string, clien ); } -async function writeCursorSnapshot( - pluginRoot: string, - params: { modelsSource: string; proxySource: string; packageName?: string }, -) { +async function writeCursorSnapshot(pluginRoot: string, pricingSource: string) { const distDir = path.join(pluginRoot, "dist"); await mkdir(distDir, { recursive: true }); - await writeFile(path.join(distDir, "models.js"), params.modelsSource, "utf8"); - await writeFile(path.join(distDir, "proxy.js"), params.proxySource, "utf8"); - - if (params.packageName) { - await writeFile( - path.join(pluginRoot, "package.json"), - JSON.stringify({ name: params.packageName }, null, 2), - "utf8", - ); - } + await writeFile(path.join(distDir, "pricing-data.js"), pricingSource, "utf8"); } -const UNSAFE_CURSOR_MODELS_SOURCE = `let cachedModels = null; -export async function getCursorModels(apiKey) { - if (cachedModels) - return cachedModels; - const discovered = await fetchCursorUsableModels(apiKey); - cachedModels = discovered && discovered.length > 0 ? discovered : FALLBACK_MODELS; - return cachedModels; -} -`; - -const SAFE_CURSOR_MODELS_SOURCE = `let cachedModels = null; -export async function getCursorModels(apiKey) { - if (cachedModels) - return cachedModels; - const discovered = await fetchCursorUsableModels(apiKey); - if (discovered && discovered.length > 0) { - cachedModels = discovered; - return cachedModels; +const CURSOR_PRICING_SOURCE = `export const CURSOR_MODEL_COSTS = { + "claude-opus-5": { + "input": 5, + "output": 25 } - return FALLBACK_MODELS; -} -`; - -const UNSAFE_CURSOR_PROXY_SOURCE = `/** Derive a key for active bridge lookup (tool-call continuations). Model-specific. */ -function deriveBridgeKey(modelId, messages) { - const firstUserMsg = messages.find((m) => m.role === "user"); - const firstUserText = firstUserMsg ? textContent(firstUserMsg.content) : ""; - return createHash("sha256") - .update(\`bridge:\${modelId}:\${firstUserText.slice(0, 200)}\`) - .digest("hex") - .slice(0, 16); -} -/** Derive a key for conversation state. Model-independent so context survives model switches. */ -function deriveConversationKey(messages) { - const firstUserMsg = messages.find((m) => m.role === "user"); - const firstUserText = firstUserMsg ? textContent(firstUserMsg.content) : ""; - return createHash("sha256") - .update(\`conv:\${firstUserText.slice(0, 200)}\`) - .digest("hex") - .slice(0, 16); -} -`; - -const SAFE_CURSOR_PROXY_SOURCE = `function normalizeConversationMessages(messages) { - return messages - .filter((m) => m.role !== "tool") - .map((m) => ({ - role: m.role, - content: textContent(m.content), - })) - .filter((m) => m.content || m.role === "user" || m.role === "system"); -} -/** Derive a key for active bridge lookup (tool-call continuations). Model-specific. */ -function deriveBridgeKey(modelId, messages) { - const normalizedMessages = normalizeConversationMessages(messages); - return createHash("sha256") - .update(JSON.stringify({ - modelId, - messages: normalizedMessages, - })) - .digest("hex") - .slice(0, 16); -} -/** Derive a key for conversation state. Model-independent so context survives model switches. */ -function deriveConversationKey(messages) { - const normalizedMessages = normalizeConversationMessages(messages); - return createHash("sha256") - .update(JSON.stringify({ - messages: normalizedMessages, - })) - .digest("hex") - .slice(0, 16); -} -`; - -const PARTIALLY_SAFE_CURSOR_PROXY_SOURCE = `function normalizeConversationMessages(messages) { - return messages - .filter((m) => m.role !== "tool") - .map((m) => ({ - role: m.role, - content: textContent(m.content), - })) - .filter((m) => m.content || m.role === "user" || m.role === "system"); -} -/** Derive a key for active bridge lookup (tool-call continuations). Model-specific. */ -function deriveBridgeKey(modelId, messages) { - const normalizedMessages = normalizeConversationMessages(messages); - return createHash("sha256") - .update(JSON.stringify({ - modelId, - messages: normalizedMessages, - })) - .digest("hex") - .slice(0, 16); -} -/** Derive a key for conversation state. Model-independent so context survives model switches. */ -function deriveConversationKey(messages) { - const firstUserMsg = messages.find((m) => m.role === "user"); - const firstUserText = firstUserMsg ? textContent(firstUserMsg.content) : ""; - return createHash("sha256") - .update(\`conv:\${firstUserText.slice(0, 200)}\`) - .digest("hex") - .slice(0, 16); -} +}; `; describe("upstream-plugin-sanitization", () => { @@ -333,101 +223,44 @@ describe("upstream-plugin-sanitization", () => { expect(constantsSource).toContain("'REDACTED_GOOGLE_OAUTH_CLIENT_SECRET'"); }); - it("rewrites unsafe Cursor OAuth snapshot guards into the safe local form", async () => { + it("leaves a credential-free Cursor snapshot unchanged", async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), "opencode-quota-sanitize-")); tempRoots.push(tempRoot); - await writeCursorSnapshot(tempRoot, { - modelsSource: UNSAFE_CURSOR_MODELS_SOURCE, - proxySource: UNSAFE_CURSOR_PROXY_SOURCE, - }); + await writeCursorSnapshot(tempRoot, CURSOR_PRICING_SOURCE); - await sanitizeUpstreamPluginSnapshot("opencode-cursor-oauth", tempRoot); + await sanitizeUpstreamPluginSnapshot("cursor-opencode-provider", tempRoot); - const modelsSource = await readFile(path.join(tempRoot, "dist", "models.js"), "utf8"); - expect(modelsSource).toContain("if (discovered && discovered.length > 0) {"); - expect(modelsSource).toContain("cachedModels = discovered;"); - expect(modelsSource).toContain("return FALLBACK_MODELS;"); - expect(modelsSource).not.toContain( - "cachedModels = discovered && discovered.length > 0 ? discovered : FALLBACK_MODELS;", + await expect(readFile(path.join(tempRoot, "dist", "pricing-data.js"), "utf8")).resolves.toBe( + CURSOR_PRICING_SOURCE, ); - - const proxySource = await readFile(path.join(tempRoot, "dist", "proxy.js"), "utf8"); - expect(proxySource).toContain('.filter((m) => m.role !== "tool")'); - expect(proxySource).toContain("messages: normalizedMessages"); - expect(proxySource).not.toContain( - 'const firstUserMsg = messages.find((m) => m.role === "user");', - ); - expect(proxySource).not.toContain("firstUserText.slice(0, 200)"); }); - it("sanitizes the tracked Cursor snapshot even when the published package name is scoped", async () => { + it("fails closed when a Cursor API key lands in the snapshot", async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), "opencode-quota-sanitize-")); tempRoots.push(tempRoot); - await writeCursorSnapshot(tempRoot, { - modelsSource: UNSAFE_CURSOR_MODELS_SOURCE, - packageName: "@playwo/opencode-cursor-oauth", - proxySource: UNSAFE_CURSOR_PROXY_SOURCE, - }); - - await sanitizeUpstreamPluginSnapshot("opencode-cursor-oauth", tempRoot); - - await expect(readFile(path.join(tempRoot, "dist", "models.js"), "utf8")).resolves.toContain( - "if (discovered && discovered.length > 0) {", - ); - await expect(readFile(path.join(tempRoot, "dist", "proxy.js"), "utf8")).resolves.toContain( - "messages: normalizedMessages", + await writeCursorSnapshot( + tempRoot, + `${CURSOR_PRICING_SOURCE}const KEY = "crsr_${"a1".repeat(16)}";\n`, ); - }); - - it("leaves already-safe Cursor OAuth snapshot guards unchanged", async () => { - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "opencode-quota-sanitize-")); - tempRoots.push(tempRoot); - - await writeCursorSnapshot(tempRoot, { - modelsSource: SAFE_CURSOR_MODELS_SOURCE, - proxySource: SAFE_CURSOR_PROXY_SOURCE, - }); - - await sanitizeUpstreamPluginSnapshot("opencode-cursor-oauth", tempRoot); - await expect(readFile(path.join(tempRoot, "dist", "models.js"), "utf8")).resolves.toBe( - SAFE_CURSOR_MODELS_SOURCE, - ); - await expect(readFile(path.join(tempRoot, "dist", "proxy.js"), "utf8")).resolves.toBe( - SAFE_CURSOR_PROXY_SOURCE, - ); + await expect( + sanitizeUpstreamPluginSnapshot("cursor-opencode-provider", tempRoot), + ).rejects.toThrow("Found unsanitized Cursor credential"); }); - it("fails closed when a Cursor OAuth guard shape changes unexpectedly", async () => { + it("fails closed when a Cursor access token lands in the snapshot", async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), "opencode-quota-sanitize-")); tempRoots.push(tempRoot); - await writeCursorSnapshot(tempRoot, { - modelsSource: SAFE_CURSOR_MODELS_SOURCE, - proxySource: `function deriveBridgeKey(modelId, messages) { - return modelId + ":" + messages.length; -} -`, - }); - - await expect(sanitizeUpstreamPluginSnapshot("opencode-cursor-oauth", tempRoot)).rejects.toThrow( - "Expected CURSOR_TRANSCRIPT_BRIDGE_KEY", + await writeCursorSnapshot( + tempRoot, + `${CURSOR_PRICING_SOURCE}const TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXVzZXIifQ.sig";\n`, ); - }); - it("fails closed when only one Cursor proxy key derivation is sanitized", async () => { - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "opencode-quota-sanitize-")); - tempRoots.push(tempRoot); - - await writeCursorSnapshot(tempRoot, { - modelsSource: SAFE_CURSOR_MODELS_SOURCE, - proxySource: PARTIALLY_SAFE_CURSOR_PROXY_SOURCE, - }); - - await expect(sanitizeUpstreamPluginSnapshot("opencode-cursor-oauth", tempRoot)).rejects.toThrow( - "Expected CURSOR_TRANSCRIPT_BRIDGE_KEY", - ); + await expect( + sanitizeUpstreamPluginSnapshot("cursor-opencode-provider", tempRoot), + ).rejects.toThrow("Found unsanitized Cursor credential"); }); }); diff --git a/tests/upstream-plugin-specs.test.ts b/tests/upstream-plugin-specs.test.ts index bd49c1c1..e3ff8ca6 100644 --- a/tests/upstream-plugin-specs.test.ts +++ b/tests/upstream-plugin-specs.test.ts @@ -5,19 +5,20 @@ import { UPSTREAM_PLUGIN_REFERENCE_ROOT, UPSTREAM_PLUGIN_SPECS, } from "../scripts/lib/upstream-plugin-specs.mjs"; +import { CURSOR_CANONICAL_PLUGIN_PACKAGE } from "../src/lib/cursor-detection.js"; describe("upstream-plugin-specs", () => { it("tracks the expected upstream plugin ids", () => { expect(UPSTREAM_PLUGIN_SPECS.map((spec) => spec.pluginId)).toEqual([ - "opencode-cursor-oauth", + "cursor-opencode-provider", "opencode-gemini-auth", "opencode-agy-auth", ]); }); it("builds the expected check issue titles", () => { - expect(getUpstreamPluginIssueTitle("opencode-cursor-oauth")).toBe( - "[check] opencode-cursor-oauth had update", + expect(getUpstreamPluginIssueTitle("cursor-opencode-provider")).toBe( + "[check] cursor-opencode-provider had update", ); }); @@ -29,6 +30,7 @@ describe("upstream-plugin-specs", () => { it("tracks the Gemini CLI auth companion package and repo", () => { expect(getUpstreamPluginSpec("opencode-gemini-auth")).toMatchObject({ + distTag: "latest", packageName: "opencode-gemini-auth", pluginId: "opencode-gemini-auth", referenceDir: `${UPSTREAM_PLUGIN_REFERENCE_ROOT}/opencode-gemini-auth`, @@ -36,17 +38,19 @@ describe("upstream-plugin-specs", () => { }); }); - it("keeps the cursor internal plugin id stable while pointing at the canonical package and repo", () => { - expect(getUpstreamPluginSpec("opencode-cursor-oauth")).toMatchObject({ - packageName: "@playwo/opencode-cursor-oauth", - pluginId: "opencode-cursor-oauth", - referenceDir: `${UPSTREAM_PLUGIN_REFERENCE_ROOT}/opencode-cursor-oauth`, - repo: "PoolPirate/opencode-cursor", + it("tracks the cursor-opencode-provider Cursor companion package and repo", () => { + expect(getUpstreamPluginSpec("cursor-opencode-provider")).toMatchObject({ + distTag: "latest", + packageName: "cursor-opencode-provider", + pluginId: "cursor-opencode-provider", + referenceDir: `${UPSTREAM_PLUGIN_REFERENCE_ROOT}/cursor-opencode-provider`, + repo: "oakimov/cursor-opencode-provider", }); }); it("tracks the scoped Google AGY companion under a stable internal id", () => { expect(getUpstreamPluginSpec("opencode-agy-auth")).toMatchObject({ + distTag: "alpha", packageName: "@anthonyhaussman/opencode-agy-auth", pluginId: "opencode-agy-auth", referenceDir: `${UPSTREAM_PLUGIN_REFERENCE_ROOT}/opencode-agy-auth`, @@ -65,4 +69,10 @@ describe("upstream-plugin-specs", () => { ), ).toEqual(["opencode-agy-auth"]); }); + + it("keeps the runtime Cursor package name aligned with the upstream spec", () => { + expect(getUpstreamPluginSpec("cursor-opencode-provider")?.packageName).toBe( + CURSOR_CANONICAL_PLUGIN_PACKAGE, + ); + }); }); diff --git a/tests/upstream-plugin-sync.test.ts b/tests/upstream-plugin-sync.test.ts index e0c8affc..10da3876 100644 --- a/tests/upstream-plugin-sync.test.ts +++ b/tests/upstream-plugin-sync.test.ts @@ -78,12 +78,12 @@ async function seedReferenceRoot(repoRoot: string) { `${JSON.stringify( { plugins: { - "opencode-cursor-oauth": { - npmUrl: "https://www.npmjs.com/package/%40playwo/opencode-cursor-oauth/v/1.0.0", - packageName: "@playwo/opencode-cursor-oauth", + "cursor-opencode-provider": { + npmUrl: "https://www.npmjs.com/package/cursor-opencode-provider/v/1.0.0", + packageName: "cursor-opencode-provider", publishedAt: "2026-03-01T00:00:00.000Z", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - repo: "PoolPirate/opencode-cursor", + referenceDir: "references/upstream-plugins/cursor-opencode-provider", + repo: "oakimov/cursor-opencode-provider", version: "1.0.0", }, "opencode-gemini-auth": { @@ -110,7 +110,11 @@ async function seedReferenceRoot(repoRoot: string) { "utf8", ); - for (const pluginId of ["opencode-cursor-oauth", "opencode-gemini-auth", "opencode-agy-auth"]) { + for (const pluginId of [ + "cursor-opencode-provider", + "opencode-gemini-auth", + "opencode-agy-auth", + ]) { const pluginDir = path.join(referenceRoot, pluginId); await mkdir(pluginDir, { recursive: true }); await writeFile( @@ -131,16 +135,16 @@ describe("upstream-plugin-sync", () => { testState.failPluginId = null; testState.latestByPluginId = new Map([ [ - "opencode-cursor-oauth", + "cursor-opencode-provider", { - npmUrl: "https://www.npmjs.com/package/%40playwo/opencode-cursor-oauth/v/2.0.0", - packageName: "@playwo/opencode-cursor-oauth", - pluginId: "opencode-cursor-oauth", + npmUrl: "https://www.npmjs.com/package/cursor-opencode-provider/v/2.0.0", + packageName: "cursor-opencode-provider", + pluginId: "cursor-opencode-provider", publishedAt: "2026-03-20T00:00:00.000Z", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - repo: "PoolPirate/opencode-cursor", + referenceDir: "references/upstream-plugins/cursor-opencode-provider", + repo: "oakimov/cursor-opencode-provider", tarballUrl: - "https://example.test/@playwo/opencode-cursor-oauth/-/opencode-cursor-oauth-2.0.0.tgz", + "https://example.test/cursor-opencode-provider/-/cursor-opencode-provider-2.0.0.tgz", version: "2.0.0", }, ], @@ -201,14 +205,14 @@ describe("upstream-plugin-sync", () => { readFile(path.join(referenceRoot, "stale-plugin", "package.json"), "utf8"), ).rejects.toThrow(); await expect( - readFile(path.join(referenceRoot, "opencode-cursor-oauth", "package.json"), "utf8"), - ).resolves.toContain('"name": "@playwo/opencode-cursor-oauth"'); + readFile(path.join(referenceRoot, "cursor-opencode-provider", "package.json"), "utf8"), + ).resolves.toContain('"name": "cursor-opencode-provider"'); await expect( - readFile(path.join(referenceRoot, "opencode-cursor-oauth", "dist", "models.js"), "utf8"), - ).resolves.toContain("if (discovered && discovered.length > 0) {"); - await expect( - readFile(path.join(referenceRoot, "opencode-cursor-oauth", "dist", "proxy.js"), "utf8"), - ).resolves.toContain("messages: normalizedMessages"); + readFile( + path.join(referenceRoot, "cursor-opencode-provider", "dist", "pricing-data.js"), + "utf8", + ), + ).resolves.toContain("export const CURSOR_MODEL_COSTS = {"); await expect( readFile(path.join(referenceRoot, "opencode-gemini-auth", "dist", "index.js"), "utf8"), ).resolves.toContain("REDACTED_GOOGLE_OAUTH_CLIENT_SECRET"); @@ -222,10 +226,10 @@ describe("upstream-plugin-sync", () => { '"version": "2.0.0"', ); await expect(readFile(path.join(referenceRoot, "lock.json"), "utf8")).resolves.toContain( - '"packageName": "@playwo/opencode-cursor-oauth"', + '"packageName": "cursor-opencode-provider"', ); await expect(readFile(path.join(referenceRoot, "lock.json"), "utf8")).resolves.toContain( - '"repo": "PoolPirate/opencode-cursor"', + '"repo": "oakimov/cursor-opencode-provider"', ); }); @@ -264,24 +268,31 @@ describe("upstream-plugin-sync", () => { }); it("redownloads same-version plugins when tracked identity changes", async () => { - const cursorLatest = testState.latestByPluginId.get("opencode-cursor-oauth"); - testState.latestByPluginId.set("opencode-cursor-oauth", { ...cursorLatest, version: "1.0.0" }); + const cursorLatest = testState.latestByPluginId.get("cursor-opencode-provider"); + testState.latestByPluginId.set("cursor-opencode-provider", { + ...cursorLatest, + version: "1.0.0", + }); const referenceRoot = path.join(testState.repoRoot, "references", "upstream-plugins"); const lockPath = path.join(referenceRoot, "lock.json"); const lock = JSON.parse(await readFile(lockPath, "utf8")); - lock.plugins["opencode-cursor-oauth"] = { - npmUrl: "https://www.npmjs.com/package/opencode-cursor-oauth/v/1.0.0", - packageName: "opencode-cursor-oauth", + lock.plugins["cursor-opencode-provider"] = { + npmUrl: "https://www.npmjs.com/package/%40old-scope/cursor-opencode-provider/v/1.0.0", + packageName: "@old-scope/cursor-opencode-provider", publishedAt: "2026-03-01T00:00:00.000Z", - referenceDir: "references/upstream-plugins/opencode-cursor-oauth", - repo: "ephraimduncan/opencode-cursor", + referenceDir: "references/upstream-plugins/cursor-opencode-provider", + repo: "old-owner/cursor-opencode-provider", version: "1.0.0", }; await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8"); await writeFile( - path.join(referenceRoot, "opencode-cursor-oauth", "package.json"), - JSON.stringify({ name: "opencode-cursor-oauth", stale: true, version: "1.0.0" }), + path.join(referenceRoot, "cursor-opencode-provider", "package.json"), + JSON.stringify({ + name: "@old-scope/cursor-opencode-provider", + stale: true, + version: "1.0.0", + }), "utf8", ); @@ -291,18 +302,18 @@ describe("upstream-plugin-sync", () => { await syncUpstreamPluginReferences(); const packageJson = await readFile( - path.join(referenceRoot, "opencode-cursor-oauth", "package.json"), + path.join(referenceRoot, "cursor-opencode-provider", "package.json"), "utf8", ); - expect(packageJson).toContain('"name": "@playwo/opencode-cursor-oauth"'); + expect(packageJson).toContain('"name": "cursor-opencode-provider"'); expect(packageJson).not.toContain('"stale":true'); await expect(readFile(lockPath, "utf8")).resolves.toContain( - '"packageName": "@playwo/opencode-cursor-oauth"', + '"packageName": "cursor-opencode-provider"', ); }); it("leaves the committed reference tree untouched when staging fails", async () => { - testState.failPluginId = "opencode-cursor-oauth"; + testState.failPluginId = "cursor-opencode-provider"; const { syncUpstreamPluginReferences } = await import( "../scripts/lib/upstream-plugin-sync.mjs" @@ -318,7 +329,7 @@ describe("upstream-plugin-sync", () => { readFile(path.join(referenceRoot, "stale-plugin", "package.json"), "utf8"), ).resolves.toContain('"stale":true'); await expect( - readFile(path.join(referenceRoot, "opencode-cursor-oauth", "package.json"), "utf8"), + readFile(path.join(referenceRoot, "cursor-opencode-provider", "package.json"), "utf8"), ).resolves.toContain('"version":"1.0.0"'); await expect(readFile(path.join(referenceRoot, "lock.json"), "utf8")).resolves.toContain( '"version": "1.0.0"', diff --git a/tests/upstream.cursor-oauth.reference.test.ts b/tests/upstream.cursor-oauth.reference.test.ts deleted file mode 100644 index 94587b92..00000000 --- a/tests/upstream.cursor-oauth.reference.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { readFileSync } from "node:fs"; - -import { describe, expect, it } from "vitest"; - -const CURSOR_MODELS_PATH = new URL( - "../references/upstream-plugins/opencode-cursor-oauth/dist/models.js", - import.meta.url, -); - -const CURSOR_PROXY_PATH = new URL( - "../references/upstream-plugins/opencode-cursor-oauth/dist/proxy.js", - import.meta.url, -); - -const CURSOR_PACKAGE_PATH = new URL( - "../references/upstream-plugins/opencode-cursor-oauth/package.json", - import.meta.url, -); - -const UPSTREAM_LOCK_PATH = new URL("../references/upstream-plugins/lock.json", import.meta.url); - -describe("synced Cursor OAuth reference guards", () => { - it("keeps the internal reference path stable while the published package metadata is canonical", () => { - const pkg = JSON.parse(readFileSync(CURSOR_PACKAGE_PATH, "utf8")); - - expect(pkg.name).toBe("@playwo/opencode-cursor-oauth"); - expect(pkg.repository?.url).toContain("PoolPirate/opencode-cursor"); - }); - - it("keeps the lock entry aligned with the committed Cursor package metadata", () => { - const lock = JSON.parse(readFileSync(UPSTREAM_LOCK_PATH, "utf8")); - const pkg = JSON.parse(readFileSync(CURSOR_PACKAGE_PATH, "utf8")); - const cursor = lock.plugins?.["opencode-cursor-oauth"]; - - expect(cursor).toBeTruthy(); - expect(cursor.packageName).toBe(pkg.name); - expect(cursor.referenceDir).toBe("references/upstream-plugins/opencode-cursor-oauth"); - expect(cursor.version).toBe(pkg.version); - }); - - it("does not memoize fallback models after discovery failure", () => { - const source = readFileSync(CURSOR_MODELS_PATH, "utf8"); - - expect(source).toContain("if (discovered && discovered.length > 0) {"); - expect(source).toContain("cachedModels = discovered;"); - expect(source).toContain("return FALLBACK_MODELS;"); - expect(source).not.toContain( - "cachedModels = discovered && discovered.length > 0 ? discovered : FALLBACK_MODELS;", - ); - }); - - it("keys conversation reuse from the normalized transcript, not the first prompt alone", () => { - const source = readFileSync(CURSOR_PROXY_PATH, "utf8"); - - expect(source).toContain('.filter((m) => m.role !== "tool")'); - expect(source).toContain("messages: normalizedMessages"); - expect(source).not.toContain('const firstUserMsg = messages.find((m) => m.role === "user");'); - expect(source).not.toContain("firstUserText.slice(0, 200)"); - }); -}); diff --git a/tests/upstream.cursor-opencode-provider.reference.test.ts b/tests/upstream.cursor-opencode-provider.reference.test.ts new file mode 100644 index 00000000..b69c714e --- /dev/null +++ b/tests/upstream.cursor-opencode-provider.reference.test.ts @@ -0,0 +1,54 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const CURSOR_REFERENCE_ROOT = new URL( + "../references/upstream-plugins/cursor-opencode-provider/", + import.meta.url, +); + +const UPSTREAM_LOCK_PATH = new URL("../references/upstream-plugins/lock.json", import.meta.url); + +function readCursorReference(relativePath: string): string { + return readFileSync(new URL(relativePath, CURSOR_REFERENCE_ROOT), "utf8"); +} + +describe("synced cursor-opencode-provider reference guards", () => { + it("tracks the published package and its OpenCode 2 plugin entries", () => { + const pkg = JSON.parse(readCursorReference("package.json")); + + expect(pkg.name).toBe("cursor-opencode-provider"); + expect(pkg.repository?.url).toContain("oakimov/cursor-opencode-provider"); + // Cursor detection matches these two OpenCode 2 entries. + expect(pkg.exports?.["./plugin/opencode2"]?.import).toBe("./dist/plugin-opencode2.js"); + expect(pkg.exports?.["./server"]?.import).toBe("./dist/plugin-opencode2.js"); + }); + + it("keeps the lock entry aligned with the synced package metadata", () => { + const lock = JSON.parse(readFileSync(UPSTREAM_LOCK_PATH, "utf8")); + const pkg = JSON.parse(readCursorReference("package.json")); + const cursor = lock.plugins?.["cursor-opencode-provider"]; + + expect(cursor).toBeTruthy(); + expect(cursor.packageName).toBe(pkg.name); + expect(cursor.referenceDir).toBe("references/upstream-plugins/cursor-opencode-provider"); + expect(cursor.version).toBe(pkg.version); + }); + + it("stores OpenCode 2 Cursor credentials under the cursor integration", () => { + expect(readCursorReference("dist/opencode2/catalog.d.ts")).toContain( + 'export declare const CURSOR_INTEGRATION_ID = "cursor";', + ); + + const integration = readCursorReference("dist/opencode2/integration.js"); + expect(integration).toContain('export const CURSOR_OAUTH_METHOD_ID = "oauth";'); + expect(integration).toContain('export const CURSOR_ENV_NAMES = ["CURSOR_API_KEY"];'); + expect(integration).toContain('method: { type: "key"'); + }); + + it("publishes Cursor model prices in pricing-data.js", () => { + expect(readCursorReference("dist/pricing-data.js")).toContain( + "export const CURSOR_MODEL_COSTS = {", + ); + }); +}); From a21114d3b64ce20dd146c063cc9617ae9fd8ec11 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:32 +0200 Subject: [PATCH 56/97] docs(readme): friendlier upvote tip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f79347f1..415f4948 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

> [!TIP] -> Help upstream: a 👍 on [opencode#38281](https://github.com/anomalyco/opencode/issues/38281) (ecosystem listing) and [#43132](https://github.com/anomalyco/opencode/issues/43132) (Web/Desktop plugin panels) helps. Please react; don't comment "+1". +> **Like this plugin?** A 👍 on [opencode#38281](https://github.com/anomalyco/opencode/issues/38281) helps get it listed, and [#43132](https://github.com/anomalyco/opencode/issues/43132) asks for Web/Desktop panels. [![OpenCode Quota sidebar](https://shawnkiser.com/opencode-quota/opencode-quota-sidebar.webp)](https://github.com/slkiser/opencode-quota) From b4f4d10e6c0ba709777bc5ae801b101aaf47e83e Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:32 +0200 Subject: [PATCH 57/97] fix(google): read projectId from OpenCode 2 provider settings Gemini CLI and Google AGY read the configured projectId through client.config.get(), which the OpenCode 2 server plugin and TUI answer with an empty config, and only from the OpenCode 1 shape provider..options. Read it instead from trusted user/global opencode.jsonc/opencode.json via getProviderConfigSettings, so native providers..settings.projectId works and a native entry replaces the legacy one, as in OpenCode 2's normalizer. Project-local config is not read. The unused config-client parameter is removed from the Google helpers. Order is unchanged: OPENCODE__PROJECT_ID, then config, then GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_PROJECT_ID; a projectId in the saved login still wins over all of these. --- docs/readme/providers.md | 6 +- src/lib/api-key-resolver.ts | 16 ++++++ src/lib/google-agy.ts | 44 ++++---------- src/lib/google-gemini-cli.ts | 46 ++++----------- src/providers/cache-policies.ts | 4 +- src/providers/google-agy.ts | 8 +-- src/providers/google-gemini-cli.ts | 8 +-- tests/lib.google-agy.test.ts | 65 ++++++++++++--------- tests/lib.google-gemini-cli.test.ts | 70 ++++++++++++++--------- tests/providers.google-agy.test.ts | 4 +- tests/providers.google-gemini-cli.test.ts | 4 +- 11 files changed, 137 insertions(+), 138 deletions(-) diff --git a/docs/readme/providers.md b/docs/readme/providers.md index 7ac63509..ff73755d 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -465,13 +465,13 @@ If you use manual provider selection, include `google-agy` in `enabledProviders` } ``` -If the AGY auth entry does not include a project id, set `OPENCODE_AGY_PROJECT_ID` or `provider.google-agy.options.projectId`. +If the AGY auth entry does not include a project id, set `OPENCODE_AGY_PROJECT_ID` or `projectId` in your user/global `opencode.json` (the OpenCode 1 form `provider.google-agy.options.projectId` also works): ```jsonc { - "provider": { + "providers": { "google-agy": { - "options": { + "settings": { "projectId": "your-google-cloud-project", }, }, diff --git a/src/lib/api-key-resolver.ts b/src/lib/api-key-resolver.ts index 55b90538..dccfa1b6 100644 --- a/src/lib/api-key-resolver.ts +++ b/src/lib/api-key-resolver.ts @@ -122,6 +122,22 @@ export function getProviderConfigSettings( return asRecord(asRecord(asRecord(root?.provider)?.[providerKey])?.options); } +/** + * Read one provider string setting from trusted user/global OpenCode config. + * The first candidate (`opencode.jsonc`, then `opencode.json`) that sets it wins. + */ +export async function readGlobalProviderConfigString( + providerKey: string, + settingKey: string, +): Promise { + for (const candidate of getGlobalOpencodeConfigCandidatePaths()) { + const result = await readOpencodeConfig(candidate.path, candidate.isJsonc); + const value = getProviderConfigSettings(result?.config, providerKey)?.[settingKey]; + if (typeof value === "string" && value.trim().length > 0) return value.trim(); + } + return undefined; +} + export function extractProviderOptionsApiKey( config: unknown, params: { diff --git a/src/lib/google-agy.ts b/src/lib/google-agy.ts index 9d0d896d..df1d8bd4 100644 --- a/src/lib/google-agy.ts +++ b/src/lib/google-agy.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import { readGlobalProviderConfigString } from "./api-key-resolver.js"; import { type AgyConfiguredCredentials, clearAgyCompanionCacheForTests, @@ -85,12 +86,6 @@ export type AgyAuthPresence = error: string; }; -type ConfigClient = { - config?: { - get?: () => Promise<{ data?: unknown }>; - }; -}; - function normalizeString(value: unknown): string | undefined { if (typeof value !== "string") { return undefined; @@ -167,27 +162,15 @@ export function resolveAgyAccounts( return accounts; } -export async function resolveAgyConfiguredProjectId( - client?: ConfigClient, -): Promise { +export async function resolveAgyConfiguredProjectId(): Promise { const explicitEnvProjectId = normalizeString(process.env.OPENCODE_AGY_PROJECT_ID); if (explicitEnvProjectId) { return explicitEnvProjectId; } - if (client?.config?.get) { - try { - const result = await client.config.get(); - const data = result?.data as { - provider?: Record }>; - }; - const configProjectId = normalizeString(data?.provider?.["google-agy"]?.options?.projectId); - if (configProjectId) { - return configProjectId; - } - } catch { - // ignore and fall back - } + const configProjectId = await readGlobalProviderConfigString("google-agy", "projectId"); + if (configProjectId) { + return configProjectId; } return ( @@ -196,12 +179,10 @@ export async function resolveAgyConfiguredProjectId( ); } -export async function resolveGoogleAgyAuthIdentity( - client?: ConfigClient, -): Promise { +export async function resolveGoogleAgyAuthIdentity(): Promise { const [auth, configuredProjectId, credentials] = await Promise.all([ readAuthFileCached({ maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS }), - resolveAgyConfiguredProjectId(client), + resolveAgyConfiguredProjectId(), resolveAgyClientCredentials(), ]); const accounts = resolveAgyAccounts(auth, configuredProjectId); @@ -231,10 +212,10 @@ export async function resolveGoogleAgyAuthIdentity( }); } -export async function inspectAgyAuthPresence(client?: ConfigClient): Promise { +export async function inspectAgyAuthPresence(): Promise { const [auth, configuredProjectId] = await Promise.all([ readAuthFileCached({ maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS }), - resolveAgyConfiguredProjectId(client), + resolveAgyConfiguredProjectId(), ]); let accountCount = 0; @@ -273,9 +254,9 @@ export async function inspectAgyAuthPresence(client?: ConfigClient): Promise { +export async function hasAgyQuotaRuntimeAvailable(): Promise { const [authPresence, companionPresence] = await Promise.all([ - inspectAgyAuthPresence(client), + inspectAgyAuthPresence(), inspectAgyCompanionPresence(), ]); @@ -713,12 +694,11 @@ async function fetchAccountQuota(params: { } export async function queryGoogleAgyQuota( - client?: ConfigClient, options: { requestTimeoutMs?: number; authData?: AuthData } = {}, ): Promise { const [auth, configuredProjectId] = await Promise.all([ options.authData ?? readAuthFileCached({ maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS }), - resolveAgyConfiguredProjectId(client), + resolveAgyConfiguredProjectId(), ]); const accounts = resolveAgyAccounts(auth, configuredProjectId); if (accounts.length === 0) { diff --git a/src/lib/google-gemini-cli.ts b/src/lib/google-gemini-cli.ts index 73695f08..3edd3245 100644 --- a/src/lib/google-gemini-cli.ts +++ b/src/lib/google-gemini-cli.ts @@ -1,3 +1,4 @@ +import { readGlobalProviderConfigString } from "./api-key-resolver.js"; import { clearGeminiCliCompanionCacheForTests as clearGeminiCliCompanionResolutionCacheForTests, type GeminiCliConfiguredCredentials, @@ -91,12 +92,6 @@ type RetrieveUserQuotaResponse = { buckets?: RetrieveUserQuotaBucket[]; }; -type ConfigClient = { - config?: { - get?: () => Promise<{ data?: unknown }>; - }; -}; - function normalizeString(value: unknown): string | undefined { if (typeof value !== "string") { return undefined; @@ -218,27 +213,15 @@ function getCompanionQuotaError(state: "missing" | "invalid"): string { : "Installed opencode-gemini-auth package is incompatible"; } -export async function resolveGeminiCliConfiguredProjectId( - client?: ConfigClient, -): Promise { +export async function resolveGeminiCliConfiguredProjectId(): Promise { const explicitEnvProjectId = normalizeString(process.env.OPENCODE_GEMINI_PROJECT_ID); if (explicitEnvProjectId) { return explicitEnvProjectId; } - if (client?.config?.get) { - try { - const result = await client.config.get(); - const data = result?.data as { - provider?: Record }>; - }; - const configProjectId = normalizeString(data?.provider?.google?.options?.projectId); - if (configProjectId) { - return configProjectId; - } - } catch { - // ignore and fall back to generic Google project env vars below - } + const configProjectId = await readGlobalProviderConfigString("google", "projectId"); + if (configProjectId) { + return configProjectId; } return ( @@ -247,12 +230,10 @@ export async function resolveGeminiCliConfiguredProjectId( ); } -export async function inspectGeminiCliAuthPresence( - client?: ConfigClient, -): Promise { +export async function inspectGeminiCliAuthPresence(): Promise { const [auth, configuredProjectId] = await Promise.all([ readAuthFileCached({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS }), - resolveGeminiCliConfiguredProjectId(client), + resolveGeminiCliConfiguredProjectId(), ]); const accountCount = countGeminiCliAuthEntries(auth); if (accountCount === 0) { @@ -279,9 +260,9 @@ export async function inspectGeminiCliAuthPresence( }; } -export async function hasGeminiCliQuotaRuntimeAvailable(client?: ConfigClient): Promise { +export async function hasGeminiCliQuotaRuntimeAvailable(): Promise { const [authPresence, companionPresence] = await Promise.all([ - inspectGeminiCliAuthPresence(client), + inspectGeminiCliAuthPresence(), inspectGeminiCliCompanionPresence(), ]); @@ -292,12 +273,10 @@ export async function hasGeminiCliQuotaRuntimeAvailable(client?: ConfigClient): ); } -export async function resolveGeminiCliAuthIdentity( - client?: ConfigClient, -): Promise { +export async function resolveGeminiCliAuthIdentity(): Promise { const [auth, configuredProjectId, credentials] = await Promise.all([ readAuthFileCached({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS }), - resolveGeminiCliConfiguredProjectId(client), + resolveGeminiCliConfiguredProjectId(), resolveGeminiCliClientCredentials(), ]); const accounts = resolveGeminiCliAccounts(auth, configuredProjectId); @@ -633,12 +612,11 @@ async function fetchAccountQuota(params: { } export async function queryGeminiCliQuota( - client?: ConfigClient, options: { requestTimeoutMs?: number; authData?: AuthData } = {}, ): Promise { const [auth, configuredProjectId] = await Promise.all([ options.authData ?? readAuthFileCached({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS }), - resolveGeminiCliConfiguredProjectId(client), + resolveGeminiCliConfiguredProjectId(), ]); const accounts = resolveGeminiCliAccounts(auth, configuredProjectId); if (accounts.length === 0) { diff --git a/src/providers/cache-policies.ts b/src/providers/cache-policies.ts index 9fcf9d2a..31b11db6 100644 --- a/src/providers/cache-policies.ts +++ b/src/providers/cache-policies.ts @@ -161,11 +161,11 @@ export const PROVIDER_CACHE_POLICIES = { }), "google-gemini-cli": { kind: "resolved-auth", - resolveIdentity: (ctx) => resolveGeminiCliAuthIdentity(ctx.client), + resolveIdentity: () => resolveGeminiCliAuthIdentity(), }, "google-agy": { kind: "resolved-auth", - resolveIdentity: (ctx) => resolveGoogleAgyAuthIdentity(ctx.client), + resolveIdentity: () => resolveGoogleAgyAuthIdentity(), }, zai: resolvedCredentialPolicy("zai", async () => { const resolved = await resolveZaiAuthCached({ maxAgeMs: DEFAULT_ZAI_AUTH_CACHE_MAX_AGE_MS }); diff --git a/src/providers/google-agy.ts b/src/providers/google-agy.ts index 4ac1b1fe..7e89659d 100644 --- a/src/providers/google-agy.ts +++ b/src/providers/google-agy.ts @@ -102,7 +102,7 @@ function formatRemainingAmount(value: string | undefined): string | undefined { async function isAgyConfigured(ctx: QuotaProviderContext): Promise { try { - return await hasAgyQuotaRuntimeAvailable(ctx.client); + return await hasAgyQuotaRuntimeAvailable(); } catch { return false; } @@ -121,7 +121,7 @@ export const googleAgyProvider: QuotaProvider = { async fetch(ctx: QuotaProviderContext): Promise { const [auth, companion] = await Promise.all([ - inspectAgyAuthPresence(ctx.client), + inspectAgyAuthPresence(), inspectAgyCompanionPresence(), ]); const statusDetails = statusDetailsFromRecord({ @@ -145,7 +145,7 @@ export const googleAgyProvider: QuotaProvider = { const results = await Promise.all( credentialRows.map(async (row) => ({ row, - result: await queryGoogleAgyQuota(ctx.client, { + result: await queryGoogleAgyQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured ? ctx.config.requestTimeoutMs : undefined, @@ -197,7 +197,7 @@ export const googleAgyProvider: QuotaProvider = { statusDetails, ); } - const result = await queryGoogleAgyQuota(ctx.client, { + const result = await queryGoogleAgyQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured ? ctx.config.requestTimeoutMs : undefined, diff --git a/src/providers/google-gemini-cli.ts b/src/providers/google-gemini-cli.ts index 485a6ca1..2bd860f9 100644 --- a/src/providers/google-gemini-cli.ts +++ b/src/providers/google-gemini-cli.ts @@ -39,7 +39,7 @@ function isGeminiCliModel(model: string): boolean { async function isGeminiCliConfigured(ctx: QuotaProviderContext): Promise { try { - return await hasGeminiCliQuotaRuntimeAvailable(ctx.client); + return await hasGeminiCliQuotaRuntimeAvailable(); } catch { return false; } @@ -58,7 +58,7 @@ export const googleGeminiCliProvider: QuotaProvider = { async fetch(ctx: QuotaProviderContext): Promise { const [auth, companion] = await Promise.all([ - inspectGeminiCliAuthPresence(ctx.client), + inspectGeminiCliAuthPresence(), inspectGeminiCliCompanionPresence(), ]); const statusDetails = statusDetailsFromRecord({ @@ -84,7 +84,7 @@ export const googleGeminiCliProvider: QuotaProvider = { const results = await Promise.all( credentialRows.map(async (row) => ({ row, - result: await queryGeminiCliQuota(ctx.client, { + result: await queryGeminiCliQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured ? ctx.config.requestTimeoutMs : undefined, @@ -147,7 +147,7 @@ export const googleGeminiCliProvider: QuotaProvider = { statusDetails, ); } - const result = await queryGeminiCliQuota(ctx.client, { + const result = await queryGeminiCliQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured ? ctx.config.requestTimeoutMs : undefined, diff --git a/tests/lib.google-agy.test.ts b/tests/lib.google-agy.test.ts index 97a97457..fec4b8fc 100644 --- a/tests/lib.google-agy.test.ts +++ b/tests/lib.google-agy.test.ts @@ -1,4 +1,7 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => { const fetchResponse = vi.fn(); @@ -150,7 +153,15 @@ function authAccount( } describe("google agy logic", () => { + let configDir: string; + + function writeGlobalConfig(config: unknown) { + writeFileSync(join(configDir, "opencode.json"), JSON.stringify(config)); + } + beforeEach(() => { + configDir = mkdtempSync(join(tmpdir(), "opencode-quota-agy-config-")); + vi.stubEnv("OPENCODE_CONFIG_DIR", configDir); vi.clearAllMocks(); mocks.readAuthFileCached.mockResolvedValue(null); mocks.fetchResponse.mockResolvedValue(mockJsonResponse({ groups: [] })); @@ -169,6 +180,10 @@ describe("google agy logic", () => { delete process.env.GOOGLE_CLOUD_PROJECT_ID; }); + afterEach(() => { + rmSync(configDir, { recursive: true, force: true }); + }); + it("composes ordered account and winning companion identities without exposing them", async () => { mocks.readAuthFileCached.mockResolvedValue({ "google-agy": authAccount("refresh-one", "project-one", "alice@example.com"), @@ -234,34 +249,28 @@ describe("google agy logic", () => { process.env.GOOGLE_CLOUD_PROJECT = "generic-gcp-project"; await expect(resolveAgyConfiguredProjectId()).resolves.toBe("generic-gcp-project"); - await expect( - resolveAgyConfiguredProjectId({ - config: { - get: async () => ({ - data: { - provider: { - "google-agy": { options: { projectId: "configured-agy-project" } }, - }, - }, - }), - }, - }), - ).resolves.toBe("configured-agy-project"); + writeGlobalConfig({ + provider: { "google-agy": { options: { projectId: "configured-agy-project" } } }, + }); + await expect(resolveAgyConfiguredProjectId()).resolves.toBe("configured-agy-project"); process.env.OPENCODE_AGY_PROJECT_ID = "explicit-agy-project"; - await expect( - resolveAgyConfiguredProjectId({ - config: { - get: async () => ({ - data: { - provider: { - "google-agy": { options: { projectId: "configured-agy-project" } }, - }, - }, - }), - }, - }), - ).resolves.toBe("explicit-agy-project"); + await expect(resolveAgyConfiguredProjectId()).resolves.toBe("explicit-agy-project"); + }); + + it("reads the project id from OpenCode 2 native provider settings", async () => { + writeGlobalConfig({ + providers: { "google-agy": { settings: { projectId: "native-agy-project" } } }, + }); + await expect(resolveAgyConfiguredProjectId()).resolves.toBe("native-agy-project"); + }); + + it("lets a native providers entry replace the legacy provider entry", async () => { + writeGlobalConfig({ + provider: { "google-agy": { options: { projectId: "legacy-agy-project" } } }, + providers: { "google-agy": { settings: { projectId: "native-agy-project" } } }, + }); + await expect(resolveAgyConfiguredProjectId()).resolves.toBe("native-agy-project"); }); it("preserves auth-key order, project precedence, and duplicate suppression", () => { @@ -351,7 +360,7 @@ describe("google agy logic", () => { }); mocks.fetchResponse.mockResolvedValueOnce(mockJsonResponse(summaryResponse())); - const result = await queryGoogleAgyQuota(undefined, { requestTimeoutMs: 12_345 }); + const result = await queryGoogleAgyQuota({ requestTimeoutMs: 12_345 }); expect(result).toMatchObject({ success: true }); expect(mocks.fetchWithTimeout).toHaveBeenCalledWith( diff --git a/tests/lib.google-gemini-cli.test.ts b/tests/lib.google-gemini-cli.test.ts index a6fd745c..b85ebc9d 100644 --- a/tests/lib.google-gemini-cli.test.ts +++ b/tests/lib.google-gemini-cli.test.ts @@ -1,4 +1,7 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => { const fetchResponse = vi.fn(); @@ -78,7 +81,15 @@ function mockJsonResponse(data: unknown, status = 200) { } describe("gemini cli auth resolution", () => { + let configDir: string; + + function writeGlobalConfig(config: unknown) { + writeFileSync(join(configDir, "opencode.json"), JSON.stringify(config)); + } + beforeEach(() => { + configDir = mkdtempSync(join(tmpdir(), "opencode-quota-gemini-config-")); + vi.stubEnv("OPENCODE_CONFIG_DIR", configDir); vi.clearAllMocks(); mocks.readAuthFileCached.mockResolvedValue(null); mocks.fetchResponse.mockResolvedValue(mockJsonResponse({ buckets: [] })); @@ -94,6 +105,10 @@ describe("gemini cli auth resolution", () => { delete process.env.GOOGLE_CLOUD_PROJECT_ID; }); + afterEach(() => { + rmSync(configDir, { recursive: true, force: true }); + }); + it("composes ordered account and companion identities", async () => { mocks.readAuthFileCached.mockResolvedValue({ "google-gemini-cli": { @@ -258,39 +273,40 @@ describe("gemini cli auth resolution", () => { it("prefers explicit OpenCode provider config over generic Google project env vars", async () => { process.env.GOOGLE_CLOUD_PROJECT = "generic-shell-project"; + writeGlobalConfig({ + provider: { google: { options: { projectId: "configured-opencode-project" } } }, + }); - await expect( - resolveGeminiCliConfiguredProjectId({ - config: { - get: async () => ({ - data: { - provider: { - google: { options: { projectId: "configured-opencode-project" } }, - }, - }, - }), - }, - }), - ).resolves.toBe("configured-opencode-project"); + await expect(resolveGeminiCliConfiguredProjectId()).resolves.toBe( + "configured-opencode-project", + ); + }); + + it("reads the project id from OpenCode 2 native provider settings", async () => { + writeGlobalConfig({ + providers: { google: { settings: { projectId: "native-opencode-project" } } }, + }); + + await expect(resolveGeminiCliConfiguredProjectId()).resolves.toBe("native-opencode-project"); + }); + + it("lets a native providers entry replace the legacy provider entry", async () => { + writeGlobalConfig({ + provider: { google: { options: { projectId: "legacy-opencode-project" } } }, + providers: { google: { settings: { projectId: "native-opencode-project" } } }, + }); + + await expect(resolveGeminiCliConfiguredProjectId()).resolves.toBe("native-opencode-project"); }); it("keeps OPENCODE_GEMINI_PROJECT_ID as the highest-priority project override", async () => { process.env.OPENCODE_GEMINI_PROJECT_ID = "explicit-gemini-project"; process.env.GOOGLE_CLOUD_PROJECT = "generic-shell-project"; + writeGlobalConfig({ + providers: { google: { settings: { projectId: "native-opencode-project" } } }, + }); - await expect( - resolveGeminiCliConfiguredProjectId({ - config: { - get: async () => ({ - data: { - provider: { - google: { options: { projectId: "configured-opencode-project" } }, - }, - }, - }), - }, - }), - ).resolves.toBe("explicit-gemini-project"); + await expect(resolveGeminiCliConfiguredProjectId()).resolves.toBe("explicit-gemini-project"); }); it("reports invalid auth when OAuth exists but no project id can be resolved", async () => { diff --git a/tests/providers.google-agy.test.ts b/tests/providers.google-agy.test.ts index f2a62fba..f50d8c43 100644 --- a/tests/providers.google-agy.test.ts +++ b/tests/providers.google-agy.test.ts @@ -45,13 +45,13 @@ describe("google agy provider", () => { (queryGoogleAgyQuota as any).mockResolvedValue(null); await googleAgyProvider.fetch({ client: {}, config: { requestTimeoutMs: 5000 } } as any); - expect(queryGoogleAgyQuota).toHaveBeenLastCalledWith({}, { requestTimeoutMs: undefined }); + expect(queryGoogleAgyQuota).toHaveBeenLastCalledWith({ requestTimeoutMs: undefined }); await googleAgyProvider.fetch({ client: {}, config: { requestTimeoutMs: 12000, requestTimeoutMsConfigured: true }, } as any); - expect(queryGoogleAgyQuota).toHaveBeenLastCalledWith({}, { requestTimeoutMs: 12000 }); + expect(queryGoogleAgyQuota).toHaveBeenLastCalledWith({ requestTimeoutMs: 12000 }); }); it("returns attempted:false when Google AGY auth is not configured", async () => { diff --git a/tests/providers.google-gemini-cli.test.ts b/tests/providers.google-gemini-cli.test.ts index a1dd792f..0420bdd6 100644 --- a/tests/providers.google-gemini-cli.test.ts +++ b/tests/providers.google-gemini-cli.test.ts @@ -31,13 +31,13 @@ describe("google gemini cli provider", () => { (queryGeminiCliQuota as any).mockResolvedValue(null); await googleGeminiCliProvider.fetch({ client: {}, config: { requestTimeoutMs: 5000 } } as any); - expect(queryGeminiCliQuota).toHaveBeenLastCalledWith({}, { requestTimeoutMs: undefined }); + expect(queryGeminiCliQuota).toHaveBeenLastCalledWith({ requestTimeoutMs: undefined }); await googleGeminiCliProvider.fetch({ client: {}, config: { requestTimeoutMs: 12000, requestTimeoutMsConfigured: true }, } as any); - expect(queryGeminiCliQuota).toHaveBeenLastCalledWith({}, { requestTimeoutMs: 12000 }); + expect(queryGeminiCliQuota).toHaveBeenLastCalledWith({ requestTimeoutMs: 12000 }); }); it("returns attempted:false when Gemini CLI auth is not configured", async () => { From 9e1f2259beee024ad01a19101f533ab63fbcb14e Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:32 +0200 Subject: [PATCH 58/97] refactor(google): drop unused provider context from availability checks --- src/providers/google-agy.ts | 6 +++--- src/providers/google-gemini-cli.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/providers/google-agy.ts b/src/providers/google-agy.ts index 7e89659d..edab3d64 100644 --- a/src/providers/google-agy.ts +++ b/src/providers/google-agy.ts @@ -100,7 +100,7 @@ function formatRemainingAmount(value: string | undefined): string | undefined { return `${display} left`; } -async function isAgyConfigured(ctx: QuotaProviderContext): Promise { +async function isAgyConfigured(): Promise { try { return await hasAgyQuotaRuntimeAvailable(); } catch { @@ -111,8 +111,8 @@ async function isAgyConfigured(ctx: QuotaProviderContext): Promise { export const googleAgyProvider: QuotaProvider = { id: "google-agy", - async isAvailable(ctx: QuotaProviderContext): Promise { - return await isAgyConfigured(ctx); + async isAvailable(): Promise { + return await isAgyConfigured(); }, matchesCurrentModel(model: string): boolean { diff --git a/src/providers/google-gemini-cli.ts b/src/providers/google-gemini-cli.ts index 2bd860f9..6c62a40b 100644 --- a/src/providers/google-gemini-cli.ts +++ b/src/providers/google-gemini-cli.ts @@ -37,7 +37,7 @@ function isGeminiCliModel(model: string): boolean { ); } -async function isGeminiCliConfigured(ctx: QuotaProviderContext): Promise { +async function isGeminiCliConfigured(): Promise { try { return await hasGeminiCliQuotaRuntimeAvailable(); } catch { @@ -48,8 +48,8 @@ async function isGeminiCliConfigured(ctx: QuotaProviderContext): Promise { - return await isGeminiCliConfigured(ctx); + async isAvailable(): Promise { + return await isGeminiCliConfigured(); }, matchesCurrentModel(model: string): boolean { From 6b1d3c29912a5a73262bee57408b3f5085b35a41 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:32 +0200 Subject: [PATCH 59/97] fix(providers): keep provider names in multi-account labels OpenCode 2 names credentials it imports from the legacy auth.json "OAuth" or "API key". The shared multi-account header formatter treated those as user-chosen names, so every fan-out provider showed headers like "[OpenAI OAuth] (Pro)", "[Copilot OAuth] (individual)" or "[Z.ai API key]". Treat them like the existing "default" label: the header is now just the provider name ("[OpenAI] (Pro)"), and real connection names still appear ("[Z.ai Work]"). The fix lives in formatCredentialDisplayNames, which OpenAI, Copilot, Anthropic, xAI, GLM (Z.ai/Zhipu), MiniMax, Kimi, OpenCode Go, Gemini CLI and AGY all use. Idea from MemberIT's fork (39842e8c2), which fixed the same generic-label problem for Kimi, GLM and MiniMax. Co-authored-by: Mikhail Koniakhin --- src/lib/opencode-auth.ts | 10 +++++++++- tests/lib.opencode-auth.test.ts | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 57af9e52..ad05a0a4 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -28,6 +28,14 @@ export type CredentialRow = { value: Record; }; +/** + * Connection labels OpenCode assigns when the user never named the connection: + * `default` for a new connection, and `OAuth` / `API key` for credentials + * imported from the legacy auth.json. They say nothing about the account, so + * they must not appear in headers like `[OpenAI OAuth]`. + */ +const GENERIC_CREDENTIAL_LABELS: ReadonlySet = new Set(["default", "oauth", "api key"]); + export function formatCredentialDisplayNames( providerName: string, credentials: ReadonlyArray<{ row: CredentialRow; fallbackName: string }>, @@ -37,7 +45,7 @@ export function formatCredentialDisplayNames( const alias = row.label.trim(); const redundantAlias = !alias || - alias.toLowerCase() === "default" || + GENERIC_CREDENTIAL_LABELS.has(alias.toLowerCase()) || alias.toLowerCase() === providerName.toLowerCase(); const fallbackCategory = fallbackName .trim() diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index b009c464..62f9ea50 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -87,6 +87,12 @@ describe("OpenCode auth reader", () => { ["xAI", "xAI SuperGrok", "personal", true, "[xAI personal] (SuperGrok)*"], ["OpenAI", "OpenAI (Pro)", " ", true, "[OpenAI] (Pro)*"], ["OpenAI", "OpenAI (Pro)", "openai", true, "[OpenAI] (Pro)*"], + // OpenCode 2 labels credentials imported from auth.json "OAuth" or "API key". + ["OpenAI", "OpenAI (Pro)", "OAuth", true, "[OpenAI] (Pro)*"], + ["Copilot", "Copilot (individual)", "OAuth", true, "[Copilot] (individual)*"], + ["Z.ai", "Z.ai", "API key", true, "[Z.ai]*"], + ["MiniMax", "MiniMax", "api key", true, "[MiniMax]*"], + ["Kimi Code", "Kimi Code", "API key", true, "[Kimi Code]*"], ])("formats %s credential aliases as %s", (providerName, fallbackName, label, active, expected) => { expect( formatCredentialDisplayNames(providerName, [ @@ -117,6 +123,21 @@ describe("OpenCode auth reader", () => { ).toEqual(["[OpenAI SEPD] (Pro)*", "[OpenAI SEPD 2] (Pro)", "[OpenAI] (Pro)"]); }); + it("keeps a named connection next to an imported generic one", () => { + expect( + formatCredentialDisplayNames("Z.ai", [ + { + row: { id: "a", integrationId: "zai", label: "Work", active: true, value: {} }, + fallbackName: "Z.ai", + }, + { + row: { id: "b", integrationId: "zai", label: "API key", active: false, value: {} }, + fallbackName: "Z.ai", + }, + ]), + ).toEqual(["[Z.ai Work]*", "[Z.ai]"]); + }); + it("reports the credential database path and honors OPENCODE_DB", async () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); From baa996d7bb212222156a8d1770fd4c0199c63135 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:33 +0200 Subject: [PATCH 60/97] docs!: remove the v4 migration guide --- README.md | 1 - docs/readme/v4-migration.md | 85 ------------------- tests/google-gemini-cli-org-only-docs.test.ts | 9 +- .../quota-providers-docs-consistency.test.ts | 3 +- tests/v4-migration-docs.test.ts | 60 ------------- 5 files changed, 2 insertions(+), 156 deletions(-) delete mode 100644 docs/readme/v4-migration.md delete mode 100644 tests/v4-migration-docs.test.ts diff --git a/README.md b/README.md index 415f4948..2fc2671e 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,6 @@ Project guides: - [Troubleshooting](docs/readme/troubleshooting.md) - [External integration](docs/readme/external-integration.md) - [Updating safely](docs/readme/updating.md) -- [v4 migration guide](docs/readme/v4-migration.md) (upgrading from v3) External references: diff --git a/docs/readme/v4-migration.md b/docs/readme/v4-migration.md deleted file mode 100644 index 9d382bf8..00000000 --- a/docs/readme/v4-migration.md +++ /dev/null @@ -1,85 +0,0 @@ -[← Back to README](../../README.md) - -# Move from v3 to v4 - -v4 adds clearer quota results, guided custom-provider setup, and JSON export v2. The updater keeps unrelated OpenCode settings. - -## Requirements - -- OpenCode 1.4.3 or newer -- Node.js 22 or newer - -## Before you update - -1. Close OpenCode. -2. Back up the OpenCode config files you use: - - `opencode.json` or `opencode.jsonc` - - `tui.json` or `tui.jsonc` - - `opencode-quota/quota-toast.json` or `quota-toast.jsonc` -3. If another app reads `opencode-quota show --json` or `quota-export.json`, review [JSON export v2](external-integration.md#json-basics). -4. Keep provider credentials in OpenCode authentication, global config, or environment variables—not project quota settings. - -## Update - -Preview the changes first: - -```bash -npx @slkiser/opencode-quota@latest update --dry-run -``` - -If the preview looks right, apply them: - -```bash -npx @slkiser/opencode-quota@latest update -``` - -Restart OpenCode, then run `/quota` and `/quota_status`. - -## What may need your attention - -### Custom providers - -v4 replaces the old `customSources` setting with `quotaProviders`. The old setting is not read or converted automatically. - -Use the guided command to add each custom provider: - -```bash -npx @slkiser/opencode-quota@latest provider add -``` - -It previews the exact global config change and asks before writing. See the [Provider setup guide](providers.md#custom-providers) for full details. - -### Gemini CLI - -Existing `google-gemini-cli` configurations continue to work, but only with Gemini Code Assist Standard or Enterprise (organization) accounts. Google ended personal accounts on 2026-06-18. OpenCode Quota does not migrate your configuration or authentication and does not silently switch providers. - -Personal Google users should configure and verify `google-agy` separately, then remove their Gemini CLI setup. See [Gemini CLI in the provider guide](providers.md#gemini-cli). - -### Apps that read quota JSON - -v4 JSON uses schema `version: 2`. It clearly labels quota, usage, spend, budget, balance, and partial failures. - -Update any app or script that reads the JSON before depending on v4 output. See [External integration](external-integration.md). - -### Alibaba custom limits - -Built-in limits continue to work. If you changed Alibaba limits, add those changes through `quotaProviders`. See [Custom providers](configuration.md#custom-providers). - -## Check the update - -After restarting OpenCode: - -1. Run `/quota` and confirm your providers and values appear. -2. Run `/quota_status` and check for setup or authentication errors. -3. If enabled, check the TUI sidebar, toast, and compact line. -4. If you added a custom provider, confirm its row appears. One failed provider should not hide successful providers. - -## Roll back to v3 - -1. Close OpenCode. -2. Restore the config backup you made before updating. -3. Pin both the server and TUI plugin entries to v3, for example `@slkiser/opencode-quota@3`. -4. Remove v4 `quotaProviders` entries because v3 does not understand them. -5. Restart OpenCode and run `/quota`. - -v3 does not read v4 cache or custom-provider state. OpenCode Quota can recreate those files if you return to v4. diff --git a/tests/google-gemini-cli-org-only-docs.test.ts b/tests/google-gemini-cli-org-only-docs.test.ts index 840caf43..791aa9fc 100644 --- a/tests/google-gemini-cli-org-only-docs.test.ts +++ b/tests/google-gemini-cli-org-only-docs.test.ts @@ -20,10 +20,9 @@ describe("Gemini CLI organization-only documentation", () => { const configuration = read("docs/readme/configuration.md"); const providers = read("docs/readme/providers.md"); const troubleshooting = read("docs/readme/troubleshooting.md"); - const migration = read("docs/readme/v4-migration.md"); it("no longer calls Gemini CLI deprecated or planned for removal", () => { - for (const document of [readme, providers, troubleshooting, migration]) { + for (const document of [readme, providers, troubleshooting]) { expect(document).not.toContain("Gemini CLI (deprecated)"); expect(document).not.toContain("v5.0.0"); expect(document).not.toContain("Existing setups only"); @@ -61,12 +60,6 @@ describe("Gemini CLI organization-only documentation", () => { expect(providerSection).toContain("opencode-gemini-auth"); expect(providerSection).toContain("opencode auth login google"); expect(providerSection).toContain("include `google-gemini-cli` in `enabledProviders`"); - - expect(migration).toContain(ORG_ONLY); - expect(migration).toContain("Google ended personal accounts on 2026-06-18."); - expect(migration).toContain( - "OpenCode Quota does not migrate your configuration or authentication and does not silently switch providers.", - ); }); it("keeps repair guidance and keeps Gemini CLI out of new configuration examples", () => { diff --git a/tests/quota-providers-docs-consistency.test.ts b/tests/quota-providers-docs-consistency.test.ts index bfee0d1f..26827233 100644 --- a/tests/quota-providers-docs-consistency.test.ts +++ b/tests/quota-providers-docs-consistency.test.ts @@ -50,7 +50,6 @@ describe("quota provider Phase 7 documentation consistency", () => { const troubleshooting = read("docs/readme/troubleshooting.md"); const external = read("docs/readme/external-integration.md"); const manualInstall = read("docs/readme/manual-install.md"); - const migration = read("docs/readme/v4-migration.md"); const readmeCommands = readMarkdownSection(readme, /^Commands$/); const readmeCustomProviders = readMarkdownSection(readme, /^Custom providers$/); @@ -75,7 +74,6 @@ describe("quota provider Phase 7 documentation consistency", () => { troubleshooting, external, manualInstall, - migration, ]) { expect(document).not.toContain("accounting-v1"); } @@ -84,6 +82,7 @@ describe("quota provider Phase 7 documentation consistency", () => { expect(configuration).toContain("experimental.quotaToast"); expect(configuration).toContain("quotaProviders"); expect(configuration).toContain("JSONC"); + expect(configuration).toContain("do not duplicate it in a second file"); for (const document of [configuration, providers]) { expect(document).toContain("apiKeyEnv"); diff --git a/tests/v4-migration-docs.test.ts b/tests/v4-migration-docs.test.ts deleted file mode 100644 index 6ba42ed1..00000000 --- a/tests/v4-migration-docs.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { describe, expect, it } from "vitest"; - -const migration = await readFile( - new URL("../docs/readme/v4-migration.md", import.meta.url), - "utf8", -); -const readme = await readFile(new URL("../README.md", import.meta.url), "utf8"); -const packageJson = JSON.parse( - await readFile(new URL("../package.json", import.meta.url), "utf8"), -) as { - engines?: Record; - peerDependencies?: Record; -}; -const exportTypes = await readFile( - new URL("../src/lib/quota-export-types.ts", import.meta.url), - "utf8", -); -const configuration = await readFile( - new URL("../docs/readme/configuration.md", import.meta.url), - "utf8", -); - -describe("v4 migration documentation contract", () => { - it("keeps requirements and navigation aligned", () => { - expect(packageJson.engines?.node).toBe("^22.13.0 || >=23.4.0"); - expect(packageJson.peerDependencies?.["@opencode/plugin"]).toBe("2.0.16"); - expect(packageJson.engines).not.toHaveProperty("opencode"); - - expect(migration).toContain("[← Back to README](../../README.md)"); - expect(migration).toContain("OpenCode 1.4.3 or newer"); - expect(migration).toContain("Node.js 22 or newer"); - expect(readme).toContain("[v4 migration guide](docs/readme/v4-migration.md)"); - }); - - it("explains the user-visible provider and JSON changes", () => { - expect(migration).toContain( - "v4 replaces the old `customSources` setting with `quotaProviders`", - ); - expect(migration).toContain("npx @slkiser/opencode-quota@latest provider add"); - expect(migration).toContain("[Provider setup guide](providers.md#custom-providers)"); - expect(migration).not.toContain("custom-accounting-sources"); - - expect(exportTypes).toContain("version: 2;"); - expect(migration).toContain("schema `version: 2`"); - expect(migration).toContain("[External integration](external-integration.md)"); - expect(configuration).toContain("experimental.quotaToast"); - expect(configuration).toContain("quotaProviders"); - expect(configuration).toContain("do not duplicate it in a second file"); - }); - - it("gives concrete preview, verification, and rollback steps", () => { - expect(migration).toContain("npx @slkiser/opencode-quota@latest update --dry-run"); - expect(migration).toContain("## Check the update"); - expect(migration).toContain("## Roll back to v3"); - expect(migration).toContain("Restart OpenCode, then run `/quota` and `/quota_status`."); - expect(migration).toContain("The old setting is not read or converted automatically."); - expect(migration).not.toContain("automatically migrates"); - }); -}); From cf3b5ab3db2ec49af5272245adb78a42eae42e8b Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:33 +0200 Subject: [PATCH 61/97] ci(release): publish prereleases to the npm next tag --- .github/workflows/publish-npm.yml | 25 ++++++++++++- README.md | 3 ++ src/lib/init-installer.ts | 35 ++++++++++++++---- src/lib/scoped-update.ts | 12 ++++-- tests/lib.init-installer.test.ts | 37 +++++++++++++++++++ tests/lib.scoped-update.test.ts | 39 ++++++++++++++++++-- tests/package-manifest.test.ts | 61 +++++++++++++++++++++++++++---- 7 files changed, 190 insertions(+), 22 deletions(-) diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index ba47919f..ebdc8cd9 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -129,7 +129,24 @@ jobs: name: release-package path: package-artifacts + - name: Choose npm dist-tag + id: dist-tag + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_PRERELEASE: ${{ github.event.release.prerelease }} + run: | + VERSION="${RELEASE_TAG#v}" + if [ "$RELEASE_PRERELEASE" = "true" ] || [[ "$VERSION" == *-* ]]; then + DIST_TAG="next" + else + DIST_TAG="latest" + fi + echo "Publishing $VERSION to the npm '$DIST_TAG' dist-tag" + echo "dist-tag=$DIST_TAG" >> "$GITHUB_OUTPUT" + - name: Verify and publish exact release artifact + env: + DIST_TAG: ${{ steps.dist-tag.outputs.dist-tag }} run: | node scripts/verify-release-artifact.mjs package-artifacts TARBALLS=(package-artifacts/*.tgz) @@ -137,7 +154,7 @@ jobs: echo "Expected exactly one release tarball." >&2 exit 1 fi - npm publish "./${TARBALLS[0]}" --access public --provenance --ignore-scripts + npm publish "./${TARBALLS[0]}" --tag "$DIST_TAG" --access public --provenance --ignore-scripts backfill-version: runs-on: ubuntu-latest @@ -195,6 +212,12 @@ jobs: BRANCH="$DEFAULT_BRANCH" fi + git fetch origin "$BRANCH" + if ! git merge-base --is-ancestor "$GITHUB_SHA" "origin/$BRANCH"; then + echo "Release commit $GITHUB_SHA is not on branch '$BRANCH'; refusing to commit the version there." >&2 + exit 1 + fi + echo "Committing synced package version to branch '$BRANCH'" git config user.name "github-actions[bot]" diff --git a/README.md b/README.md index 2fc2671e..c19ebd30 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ npx @slkiser/opencode-quota init Upgrading from 4.x? Read [what changed in 5.0](#breaking-changes-in-500). +> [!NOTE] +> **Try the 5.0 beta.** Until 5.0.0 ships, the plain command installs 4.x. Run `npx @slkiser/opencode-quota@next init`, or put `"plugin": ["@slkiser/opencode-quota@next"]` in `opencode.json`. `npx @slkiser/opencode-quota@next update` keeps you on `@next`. + After installation: 1. Restart OpenCode. diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index aadbf9c0..4e669472 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -38,8 +38,8 @@ import { } from "./quota-format-style.js"; import { QUOTA_PROVIDERS_AGGREGATE_ID } from "./quota-providers.js"; import type { QuotaToastConfig, SessionTokenScope, TuiCommandDisplay } from "./types.js"; +import { getPackageVersion } from "./version.js"; -const QUOTA_PLUGIN_SPEC = "@slkiser/opencode-quota@latest"; const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json"; const GITHUB_REPO_URL = "https://github.com/slkiser/opencode-quota"; const GITHUB_STAR_NOTE = `if this helps, stars are appreciated: ${GITHUB_REPO_URL}`; @@ -119,6 +119,19 @@ export class InitInstallerError extends Error { type JsonObject = Record; +/** A prerelease (version with `-`) installs from the npm `next` tag; a stable release from `latest`. */ +export function getQuotaPluginSpecForVersion(version: string): string { + return version.includes("-") ? "@slkiser/opencode-quota@next" : "@slkiser/opencode-quota@latest"; +} + +async function resolveQuotaPluginSpec(): Promise { + const version = await getPackageVersion(); + if (!version) { + throw new InitInstallerError("Cannot read the OpenCode Quota package version."); + } + return getQuotaPluginSpecForVersion(version); +} + type PromptOption = { label: string; value: string; @@ -290,18 +303,19 @@ function isQuotaPluginEntry(entry: unknown): boolean { function appendQuotaPluginIfMissing(params: { container: unknown[]; pathLabel: string; + pluginSpec: string; edit: PlannedConfigEdit; }): void { const alreadyConfigured = params.container.some(isQuotaPluginEntry); if (alreadyConfigured) { - params.edit.skippedValues.push(`${params.pathLabel} already includes ${QUOTA_PLUGIN_SPEC}`); + params.edit.skippedValues.push(`${params.pathLabel} already includes ${params.pluginSpec}`); return; } - params.container.push(QUOTA_PLUGIN_SPEC); + params.container.push(params.pluginSpec); params.edit.changed = true; - params.edit.addedPlugins.push(`${params.pathLabel}: ${QUOTA_PLUGIN_SPEC}`); + params.edit.addedPlugins.push(`${params.pathLabel}: ${params.pluginSpec}`); } function ensureTopLevelPluginArray(root: JsonObject, edit: PlannedConfigEdit): unknown[] { @@ -607,6 +621,7 @@ function syncLegacyQuotaToast(params: { async function planOpencodeEdit(params: { selections: InitInstallerSelections; baseDir: string; + pluginSpec: string; legacyQuotaToastToSync?: JsonObject; }): Promise { const target = resolveEditableConfigPath({ @@ -642,12 +657,13 @@ async function planOpencodeEdit(params: { // OpenCode 2 also loads plugins from the native `plugins` array; never add a second entry. if (Array.isArray(root.plugins) && root.plugins.some(isQuotaPluginEntry)) { - edit.skippedValues.push(`plugins already includes ${QUOTA_PLUGIN_SPEC}`); + edit.skippedValues.push(`plugins already includes ${params.pluginSpec}`); } else { const plugin = ensureTopLevelPluginArray(root, edit); appendQuotaPluginIfMissing({ container: plugin, pathLabel: "plugin", + pluginSpec: params.pluginSpec, edit, }); } @@ -1043,6 +1059,7 @@ export async function planInitInstaller(params: { await planOpencodeEdit({ selections, baseDir, + pluginSpec: await resolveQuotaPluginSpec(), legacyQuotaToastToSync: params.syncLegacyConfig ? quotaEdit.plannedData : undefined, }), quotaEdit, @@ -1194,7 +1211,7 @@ async function readExistingInstallerAnswers(baseDir: string): Promise { const interfaces = await prompts.select({ message: "Which OpenCode interfaces do you use?", @@ -1340,7 +1357,7 @@ async function promptForSelections( manualProviders = selected.filter((value): value is string => typeof value === "string"); } prompts.log.info("Custom providers are configured after installation."); - prompts.log.info("npx @slkiser/opencode-quota@latest provider add"); + prompts.log.info(`npx ${context.pluginSpec} provider add`); const formatStyle = await prompts.select({ message: "Quota reset periods", @@ -1441,10 +1458,12 @@ export async function runInitInstaller(params?: { prompts.intro("Configure @slkiser/opencode-quota"); try { + const pluginSpec = await resolveQuotaPluginSpec(); const selections = await promptForSelections(prompts, { cwd: params?.cwd, env: params?.env, homeDir: params?.homeDir, + pluginSpec, }); if (!selections) { prompts.outro("OpenCode Quota setup cancelled — no files changed."); @@ -1472,7 +1491,7 @@ export async function runInitInstaller(params?: { if (params?.dryRun) { prompts.outro( - "OpenCode Quota setup preview complete — no files changed. Run npx @slkiser/opencode-quota@latest init to apply.", + `OpenCode Quota setup preview complete — no files changed. Run npx ${pluginSpec} init to apply.`, ); return 0; } diff --git a/src/lib/scoped-update.ts b/src/lib/scoped-update.ts index 5337fd38..b8146349 100644 --- a/src/lib/scoped-update.ts +++ b/src/lib/scoped-update.ts @@ -30,6 +30,12 @@ import { export const QUOTA_PACKAGE_NAME = "@slkiser/opencode-quota"; export const QUOTA_LATEST_SPEC = `${QUOTA_PACKAGE_NAME}@latest`; +/** Moving specs that update keeps as written; OpenCode re-resolves them after the cache cleanup. */ +const QUOTA_MOVING_SPECS = new Set([ + QUOTA_LATEST_SPEC, + `${QUOTA_PACKAGE_NAME}@next`, + `${QUOTA_PACKAGE_NAME}@4`, +]); const GITHUB_REPO_URL = "https://github.com/slkiser/opencode-quota"; const EXACT_SEMVER = @@ -85,7 +91,7 @@ export class ScopedUpdateError extends Error { } export function isCanonicalQuotaUpdateSpec(spec: string): boolean { - if (spec === QUOTA_PACKAGE_NAME || spec === QUOTA_LATEST_SPEC) return true; + if (spec === QUOTA_PACKAGE_NAME || QUOTA_MOVING_SPECS.has(spec)) return true; const prefix = `${QUOTA_PACKAGE_NAME}@`; return spec.startsWith(prefix) && EXACT_SEMVER.test(spec.slice(prefix.length)); } @@ -169,7 +175,7 @@ function updateConfig( : null; if (spec === null || !isCanonicalQuotaUpdateSpec(spec)) continue; specs.push(spec); - if (spec === QUOTA_LATEST_SPEC) continue; + if (QUOTA_MOVING_SPECS.has(spec)) continue; // Replace only the package spec; keep the entry's plugin options. const targetPath = typeof entry === "string" @@ -621,7 +627,7 @@ export async function applyScopedUpdatePlan( } if (!snapshot.roles.includes("package-authority")) continue; const currentPlan = updateConfig(current.toString("utf8"), snapshot.path); - if (currentPlan.specs.includes(QUOTA_LATEST_SPEC)) authoritativeLatest = true; + if (currentPlan.specs.some((spec) => QUOTA_MOVING_SPECS.has(spec))) authoritativeLatest = true; } const removedCachePaths: string[] = []; diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index d0788301..bcbe8577 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -4,12 +4,20 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { applyInitInstallerPlan, + getQuotaPluginSpecForVersion, type InitInstallerSelections, planInitInstaller, runInitInstaller, } from "../src/lib/init-installer.js"; import { parseJsonOrJsonc } from "../src/lib/jsonc.js"; +// The release workflow syncs package.json to the release tag before `pnpm verify`, +// so these tests pin the running package version instead of reading package.json. +const packageVersion = vi.hoisted(() => ({ value: "5.0.0" })); +vi.mock("../src/lib/version.js", () => ({ + getPackageVersion: async () => packageVersion.value, +})); + function readJson(path: string): any { const resolvedPath = !existsSync(path) && path.endsWith(".json") ? `${path}c` : path; const content = readFileSync(resolvedPath, "utf8"); @@ -111,6 +119,7 @@ describe("init installer planning and merge behavior", () => { afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); + packageVersion.value = "5.0.0"; }); it("creates recommended project opencode.jsonc at the worktree root for toast mode", async () => { @@ -1113,6 +1122,34 @@ describe("init installer planning and merge behavior", () => { expect(secondPlan.edits.find((edit) => edit.kind === "opencode")?.changed).toBe(false); }); + it("chooses the npm tag from the running package version", () => { + expect(getQuotaPluginSpecForVersion("5.0.0")).toBe("@slkiser/opencode-quota@latest"); + expect(getQuotaPluginSpecForVersion("5.0.0-beta.1")).toBe("@slkiser/opencode-quota@next"); + }); + + it("writes and suggests @next when the running package is a prerelease", async () => { + packageVersion.value = "5.0.0-beta.1"; + const projectDir = join(tempDir, "project-next"); + mkdirSync(projectDir, { recursive: true }); + + const plan = await planInitInstaller({ cwd: projectDir, selections: installerSelections() }); + await applyInitInstallerPlan(plan); + expect(readJson(join(projectDir, "opencode.jsonc")).plugin).toEqual([ + "@slkiser/opencode-quota@next", + ]); + + const prompts = createPromptStub({ + selectValues: DEFAULT_PROMPT_SELECT_VALUES, + multiselectValues: [["toast"]], + }); + const code = await runInitInstaller({ cwd: tempDir, prompts: prompts as any, dryRun: true }); + expect(code).toBe(0); + expect(prompts.infoCalls).toContain("npx @slkiser/opencode-quota@next provider add"); + expect(prompts.outroCalls).toContain( + "OpenCode Quota setup preview complete — no files changed. Run npx @slkiser/opencode-quota@next init to apply.", + ); + }); + it("preserves existing exact, range, tag, tuple, and local specs", async () => { const projectDir = join(tempDir, "project"); mkdirSync(projectDir, { recursive: true }); diff --git a/tests/lib.scoped-update.test.ts b/tests/lib.scoped-update.test.ts index fc15c978..d9d4251f 100644 --- a/tests/lib.scoped-update.test.ts +++ b/tests/lib.scoped-update.test.ts @@ -57,16 +57,19 @@ afterEach(() => { }); describe("scoped update specs and paths", () => { - it("accepts only bare, latest, and exact SemVer package specs", () => { + it("accepts only bare, latest, next, 4, and exact SemVer package specs", () => { for (const spec of [ "@slkiser/opencode-quota", "@slkiser/opencode-quota@latest", + "@slkiser/opencode-quota@next", + "@slkiser/opencode-quota@4", "@slkiser/opencode-quota@3.11.1", "@slkiser/opencode-quota@3.11.2-beta.1+build.2", ]) expect(isCanonicalQuotaUpdateSpec(spec)).toBe(true); for (const spec of [ - "@slkiser/opencode-quota@next", + "@slkiser/opencode-quota@beta", + "@slkiser/opencode-quota@5", "@slkiser/opencode-quota@^3.11.1", "@slkiser/opencode-quota@~3.11.1", "npm:@slkiser/opencode-quota@3.11.1", @@ -144,7 +147,10 @@ describe("scoped update config planning", () => { ); const params = { cwd: f.project, env: f.env, homeDir: join(f.root, "home") }; const plan = await planScopedUpdate(params); - expect(plan.foundSpecs).toEqual(["@slkiser/opencode-quota@3.11.1"]); + expect(plan.foundSpecs).toEqual([ + "@slkiser/opencode-quota@next", + "@slkiser/opencode-quota@3.11.1", + ]); await applyScopedUpdatePlan(plan); expect(JSON.parse(readFileSync(config, "utf8"))).toEqual({ plugins: [ @@ -156,6 +162,33 @@ describe("scoped update config planning", () => { }); }); + it.each([ + "@slkiser/opencode-quota@next", + "@slkiser/opencode-quota@4", + ])("keeps %s as written and refreshes its package cache", async (spec) => { + const f = fixture(); + const config = join(f.project, "opencode.json"); + const original = `{"plugin":["${spec}"]}`; + write(config, original); + const cache = join(f.cache, "packages", ...spec.split("/")); + const manifest = join(cache, "node_modules", "@slkiser", "opencode-quota", "package.json"); + write(manifest, `{"name":"@slkiser/opencode-quota"}`); + const plan = await planScopedUpdate({ + cwd: f.project, + env: f.env, + homeDir: join(f.root, "home"), + platform: "linux", + }); + expect(plan.configEdits).toEqual([]); + expect(plan.foundSpecs).toEqual([spec]); + expect(plan.authoritativeLatest).toBe(true); + + const result = await applyScopedUpdatePlan(plan); + expect(readFileSync(config, "utf8")).toBe(original); + expect(result.writtenPaths).toEqual([]); + expect(result.removedCachePaths).toEqual([cache]); + }); + it("leaves tui.json files untouched", async () => { const f = fixture(); const projectTui = join(f.project, "tui.json"); diff --git a/tests/package-manifest.test.ts b/tests/package-manifest.test.ts index 621837e4..404f00c3 100644 --- a/tests/package-manifest.test.ts +++ b/tests/package-manifest.test.ts @@ -1,10 +1,16 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { access, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; interface WorkflowStep { + id?: string; name?: string; + env?: Record; uses?: string; run?: string; with?: Record; @@ -560,14 +566,17 @@ describe("package manifest compatibility", () => { name: "release-package", path: "package-artifacts", }); - const publishRun = namedStep(publish, "Verify and publish exact release artifact").run ?? ""; + expect(stepIndex(publish, "Choose npm dist-tag")).toBeLessThan( + stepIndex(publish, "Verify and publish exact release artifact"), + ); + const publishStep = namedStep(publish, "Verify and publish exact release artifact"); + expect(publishStep.env).toEqual({ DIST_TAG: "${{ steps.dist-tag.outputs.dist-tag }}" }); + const publishRun = publishStep.run ?? ""; expect(publishRun).toContain("node scripts/verify-release-artifact.mjs package-artifacts"); expect(publishRun).toContain( - 'npm publish "./${TARBALLS[0]}" --access public --provenance --ignore-scripts', - ); - expect(publishRun).not.toContain( - 'npm publish "${TARBALLS[0]}" --access public --provenance --ignore-scripts', + 'npm publish "./${TARBALLS[0]}" --tag "$DIST_TAG" --access public --provenance --ignore-scripts', ); + expect(publishRun).not.toContain('npm publish "${TARBALLS[0]}"'); expect(publishRun).not.toContain("pnpm pack"); expect(publishRun).not.toContain("pnpm run build"); @@ -590,8 +599,46 @@ describe("package manifest compatibility", () => { expect(namedStep(backfill, "Sync and verify version for repository backfill").run).toContain( "pnpm run verify:release-version", ); - expect(namedStep(backfill, "Commit synced version back to repository").run).toContain( - 'git push origin HEAD:"$BRANCH"', + const backfillCommitRun = + namedStep(backfill, "Commit synced version back to repository").run ?? ""; + expect(backfillCommitRun).toContain('git push origin HEAD:"$BRANCH"'); + // A beta targets the v5 branch; never write its version onto a branch without the release commit. + const ancestryCheck = backfillCommitRun.indexOf( + 'git merge-base --is-ancestor "$GITHUB_SHA" "origin/$BRANCH"', ); + expect(ancestryCheck).toBeGreaterThanOrEqual(0); + expect(ancestryCheck).toBeLessThan(backfillCommitRun.indexOf("git commit")); + }); + + it("publishes prereleases to the npm next tag and stable releases to latest", () => { + const chooseTag = namedStep(publishWorkflow.jobs.publish, "Choose npm dist-tag"); + expect(chooseTag.id).toBe("dist-tag"); + expect(chooseTag.env).toEqual({ + RELEASE_TAG: "${{ github.event.release.tag_name }}", + RELEASE_PRERELEASE: "${{ github.event.release.prerelease }}", + }); + + const distTagFor = (tag: string, prerelease: string): string => { + const dir = mkdtempSync(join(tmpdir(), "opencode-quota-dist-tag-")); + try { + const output = join(dir, "github-output"); + execFileSync("bash", ["-e", "-c", chooseTag.run ?? "exit 1"], { + env: { + ...process.env, + RELEASE_TAG: tag, + RELEASE_PRERELEASE: prerelease, + GITHUB_OUTPUT: output, + }, + stdio: "ignore", + }); + return readFileSync(output, "utf8"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }; + expect(distTagFor("v5.0.0-beta.1", "true")).toBe("dist-tag=next\n"); + expect(distTagFor("v5.0.0-beta.1", "false")).toBe("dist-tag=next\n"); + expect(distTagFor("v5.0.0", "true")).toBe("dist-tag=next\n"); + expect(distTagFor("v5.0.0", "false")).toBe("dist-tag=latest\n"); }); }); From 82081f858b5da89aeee3052c21aaa486aa981598 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:13:04 +0200 Subject: [PATCH 62/97] feat(cli): check the OpenCode version in init and update --- README.md | 4 +- docs/readme/updating.md | 12 ++- src/lib/init-installer.ts | 23 ++++++ src/lib/opencode-version.ts | 35 +++++++++ src/lib/scoped-update.ts | 50 +++++++++++-- tests/lib.init-installer.test.ts | 72 +++++++++++++++++- tests/lib.opencode-version.test.ts | 28 +++++++ tests/lib.scoped-update.test.ts | 115 +++++++++++++++++++++++++++++ 8 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 src/lib/opencode-version.ts create mode 100644 tests/lib.opencode-version.test.ts diff --git a/README.md b/README.md index c19ebd30..9aed32f9 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ npx @slkiser/opencode-quota init ``` > [!IMPORTANT] -> Requires OpenCode `2.0.16` or newer. Node.js `22.13+` is required for `npx @slkiser/opencode-quota ...` (on Node 23, `23.4+`). +> Requires OpenCode `2.0.16` or newer. On OpenCode 1? Use `npx @slkiser/opencode-quota@4 init`. Node.js `22.13+` is required for `npx @slkiser/opencode-quota ...` (on Node 23, `23.4+`). Upgrading from 4.x? Read [what changed in 5.0](#breaking-changes-in-500). @@ -65,7 +65,7 @@ The updater always shows a preview first. `--yes` applies only the safe edits it ### Breaking changes in 5.0.0 > [!WARNING] -> - OpenCode 1 is no longer supported. Stay on `4.10.3` if you still use it. +> - OpenCode 1 is no longer supported. `init` and `update` detect OpenCode 1 and keep you on 4.x (`@slkiser/opencode-quota@4`). > - Logins come only from OpenCode 2's `opencode.db`, never `auth.json`. If a provider is missing, log in again. > - Web and Desktop get the slash commands (report posts in the chat) but no toasts or panels. > - One `"plugin"` entry in `opencode.json` loads the server and the TUI; no `tui.json` entry is needed. diff --git a/docs/readme/updating.md b/docs/readme/updating.md index 8f20485b..038abee5 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -48,13 +48,21 @@ The updater builds one plan, prints it in full, and then either stops or applies What changed in `5.0.0`: -- **OpenCode 1 is no longer supported.** Stay on `4.10.3` if you still use OpenCode 1. +- **OpenCode 1 is no longer supported.** `init` and `update` detect OpenCode 1 and keep you on 4.x. See [OpenCode 1](#opencode-1). - **Logins come only from `opencode.db`.** OpenCode Quota reads logins only from OpenCode 2's database, never from `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. - **Web and Desktop have slash commands but no toasts or panels,** because OpenCode 2 gives plugins no Web UI hooks. On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. See [Web and Desktop notes](manual-install.md#web-and-desktop-notes). - **One plugin entry.** One `"plugin"` entry in `opencode.json` loads both the server and the TUI; no `tui.json` entry is needed. - **TUI reports open in a popup.** TUI slash commands and the command palette open the report in a popup and leave no chat message. `tuiCommandDisplay` now defaults to `"dialog"`; set `"inline"` to keep TUI slash reports in the chat instead. - **Removed setting.** `tuiCompactStatus.suppressWhenNativeProviderQuota` was removed; the old key is ignored. +## OpenCode 1 + +`init` and `update` run `opencode --version` first. + +- **OpenCode 2:** they work as described on this page. +- **OpenCode 1:** `init` stops without changing anything and tells you to run `npx @slkiser/opencode-quota@4 init`. `update` keeps you on 4.x: its preview proposes pinning bare, `@latest`, `@next`, and exact-version quota specs to `@slkiser/opencode-quota@4`, applied like any other safe change. +- **Unknown** (for example, `opencode` is not on your PATH): `init` asks which OpenCode you use. `update` assumes OpenCode 2 and prints a note; on OpenCode 1, set `"plugin": ["@slkiser/opencode-quota@4"]` yourself. + ## Read the preview The preview can contain three sections: @@ -76,7 +84,7 @@ Manual findings do not make the command fail. They remain your responsibility. The updater can: -- change supported OpenCode Quota plugin package specs to `@latest`; +- change supported OpenCode Quota plugin package specs to `@latest` (on OpenCode 1, pin them to `@4` instead); - remove only package-cache directories that pass path, symlink, containment, and exact package-manifest checks; - migrate recognized `opencodeZenDisplay` values in known file-backed quota config locations: - `"default"` becomes root `accountingDetail: "summary"`; diff --git a/src/lib/init-installer.ts b/src/lib/init-installer.ts index 4e669472..425ff005 100644 --- a/src/lib/init-installer.ts +++ b/src/lib/init-installer.ts @@ -25,6 +25,7 @@ import { validateConfigDocumentEdit, } from "./opencode-config-editor.js"; import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; +import { detectOpenCodeMajor } from "./opencode-version.js"; import { getQuotaProviderDisplayLabel, normalizeQuotaProviderId, @@ -1458,6 +1459,28 @@ export async function runInitInstaller(params?: { prompts.intro("Configure @slkiser/opencode-quota"); try { + let openCodeMajor = await detectOpenCodeMajor(); + if (openCodeMajor === undefined) { + const answer = await prompts.select({ + message: "Could not detect your OpenCode version. Which OpenCode do you use?", + initialValue: "2", + options: [ + { label: "OpenCode 2", value: "2", hint: "default" }, + { label: "OpenCode 1", value: "1" }, + ], + }); + if (prompts.isCancel(answer)) { + prompts.outro("OpenCode Quota setup cancelled — no files changed."); + return 0; + } + openCodeMajor = answer === "1" ? 1 : 2; + } + if (openCodeMajor === 1) { + prompts.log.error("You're on OpenCode 1. OpenCode Quota 5 needs OpenCode 2."); + prompts.outro("For OpenCode 1, run: npx @slkiser/opencode-quota@4 init — no files changed."); + return 1; + } + const pluginSpec = await resolveQuotaPluginSpec(); const selections = await promptForSelections(prompts, { cwd: params?.cwd, diff --git a/src/lib/opencode-version.ts b/src/lib/opencode-version.ts new file mode 100644 index 00000000..b209af5a --- /dev/null +++ b/src/lib/opencode-version.ts @@ -0,0 +1,35 @@ +import { execFile } from "node:child_process"; + +export type OpenCodeMajor = 1 | 2; + +const OPENCODE_VERSION_TIMEOUT_MS = 5_000; + +/** OpenCode 1 prints a bare `1.18.32`; OpenCode 2 prints `opencode v2.0.16`. */ +export function parseOpenCodeMajor(output: string): OpenCodeMajor | undefined { + const match = /^(?:opencode\s+)?v?(\d+)\.\d+\.\d+/.exec(output.trim()); + if (match?.[1] === "1") return 1; + if (match?.[1] === "2") return 2; + return undefined; +} + +function runOpenCodeVersion(): Promise { + return new Promise((resolve, reject) => { + execFile( + "opencode", + ["--version"], + { encoding: "utf8", timeout: OPENCODE_VERSION_TIMEOUT_MS, windowsHide: true }, + (error, stdout) => (error ? reject(error) : resolve(stdout)), + ); + }); +} + +/** Returns undefined when `opencode` is not on PATH, fails, times out, or prints an unknown version. */ +export async function detectOpenCodeMajor( + runVersion: () => Promise = runOpenCodeVersion, +): Promise { + try { + return parseOpenCodeMajor(await runVersion()); + } catch { + return undefined; + } +} diff --git a/src/lib/scoped-update.ts b/src/lib/scoped-update.ts index b8146349..e3906487 100644 --- a/src/lib/scoped-update.ts +++ b/src/lib/scoped-update.ts @@ -16,6 +16,7 @@ import { import { sanitizeSingleLineDisplayText } from "./display-sanitize.js"; import { editConfigDocumentPaths, parseConfigDocument } from "./opencode-config-editor.js"; import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; +import { detectOpenCodeMajor, type OpenCodeMajor } from "./opencode-version.js"; import { auditObsoleteUpdateSources, discoverExistingScopedUpdateMigrationCandidates, @@ -30,11 +31,12 @@ import { export const QUOTA_PACKAGE_NAME = "@slkiser/opencode-quota"; export const QUOTA_LATEST_SPEC = `${QUOTA_PACKAGE_NAME}@latest`; +export const QUOTA_V4_SPEC = `${QUOTA_PACKAGE_NAME}@4`; /** Moving specs that update keeps as written; OpenCode re-resolves them after the cache cleanup. */ -const QUOTA_MOVING_SPECS = new Set([ +const QUOTA_MOVING_SPECS: ReadonlySet = new Set([ QUOTA_LATEST_SPEC, `${QUOTA_PACKAGE_NAME}@next`, - `${QUOTA_PACKAGE_NAME}@4`, + QUOTA_V4_SPEC, ]); const GITHUB_REPO_URL = "https://github.com/slkiser/opencode-quota"; @@ -70,6 +72,7 @@ export interface ScopedUpdatePlan { foundSpecs: string[]; cacheCandidates: string[]; authoritativeLatest: boolean; + openCodeMajor: OpenCodeMajor | undefined; safeActions: ScopedUpdateSafeAction[]; manualFindings: ScopedUpdateManualFinding[]; } @@ -143,9 +146,20 @@ function pluginArrays(config: unknown): Array<{ path: (string | number)[]; entri return arrays; } +/** OpenCode Quota 5 needs OpenCode 2, so on OpenCode 1 every quota spec except `@4` is pinned to `@4`. */ +function quotaUpdateTarget(openCodeMajor: OpenCodeMajor | undefined): { + keptSpecs: ReadonlySet; + replacementSpec: string; +} { + return openCodeMajor === 1 + ? { keptSpecs: new Set([QUOTA_V4_SPEC]), replacementSpec: QUOTA_V4_SPEC } + : { keptSpecs: QUOTA_MOVING_SPECS, replacementSpec: QUOTA_LATEST_SPEC }; +} + function updateConfig( raw: string, path: string, + openCodeMajor: OpenCodeMajor | undefined, ): { updated: string; replacements: number; @@ -159,6 +173,7 @@ function updateConfig( throw new ScopedUpdateError(`Cannot update unparseable config: ${path}`, { path }); } + const target = quotaUpdateTarget(openCodeMajor); const edits: Array<{ path: (string | number)[]; value: unknown }> = []; let replacements = 0; const specs: string[] = []; @@ -175,7 +190,7 @@ function updateConfig( : null; if (spec === null || !isCanonicalQuotaUpdateSpec(spec)) continue; specs.push(spec); - if (QUOTA_MOVING_SPECS.has(spec)) continue; + if (target.keptSpecs.has(spec)) continue; // Replace only the package spec; keep the entry's plugin options. const targetPath = typeof entry === "string" @@ -183,7 +198,7 @@ function updateConfig( : Array.isArray(entry) ? [...array.path, index, 0] : [...array.path, index, "package"]; - edits.push({ path: targetPath, value: QUOTA_LATEST_SPEC }); + edits.push({ path: targetPath, value: target.replacementSpec }); replacements++; } } @@ -214,6 +229,7 @@ export async function planScopedUpdate( env?: NodeJS.ProcessEnv; homeDir?: string; platform?: NodeJS.Platform; + openCodeMajor?: OpenCodeMajor; } = {}, ): Promise { const cwd = params.cwd ?? process.cwd(); @@ -234,7 +250,7 @@ export async function planScopedUpdate( const canonicalPath = await realpath(path); const originalBytes = await readFile(path); const original = originalBytes.toString("utf8"); - const planned = updateConfig(original, path); + const planned = updateConfig(original, path, params.openCodeMajor); foundSpecs.push(...planned.specs); const roles = new Set(["package-authority"]); if (planned.replacements > 0) { @@ -359,7 +375,9 @@ export async function planScopedUpdate( } const uniqueSpecs = [...new Set(foundSpecs)]; - const cacheSpecs = [...new Set([...uniqueSpecs, QUOTA_LATEST_SPEC])]; + const cacheSpecs = [ + ...new Set([...uniqueSpecs, quotaUpdateTarget(params.openCodeMajor).replacementSpec]), + ]; const cacheCandidates = cacheSpecs.map((spec) => join(runtime.cacheDir, "packages", sanitizeOpenCodePackageSpec(spec, params.platform)), ); @@ -371,6 +389,7 @@ export async function planScopedUpdate( foundSpecs: uniqueSpecs, cacheCandidates: [...new Set(cacheCandidates)], authoritativeLatest: uniqueSpecs.length > 0, + openCodeMajor: params.openCodeMajor, safeActions: sortScopedUpdateSafeActions(safeActions), manualFindings: sortScopedUpdateManualFindings(manualFindings), }; @@ -480,6 +499,18 @@ function formatManualFinding(finding: ScopedUpdateManualFinding): string { export function formatScopedUpdatePreview(plan: ScopedUpdatePlan): string[] { const lines = ["Responsible OpenCode Quota update preview"]; + if (plan.openCodeMajor === 1) { + lines.push( + "", + `You're on OpenCode 1. OpenCode Quota 5 needs OpenCode 2, so update keeps you on OpenCode Quota 4 by pinning ${QUOTA_V4_SPEC}.`, + ); + } else if (plan.openCodeMajor === undefined) { + lines.push( + "", + `Could not detect your OpenCode version; assuming OpenCode 2. On OpenCode 1, use ${QUOTA_V4_SPEC} instead.`, + ); + } + if (plan.safeActions.length > 0) { lines.push("", "Safe changes this command can make:"); lines.push(...plan.safeActions.map(formatSafeAction)); @@ -626,7 +657,7 @@ export async function applyScopedUpdatePlan( throw failure("Config changed before cache deletion:", snapshot.path); } if (!snapshot.roles.includes("package-authority")) continue; - const currentPlan = updateConfig(current.toString("utf8"), snapshot.path); + const currentPlan = updateConfig(current.toString("utf8"), snapshot.path, plan.openCodeMajor); if (currentPlan.specs.some((spec) => QUOTA_MOVING_SPECS.has(spec))) authoritativeLatest = true; } @@ -660,7 +691,10 @@ export async function runScopedUpdateCommand( const yes = argv.includes("--yes"); const log = params.log ?? console.log; try { - const plan = await planScopedUpdate(params); + const plan = await planScopedUpdate({ + ...params, + openCodeMajor: await detectOpenCodeMajor(), + }); for (const line of formatScopedUpdatePreview(plan)) log(line); const hasConfigChanges = plan.configSnapshots.some((snapshot) => snapshot.changed); diff --git a/tests/lib.init-installer.test.ts b/tests/lib.init-installer.test.ts index bcbe8577..73b31dce 100644 --- a/tests/lib.init-installer.test.ts +++ b/tests/lib.init-installer.test.ts @@ -1,4 +1,12 @@ -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -17,6 +25,10 @@ const packageVersion = vi.hoisted(() => ({ value: "5.0.0" })); vi.mock("../src/lib/version.js", () => ({ getPackageVersion: async () => packageVersion.value, })); +const openCodeMajor = vi.hoisted(() => ({ value: 2 as 1 | 2 | undefined })); +vi.mock("../src/lib/opencode-version.js", () => ({ + detectOpenCodeMajor: async () => openCodeMajor.value, +})); function readJson(path: string): any { const resolvedPath = !existsSync(path) && path.endsWith(".json") ? `${path}c` : path; @@ -64,6 +76,7 @@ function createPromptStub(params: { const multiselectCalls: { message: string; required?: boolean; options: unknown[] }[] = []; const outroCalls: string[] = []; const infoCalls: string[] = []; + const errorCalls: string[] = []; const confirmCalls: { message: string; initialValue?: boolean }[] = []; return { @@ -89,12 +102,15 @@ function createPromptStub(params: { infoCalls.push(message); }, success: () => {}, - error: () => {}, + error: (message: string) => { + errorCalls.push(message); + }, }, selectCalls, multiselectCalls, outroCalls, infoCalls, + errorCalls, confirmCalls, }; } @@ -120,6 +136,58 @@ describe("init installer planning and merge behavior", () => { afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); packageVersion.value = "5.0.0"; + openCodeMajor.value = 2; + }); + + it("stops on OpenCode 1 before prompting or writing anything", async () => { + openCodeMajor.value = 1; + const prompts = createPromptStub({ selectValues: DEFAULT_PROMPT_SELECT_VALUES }); + + const code = await runInitInstaller({ cwd: tempDir, prompts: prompts as any }); + + expect(code).toBe(1); + expect(prompts.selectCalls).toEqual([]); + expect(prompts.errorCalls).toEqual([ + "You're on OpenCode 1. OpenCode Quota 5 needs OpenCode 2.", + ]); + expect(prompts.outroCalls).toEqual([ + "For OpenCode 1, run: npx @slkiser/opencode-quota@4 init — no files changed.", + ]); + expect(readdirSync(tempDir)).toEqual([]); + }); + + it("asks which OpenCode is used when the version is unknown and stops on OpenCode 1", async () => { + openCodeMajor.value = undefined; + const prompts = createPromptStub({ selectValues: ["1", ...DEFAULT_PROMPT_SELECT_VALUES] }); + + const code = await runInitInstaller({ cwd: tempDir, prompts: prompts as any }); + + expect(code).toBe(1); + expect(prompts.selectCalls).toHaveLength(1); + expect(prompts.selectCalls[0]).toMatchObject({ + message: "Could not detect your OpenCode version. Which OpenCode do you use?", + initialValue: "2", + }); + expect(prompts.outroCalls).toEqual([ + "For OpenCode 1, run: npx @slkiser/opencode-quota@4 init — no files changed.", + ]); + expect(readdirSync(tempDir)).toEqual([]); + }); + + it("continues setup when an unknown OpenCode version is answered as OpenCode 2", async () => { + openCodeMajor.value = undefined; + const prompts = createPromptStub({ + selectValues: ["2", ...DEFAULT_PROMPT_SELECT_VALUES], + multiselectValues: [["toast"]], + confirmValues: [true, true], + }); + + const code = await runInitInstaller({ cwd: tempDir, prompts: prompts as any }); + + expect(code).toBe(0); + expect(readJson(join(tempDir, "opencode.jsonc")).plugin).toEqual([ + "@slkiser/opencode-quota@latest", + ]); }); it("creates recommended project opencode.jsonc at the worktree root for toast mode", async () => { diff --git a/tests/lib.opencode-version.test.ts b/tests/lib.opencode-version.test.ts new file mode 100644 index 00000000..2c899ed2 --- /dev/null +++ b/tests/lib.opencode-version.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { detectOpenCodeMajor, parseOpenCodeMajor } from "../src/lib/opencode-version.js"; + +describe("OpenCode version detection", () => { + it.each([ + ["1.18.32\n", 1], + ["opencode v2.0.16\n", 2], + ["opencode v2.1.0-beta.1", 2], + ["v1.0.0", 1], + ["0.15.8", undefined], + ["opencode v3.0.0", undefined], + ["", undefined], + ["command not found", undefined], + ])("parses %j as %j", (output, major) => { + expect(parseOpenCodeMajor(output)).toBe(major); + }); + + it("runs the version command through the given runner", async () => { + expect(await detectOpenCodeMajor(async () => "1.18.32\n")).toBe(1); + expect(await detectOpenCodeMajor(async () => "opencode v2.0.16\n")).toBe(2); + }); + + it("returns undefined when the command fails or times out", async () => { + const missing = Object.assign(new Error("spawn opencode ENOENT"), { code: "ENOENT" }); + expect(await detectOpenCodeMajor(async () => Promise.reject(missing))).toBeUndefined(); + }); +}); diff --git a/tests/lib.scoped-update.test.ts b/tests/lib.scoped-update.test.ts index d9d4251f..17b3c8ca 100644 --- a/tests/lib.scoped-update.test.ts +++ b/tests/lib.scoped-update.test.ts @@ -10,10 +10,17 @@ import { isCanonicalQuotaUpdateSpec, planScopedUpdate, QUOTA_LATEST_SPEC, + QUOTA_V4_SPEC, runScopedUpdateCommand, sanitizeOpenCodePackageSpec, } from "../src/lib/scoped-update.js"; +// Commands must not run a real `opencode` binary; undefined matches plans built without a version. +const openCodeMajor = vi.hoisted(() => ({ value: undefined as 1 | 2 | undefined })); +vi.mock("../src/lib/opencode-version.js", () => ({ + detectOpenCodeMajor: async () => openCodeMajor.value, +})); + const tempDirs: string[] = []; function tempDir(): string { const path = mkdtempSync(join(tmpdir(), "opencode-quota-update-")); @@ -51,6 +58,7 @@ function fixture() { } afterEach(() => { + openCodeMajor.value = undefined; vi.restoreAllMocks(); vi.unstubAllEnvs(); for (const path of tempDirs.splice(0)) rmSync(path, { recursive: true, force: true }); @@ -1209,3 +1217,110 @@ describe("scoped update application safety", () => { expect(log.mock.calls.flat().join("\n")).not.toContain("star"); }); }); + +describe("scoped update OpenCode version handling", () => { + it("on OpenCode 1 pins bare, latest, next, and exact specs to @4 and keeps @4", async () => { + const f = fixture(); + const config = join(f.project, "opencode.json"); + write( + config, + `{"plugin":["@slkiser/opencode-quota",["@slkiser/opencode-quota@latest",{"setting":true}],"@slkiser/opencode-quota@next","@slkiser/opencode-quota@5.0.0","${QUOTA_V4_SPEC}","other"]}`, + ); + const plan = await planScopedUpdate({ + cwd: f.project, + env: f.env, + homeDir: join(f.root, "home"), + platform: "linux", + openCodeMajor: 1, + }); + expect(plan.safeActions).toEqual([{ kind: "package-spec", path: config, replacements: 4 }]); + expect(plan.cacheCandidates).toContain(join(f.cache, "packages", QUOTA_V4_SPEC)); + expect(formatScopedUpdatePreview(plan)).toContain( + `You're on OpenCode 1. OpenCode Quota 5 needs OpenCode 2, so update keeps you on OpenCode Quota 4 by pinning ${QUOTA_V4_SPEC}.`, + ); + + await applyScopedUpdatePlan(plan); + expect(JSON.parse(readFileSync(config, "utf8"))).toEqual({ + plugin: [ + QUOTA_V4_SPEC, + [QUOTA_V4_SPEC, { setting: true }], + QUOTA_V4_SPEC, + QUOTA_V4_SPEC, + QUOTA_V4_SPEC, + "other", + ], + }); + }); + + it("update --yes applies the @4 pin when it detects OpenCode 1", async () => { + openCodeMajor.value = 1; + const f = fixture(); + const config = join(f.project, "opencode.json"); + write(config, `{"plugin":["${QUOTA_LATEST_SPEC}"]}`); + const log = vi.fn(); + + expect( + await runScopedUpdateCommand({ + argv: ["--yes"], + cwd: f.project, + env: f.env, + homeDir: join(f.root, "home"), + platform: "linux", + log, + }), + ).toBe(0); + + const output = log.mock.calls.flat().join("\n"); + expect(output).toContain("You're on OpenCode 1."); + expect(output).toContain(`edit ${config} (1 package replacement)`); + expect(readFileSync(config, "utf8")).toBe(`{"plugin":["${QUOTA_V4_SPEC}"]}`); + }); + + it("on OpenCode 2 keeps moving specs and prints no version note", async () => { + openCodeMajor.value = 2; + const f = fixture(); + const config = join(f.project, "opencode.json"); + const original = `{"plugin":["${QUOTA_LATEST_SPEC}"]}`; + write(config, original); + const log = vi.fn(); + + expect( + await runScopedUpdateCommand({ + argv: ["--dry-run"], + cwd: f.project, + env: f.env, + homeDir: join(f.root, "home"), + platform: "linux", + log, + }), + ).toBe(0); + + const output = log.mock.calls.flat().join("\n"); + expect(output).not.toContain("OpenCode 1"); + expect(output).not.toContain("Could not detect"); + expect(readFileSync(config, "utf8")).toBe(original); + }); + + it("with an unknown OpenCode version updates as OpenCode 2 and prints a note", async () => { + const f = fixture(); + const config = join(f.project, "opencode.json"); + write(config, `{"plugin":["@slkiser/opencode-quota@4.10.3"]}`); + const log = vi.fn(); + + expect( + await runScopedUpdateCommand({ + argv: ["--yes"], + cwd: f.project, + env: f.env, + homeDir: join(f.root, "home"), + platform: "linux", + log, + }), + ).toBe(0); + + expect(log).toHaveBeenCalledWith( + `Could not detect your OpenCode version; assuming OpenCode 2. On OpenCode 1, use ${QUOTA_V4_SPEC} instead.`, + ); + expect(readFileSync(config, "utf8")).toBe(`{"plugin":["${QUOTA_LATEST_SPEC}"]}`); + }); +}); From 7143e4d930b2c0429df124255d81ad6ce4729eb7 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:14:57 +0200 Subject: [PATCH 63/97] test: never let test git commands touch the real repository (cherry picked from commit 396e7920f7c8dedcb0492b054d80abb10d921adf) --- tests/helpers/isolated-git-env.ts | 22 +++++++++++++++ tests/release-gates.test.ts | 43 +++++++++++++++++++++++++++--- tests/upstream-plugin-lock.test.ts | 9 +++++-- 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 tests/helpers/isolated-git-env.ts diff --git a/tests/helpers/isolated-git-env.ts b/tests/helpers/isolated-git-env.ts new file mode 100644 index 00000000..00e84788 --- /dev/null +++ b/tests/helpers/isolated-git-env.ts @@ -0,0 +1,22 @@ +import { vi } from "vitest"; + +// Git hooks (for example the lefthook pre-push `pnpm verify`) export GIT_DIR, +// GIT_INDEX_FILE, and similar variables. A child git process that inherits them +// ignores its cwd and writes to the real repository instead of the temp folder. +function isGitEnvKey(key: string): boolean { + return key.startsWith("GIT_"); +} + +/** process.env for a git child process that must only touch the repo in its cwd. */ +export function isolatedGitEnv(): NodeJS.ProcessEnv { + const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !isGitEnvKey(key))); + return { ...env, GIT_CONFIG_NOSYSTEM: "1" }; +} + +/** Same isolation for git spawned inside code under test, which reads process.env itself. */ +export function stubIsolatedGitEnv(): void { + for (const key of Object.keys(process.env)) { + if (isGitEnvKey(key)) vi.stubEnv(key, undefined); + } + vi.stubEnv("GIT_CONFIG_NOSYSTEM", "1"); +} diff --git a/tests/release-gates.test.ts b/tests/release-gates.test.ts index 941b2456..9d050811 100644 --- a/tests/release-gates.test.ts +++ b/tests/release-gates.test.ts @@ -5,7 +5,9 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { isolatedGitEnv } from "./helpers/isolated-git-env.js"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); const packageScript = fileURLToPath( @@ -29,12 +31,12 @@ function run(script: string, args: string[] = [], env: NodeJS.ProcessEnv = {}) { return spawnSync(process.execPath, [script, ...args], { cwd: repoRoot, encoding: "utf8", - env: { ...process.env, ...env }, + env: { ...isolatedGitEnv(), ...env }, }); } function git(cwd: string, args: string[]): string { - const result = spawnSync("git", args, { cwd, encoding: "utf8" }); + const result = spawnSync("git", args, { cwd, encoding: "utf8", env: isolatedGitEnv() }); if (result.status !== 0) { throw new Error(`git ${args.join(" ")} failed: ${result.stderr || result.stdout}`); } @@ -134,6 +136,41 @@ describe("v4 release gates", () => { expect(history.stdout).toContain("V4 history privacy verified"); }); + it("keeps temp-repo git away from the repository named by a git hook's GIT_DIR", async () => { + const hookRepo = path.join(tempDir, "hook-repo"); + await mkdir(hookRepo); + git(hookRepo, ["init", "-q"]); + git(hookRepo, [ + "-c", + "user.name=Hook Repo", + "-c", + "user.email=hook-repo@example.invalid", + "commit", + "--allow-empty", + "-qm", + "hook repo head", + ]); + const hookRepoHead = git(hookRepo, ["rev-parse", "HEAD"]); + const hookGitDir = path.join(hookRepo, ".git"); + + vi.stubEnv("GIT_DIR", hookGitDir); + vi.stubEnv("GIT_INDEX_FILE", path.join(hookGitDir, "index")); + const historyRepo = path.join(tempDir, "history-under-hook"); + const base = await createHistoryRepo(historyRepo); + const history = run(historyScript, [], { + V4_HISTORY_BASE: base, + V4_HISTORY_REPO: historyRepo, + }); + vi.unstubAllEnvs(); + + expect(history.status).toBe(0); + expect(git(historyRepo, ["log", "-1", "--format=%H %s"])).toBe(`${base} base`); + expect(git(hookRepo, ["rev-parse", "HEAD"])).toBe(hookRepoHead); + expect(git(hookRepo, ["config", "--get", "core.bare"])).toBe("false"); + expect(git(hookRepo, ["config", "--local", "--list"])).not.toContain("user."); + expect(git(hookRepo, ["ls-files"])).toBe(""); + }); + it("rejects every forced-added private path from temporary commit history", async () => { const historyRepo = path.join(tempDir, "history"); const base = await createHistoryRepo(historyRepo); diff --git a/tests/upstream-plugin-lock.test.ts b/tests/upstream-plugin-lock.test.ts index cf7dbdd1..51670da2 100644 --- a/tests/upstream-plugin-lock.test.ts +++ b/tests/upstream-plugin-lock.test.ts @@ -6,6 +6,8 @@ import { promisify } from "node:util"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { isolatedGitEnv, stubIsolatedGitEnv } from "./helpers/isolated-git-env.js"; + const execFileAsync = promisify(execFile); const testState = vi.hoisted(() => ({ @@ -72,9 +74,10 @@ describe("upstream-plugin-lock", () => { `${JSON.stringify({ plugins: { "cursor-opencode-provider": previousEntry } }, null, 2)}\n`, "utf8", ); - await execFileAsync("git", ["init"], { cwd: testState.repoRoot }); + await execFileAsync("git", ["init"], { cwd: testState.repoRoot, env: isolatedGitEnv() }); await execFileAsync("git", ["add", "references/upstream-plugins/lock.json"], { cwd: testState.repoRoot, + env: isolatedGitEnv(), }); await execFileAsync( "git", @@ -89,7 +92,7 @@ describe("upstream-plugin-lock", () => { "-m", "baseline", ], - { cwd: testState.repoRoot }, + { cwd: testState.repoRoot, env: isolatedGitEnv() }, ); const currentEntry = { @@ -110,6 +113,8 @@ describe("upstream-plugin-lock", () => { const { buildChangedPluginSummaries } = await import( "../scripts/lib/upstream-plugin-review.mjs" ); + // readCommittedUpstreamPluginLock runs `git show` with the inherited process.env. + stubIsolatedGitEnv(); const committedLock = await readCommittedUpstreamPluginLock({ repositoryRoot: testState.repoRoot, lockPath, From 78003a3ed2b25310b7487c3b1f67841801917041 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:00:34 +0200 Subject: [PATCH 64/97] feat(server): compute quota surfaces on the server behind a plugin RPC Move the TUI's surface computation (sidebar, toasts, prompt line, Home footer, export write) into src/lib/quota-surface-data.ts and the command specs into src/lib/quota-dialog-command-specs.ts. The server plugin registers the slkiser.opencode-quota RPC (surface, footer, writeExport, command) in setup. The TUI still computes locally and the credential reader is unchanged, so product behavior stays the same. --- src/lib/quota-dialog-command-specs.ts | 201 ++++++++++ src/lib/quota-dialog-commands.ts | 203 +--------- src/lib/quota-report-message.ts | 2 +- src/lib/quota-surface-data.ts | 376 ++++++++++++++++++ src/plugin.ts | 94 ++++- src/rpc.ts | 117 ++++++ src/tui-v2.tsx | 372 +---------------- tests/lib.quota-surface-data.test.ts | 223 +++++++++++ tests/plugin-rpc.test.ts | 287 +++++++++++++ tests/plugin-v2.test.ts | 3 +- tests/plugin.command-handled-boundary.test.ts | 5 +- tests/plugin.maintainer-announcements.test.ts | 2 +- tests/plugin.quota-command.test.ts | 4 + tests/plugin.tokens-session-all.test.ts | 2 +- tests/provider-surfaces.opencode-go.test.ts | 1 + tests/rpc-definition.test.ts | 144 +++++++ tests/tui-dist-packaging.test.ts | 2 +- tests/v4-phase5-cross-surface.test.ts | 1 + 18 files changed, 1464 insertions(+), 575 deletions(-) create mode 100644 src/lib/quota-dialog-command-specs.ts create mode 100644 src/lib/quota-surface-data.ts create mode 100644 src/rpc.ts create mode 100644 tests/lib.quota-surface-data.test.ts create mode 100644 tests/plugin-rpc.test.ts create mode 100644 tests/rpc-definition.test.ts diff --git a/src/lib/quota-dialog-command-specs.ts b/src/lib/quota-dialog-command-specs.ts new file mode 100644 index 00000000..81fe218a --- /dev/null +++ b/src/lib/quota-dialog-command-specs.ts @@ -0,0 +1,201 @@ +/** + * Ids, titles and dialog sizes of the quota commands. They live apart from the report + * builders in quota-dialog-commands.ts so code that only lists or names the commands + * does not load the quota and token report code. + */ +import { formatYmd, type Ymd } from "./command-parsing.js"; + +export type QuotaDialogCommandId = + | "quota" + | "quota_status" + | "quota_announcements" + | "pricing_refresh" + | TokenReportCommandId; + +export type QuotaDialogCommandSpec = { + id: QuotaDialogCommandId; + slashName: string; + title: string; + description: string; + dialogSize: "medium" | "large" | "xlarge"; + requiresSession?: boolean; +}; + +export type QuotaDialogCommandOutputResult = + | { + state: "output"; + command: QuotaDialogCommandId; + title: string; + output: string; + dialogSize: "medium" | "large" | "xlarge"; + } + | { + state: "noop"; + command: QuotaDialogCommandId; + reason: "disabled"; + }; + +export type TokenReportCommandId = + | "tokens_today" + | "tokens_daily" + | "tokens_weekly" + | "tokens_monthly" + | "tokens_all" + | "tokens_session" + | "tokens_session_all" + | "tokens_between"; + +export type TokenReportCommandSpec = + | { + id: Exclude; + template: `/${string}`; + description: string; + title: string; + metadataTitle: string; + kind: "rolling" | "today" | "all" | "session" | "session_tree"; + windowMs?: number; + topModels?: number; + topSessions?: number; + } + | { + id: "tokens_between"; + template: "/tokens_between"; + description: string; + titleForRange: (startYmd: Ymd, endYmd: Ymd) => string; + metadataTitle: string; + kind: "between"; + }; + +export const TOKEN_REPORT_COMMANDS: readonly TokenReportCommandSpec[] = [ + { + id: "tokens_today", + template: "/tokens_today", + description: "Token + deterministic cost summary for today (calendar day, local timezone).", + title: "Tokens used (Today) (/tokens_today)", + metadataTitle: "Tokens used (Today)", + kind: "today", + }, + { + id: "tokens_daily", + template: "/tokens_daily", + description: "Token + deterministic cost summary for the last 24 hours (rolling).", + title: "Tokens used (Last 24 Hours) (/tokens_daily)", + metadataTitle: "Tokens used (Last 24 Hours)", + kind: "rolling", + windowMs: 24 * 60 * 60 * 1000, + }, + { + id: "tokens_weekly", + template: "/tokens_weekly", + description: "Token + deterministic cost summary for the last 7 days (rolling).", + title: "Tokens used (Last 7 Days) (/tokens_weekly)", + metadataTitle: "Tokens used (Last 7 Days)", + kind: "rolling", + windowMs: 7 * 24 * 60 * 60 * 1000, + }, + { + id: "tokens_monthly", + template: "/tokens_monthly", + description: "Token + deterministic cost summary for the last 30 days (rolling).", + title: "Tokens used (Last 30 Days) (/tokens_monthly)", + metadataTitle: "Tokens used (Last 30 Days)", + kind: "rolling", + windowMs: 30 * 24 * 60 * 60 * 1000, + }, + { + id: "tokens_all", + template: "/tokens_all", + description: "Token + deterministic cost summary for all locally saved OpenCode history.", + title: "Tokens used (All Time) (/tokens_all)", + metadataTitle: "Tokens used (All Time)", + kind: "all", + topModels: 12, + topSessions: 12, + }, + { + id: "tokens_session", + template: "/tokens_session", + description: "Token + deterministic cost summary for current session only.", + title: "Tokens used (Current Session) (/tokens_session)", + metadataTitle: "Tokens used (Current Session)", + kind: "session", + }, + { + id: "tokens_session_all", + template: "/tokens_session_all", + description: + "Token + deterministic cost summary for current session and all descendant child/subagent sessions.", + title: "Tokens used (Current Session Tree) (/tokens_session_all)", + metadataTitle: "Tokens used (Current Session Tree)", + kind: "session_tree", + }, + { + id: "tokens_between", + template: "/tokens_between", + description: + "Token + deterministic cost report between two YYYY-MM-DD dates (local timezone, inclusive).", + titleForRange: (startYmd: Ymd, endYmd: Ymd) => { + return `Tokens used (${formatYmd(startYmd)} .. ${formatYmd(endYmd)}) (/tokens_between)`; + }, + metadataTitle: "Tokens used (Date Range)", + kind: "between", + }, +] as const; + +export const QUOTA_DIALOG_COMMANDS: readonly QuotaDialogCommandSpec[] = [ + { + id: "quota", + slashName: "quota", + title: "OpenCode Quota", + description: "Show deterministic quota output.", + dialogSize: "xlarge", + requiresSession: true, + }, + { + id: "quota_status", + slashName: "quota_status", + title: "OpenCode Quota Status", + description: "Diagnostics for quota, TUI, pricing, and local storage.", + dialogSize: "xlarge", + requiresSession: true, + }, + { + id: "quota_announcements", + slashName: "quota_announcements", + title: "OpenCode Quota Announcements", + description: "List active bundled maintainer announcements.", + dialogSize: "xlarge", + }, + { + id: "pricing_refresh", + slashName: "pricing_refresh", + title: "OpenCode Quota Pricing Refresh", + description: "Refresh the local runtime pricing snapshot from models.dev.", + dialogSize: "xlarge", + }, + ...TOKEN_REPORT_COMMANDS.map( + (spec): QuotaDialogCommandSpec => ({ + id: spec.id, + slashName: spec.id, + title: spec.kind === "between" ? "OpenCode Quota Token Report" : spec.metadataTitle, + description: spec.description, + dialogSize: "xlarge", + requiresSession: spec.kind === "session" || spec.kind === "session_tree", + }), + ), +] as const; + +export const QUOTA_DIALOG_COMMANDS_BY_ID: ReadonlyMap< + QuotaDialogCommandId, + QuotaDialogCommandSpec +> = (() => { + const map = new Map(); + for (const spec of QUOTA_DIALOG_COMMANDS) { + map.set(spec.id, spec); + } + return map; +})(); + +export function isQuotaDialogCommand(command: string): command is QuotaDialogCommandId { + return QUOTA_DIALOG_COMMANDS_BY_ID.has(command as QuotaDialogCommandId); +} diff --git a/src/lib/quota-dialog-commands.ts b/src/lib/quota-dialog-commands.ts index f9bc295c..ff81123e 100644 --- a/src/lib/quota-dialog-commands.ts +++ b/src/lib/quota-dialog-commands.ts @@ -1,9 +1,7 @@ import { - formatYmd, parseQuotaBetweenArgs, startOfLocalDayMs, startOfNextLocalDayMs, - type Ymd, } from "./command-parsing.js"; import type { RuntimeContextRootHints } from "./config-file-utils.js"; import { isCursorProviderId } from "./cursor-pricing.js"; @@ -23,6 +21,14 @@ import { setPricingSnapshotSelection, } from "./modelsdev-pricing.js"; import { formatQuotaCommand } from "./quota-command-format.js"; +import { + QUOTA_DIALOG_COMMANDS_BY_ID, + type QuotaDialogCommandId, + type QuotaDialogCommandOutputResult, + TOKEN_REPORT_COMMANDS, + type TokenReportCommandId, + type TokenReportCommandSpec, +} from "./quota-dialog-command-specs.js"; import { ALL_WINDOWS_FORMAT_STYLE } from "./quota-format-style.js"; import { type CollectQuotaRenderDataResult, @@ -51,143 +57,6 @@ import { buildQuotaStatusReport, type SessionTokenError } from "./quota-status.j import type { PricingSnapshotSource } from "./types.js"; import { getPackageVersion } from "./version.js"; -export type QuotaDialogCommandId = - | "quota" - | "quota_status" - | "quota_announcements" - | "pricing_refresh" - | TokenReportCommandId; - -export type QuotaDialogCommandSpec = { - id: QuotaDialogCommandId; - slashName: string; - title: string; - description: string; - dialogSize: "medium" | "large" | "xlarge"; - requiresSession?: boolean; -}; - -export type QuotaDialogCommandOutputResult = - | { - state: "output"; - command: QuotaDialogCommandId; - title: string; - output: string; - dialogSize: "medium" | "large" | "xlarge"; - } - | { - state: "noop"; - command: QuotaDialogCommandId; - reason: "disabled"; - }; - -type TokenReportCommandId = - | "tokens_today" - | "tokens_daily" - | "tokens_weekly" - | "tokens_monthly" - | "tokens_all" - | "tokens_session" - | "tokens_session_all" - | "tokens_between"; - -type TokenReportCommandSpec = - | { - id: Exclude; - template: `/${string}`; - description: string; - title: string; - metadataTitle: string; - kind: "rolling" | "today" | "all" | "session" | "session_tree"; - windowMs?: number; - topModels?: number; - topSessions?: number; - } - | { - id: "tokens_between"; - template: "/tokens_between"; - description: string; - titleForRange: (startYmd: Ymd, endYmd: Ymd) => string; - metadataTitle: string; - kind: "between"; - }; - -const TOKEN_REPORT_COMMANDS: readonly TokenReportCommandSpec[] = [ - { - id: "tokens_today", - template: "/tokens_today", - description: "Token + deterministic cost summary for today (calendar day, local timezone).", - title: "Tokens used (Today) (/tokens_today)", - metadataTitle: "Tokens used (Today)", - kind: "today", - }, - { - id: "tokens_daily", - template: "/tokens_daily", - description: "Token + deterministic cost summary for the last 24 hours (rolling).", - title: "Tokens used (Last 24 Hours) (/tokens_daily)", - metadataTitle: "Tokens used (Last 24 Hours)", - kind: "rolling", - windowMs: 24 * 60 * 60 * 1000, - }, - { - id: "tokens_weekly", - template: "/tokens_weekly", - description: "Token + deterministic cost summary for the last 7 days (rolling).", - title: "Tokens used (Last 7 Days) (/tokens_weekly)", - metadataTitle: "Tokens used (Last 7 Days)", - kind: "rolling", - windowMs: 7 * 24 * 60 * 60 * 1000, - }, - { - id: "tokens_monthly", - template: "/tokens_monthly", - description: "Token + deterministic cost summary for the last 30 days (rolling).", - title: "Tokens used (Last 30 Days) (/tokens_monthly)", - metadataTitle: "Tokens used (Last 30 Days)", - kind: "rolling", - windowMs: 30 * 24 * 60 * 60 * 1000, - }, - { - id: "tokens_all", - template: "/tokens_all", - description: "Token + deterministic cost summary for all locally saved OpenCode history.", - title: "Tokens used (All Time) (/tokens_all)", - metadataTitle: "Tokens used (All Time)", - kind: "all", - topModels: 12, - topSessions: 12, - }, - { - id: "tokens_session", - template: "/tokens_session", - description: "Token + deterministic cost summary for current session only.", - title: "Tokens used (Current Session) (/tokens_session)", - metadataTitle: "Tokens used (Current Session)", - kind: "session", - }, - { - id: "tokens_session_all", - template: "/tokens_session_all", - description: - "Token + deterministic cost summary for current session and all descendant child/subagent sessions.", - title: "Tokens used (Current Session Tree) (/tokens_session_all)", - metadataTitle: "Tokens used (Current Session Tree)", - kind: "session_tree", - }, - { - id: "tokens_between", - template: "/tokens_between", - description: - "Token + deterministic cost report between two YYYY-MM-DD dates (local timezone, inclusive).", - titleForRange: (startYmd: Ymd, endYmd: Ymd) => { - return `Tokens used (${formatYmd(startYmd)} .. ${formatYmd(endYmd)}) (/tokens_between)`; - }, - metadataTitle: "Tokens used (Date Range)", - kind: "between", - }, -] as const; - // Token report model names are capped at the length of this reference model. const TUI_TOKEN_REPORT_MODEL_NAME_WIDTH_REFERENCE = "gemini-3-pro-preview"; const TUI_TOKEN_REPORT_MODEL_MAX_WIDTH = TUI_TOKEN_REPORT_MODEL_NAME_WIDTH_REFERENCE.length; @@ -201,62 +70,6 @@ const TOKEN_REPORT_COMMANDS_BY_ID: ReadonlyMap ({ - id: spec.id, - slashName: spec.id, - title: spec.kind === "between" ? "OpenCode Quota Token Report" : spec.metadataTitle, - description: spec.description, - dialogSize: "xlarge", - requiresSession: spec.kind === "session" || spec.kind === "session_tree", - }), - ), -] as const; - -const QUOTA_DIALOG_COMMANDS_BY_ID: ReadonlyMap = - (() => { - const map = new Map(); - for (const spec of QUOTA_DIALOG_COMMANDS) { - map.set(spec.id, spec); - } - return map; - })(); - -export function isQuotaDialogCommand(command: string): command is QuotaDialogCommandId { - return QUOTA_DIALOG_COMMANDS_BY_ID.has(command as QuotaDialogCommandId); -} - function isTokenReportCommand(cmd: string): cmd is TokenReportCommandId { return TOKEN_REPORT_COMMANDS_BY_ID.has(cmd as TokenReportCommandId); } diff --git a/src/lib/quota-report-message.ts b/src/lib/quota-report-message.ts index fe9b1c61..bc111dd6 100644 --- a/src/lib/quota-report-message.ts +++ b/src/lib/quota-report-message.ts @@ -3,7 +3,7 @@ * that never starts a model turn. These helpers mark such messages so the server plugin * can keep them out of every model request, and the TUI can show them in its dialog. */ -import type { QuotaDialogCommandId } from "./quota-dialog-commands.js"; +import type { QuotaDialogCommandId } from "./quota-dialog-command-specs.js"; /** Metadata key on every quota report message. OpenCode stores message metadata with the message. */ export const QUOTA_REPORT_METADATA_KEY = "opencodeQuota"; diff --git a/src/lib/quota-surface-data.ts b/src/lib/quota-surface-data.ts new file mode 100644 index 00000000..bcffdda1 --- /dev/null +++ b/src/lib/quota-surface-data.ts @@ -0,0 +1,376 @@ +/** + * Computes the text of the TUI quota surfaces: the sidebar panel, the toasts, the + * session prompt line and the Home footer, plus the optional export file. The server + * plugin serves these over its RPC; the TUI still calls them in-process. + */ +import type { RuntimeContextRootHints } from "./config-file-utils.js"; +import { sanitizeDisplayText } from "./display-sanitize.js"; +import { formatQuotaRows } from "./format.js"; +import { + BUNDLED_MAINTAINER_ANNOUNCEMENTS, + formatMaintainerAnnouncementHomeCountLine, + getMaintainerAnnouncementsSummary, + getMaintainerAnnouncementTargetProviderIds, +} from "./maintainer-announcements.js"; +import { getQuotaProviderShape, normalizeQuotaProviderId } from "./provider-metadata.js"; +import { + buildQuotaExport, + createExportProviderContext, + resolveExportPath, + writeQuotaExport, +} from "./quota-export.js"; +import { resolveQuotaFormatStyle } from "./quota-format-style.js"; +import { + type CollectQuotaRenderDataResult, + collectConcreteEnabledProviderIds, + collectQuotaRenderData, +} from "./quota-render-data.js"; +import { + formatQuotaResetNotification, + observeQuotaResetNotifications, +} from "./quota-reset-notifications.js"; +import { + createQuotaProviderRuntimeContext, + createQuotaRuntimeRequestContext, + type QuotaRuntimeClient, + type QuotaRuntimeContext, + type QuotaSessionModelContext, + resolveQuotaRuntimeContext, +} from "./quota-runtime-context.js"; +import { buildCompactQuotaStatusLine } from "./tui-compact-format.js"; +import { + formatPromptBarPercentMeta, + pickPromptBarEntry, + resolvePromptBarLabel, +} from "./tui-prompt-bar-format.js"; +import { buildSidebarQuotaPanelLines } from "./tui-sidebar-format.js"; +import type { QuotaToastConfig } from "./types.js"; + +/** Where the surfaces read settings, provider ids and the session model from. */ +export type QuotaSurfaceHost = { + client: QuotaRuntimeClient; + roots: RuntimeContextRootHints; + resolveSessionMeta: (sessionID: string) => Promise; +}; + +export async function getQuotaMessage( + host: QuotaSurfaceHost, + sessionID: string, + surface: "sidebar" | "idle" | "compacted" | "question", +): Promise< + | { message: string; duration: number; activeProviderCount: number; resetNotification?: string } + | undefined +> { + const runtime = await resolveQuotaRuntimeContext({ + client: host.client, + roots: host.roots, + sessionID, + resolveSessionMeta: host.resolveSessionMeta, + includeSessionMeta: (config) => config.onlyCurrentModel, + }); + const config = runtime.config; + if (!config.enabled) return; + if (surface === "sidebar") { + if (!config.tuiSidebarPanel.enabled) return; + } else if (!config.enableToast) { + return; + } + if ( + (surface === "idle" && !config.showOnIdle) || + (surface === "compacted" && !config.showOnCompact) || + (surface === "question" && !config.showOnQuestion) + ) { + return; + } + + if ( + surface !== "sidebar" && + config.debug && + config.enabledProviders !== "auto" && + config.enabledProviders.length === 0 + ) { + return { + message: sanitizeDisplayText( + formatQuotaToastDebugInfo({ + trigger: surface, + reason: "enabledProviders empty", + config, + configMeta: runtime.configMeta, + }), + ), + duration: config.toastDurationMs, + activeProviderCount: 0, + }; + } + + const rootFormatStyle = resolveQuotaFormatStyle(config.formatStyle); + const formatStyle = + surface === "sidebar" && config.tuiSidebarPanel.formatStyle + ? resolveQuotaFormatStyle(config.tuiSidebarPanel.formatStyle) + : rootFormatStyle; + const result = await collectQuotaRenderData({ + client: runtime.client, + resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, + config, + configMeta: runtime.configMeta, + request: createQuotaRuntimeRequestContext(runtime), + surfaceExplicitProviderIssues: true, + formatStyle, + providers: runtime.providers, + }); + let resetNotification: string | undefined; + if ( + surface !== "sidebar" && + config.resetNotifications.enabled && + result.providerResults.length > 0 + ) { + try { + const notices = await observeQuotaResetNotifications({ + providers: result.providerResults, + windows: config.resetNotifications.windows, + }); + resetNotification = formatQuotaResetNotification(notices) ?? undefined; + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + console.warn(`[opencode-quota] failed to observe quota reset transitions: ${reason}`); + } + } + const data = result.data; + const message = + surface === "sidebar" + ? data + ? buildSidebarQuotaPanelLines({ + data, + config: { + ...config, + formatStyle, + }, + }).join("\n") + : undefined + : getToastMessage({ trigger: surface, runtime, result, style: rootFormatStyle }); + return message + ? { + message: sanitizeDisplayText(message), + duration: config.toastDurationMs, + activeProviderCount: result.active.length, + resetNotification, + } + : undefined; +} + +function getToastMessage(params: { + trigger: string; + runtime: QuotaRuntimeContext; + result: CollectQuotaRenderDataResult; + style: ReturnType; +}): string | undefined { + const config = params.runtime.config; + const { availability, active, hasExplicitProviderIssues, data } = params.result; + const debugInfo = (reason: string) => + formatQuotaToastDebugInfo({ + trigger: params.trigger, + reason, + config, + configMeta: params.runtime.configMeta, + currentModel: params.result.selection?.currentModel, + availability: availability.map((item) => ({ id: item.provider.id, ok: item.ok })), + }); + + if (data?.entries.length || data?.sessionTokens) { + const formatted = formatQuotaRows({ + version: "2.0.0", + layout: config.layout, + entries: data?.entries ?? [], + errors: data?.errors ?? [], + style: params.style, + percentDisplayMode: config.percentDisplayMode, + resetTimeDecimals: config.resetTimeDecimals, + sessionTokens: data?.sessionTokens, + }); + if (!config.debug) return formatted; + const debugFooter = `\n\n[debug] src=${params.runtime.configMeta.source} providers=${config.enabledProviders === "auto" ? "(auto)" : config.enabledProviders.join(",") || "(none)"} avail=${availability + .map((item) => `${item.provider.id}:${item.ok ? "ok" : "no"}`) + .join(" ")}`; + return formatted + debugFooter; + } + + if (config.showOnBothFail && data?.errors.length) { + const errorLines = data.errors.map((error) => `${error.label}: ${error.message}`).join("\n"); + if (!config.debug) return errorLines; + return `${errorLines}\n\n${debugInfo( + hasExplicitProviderIssues ? "providers missing/unavailable" : "all providers failed", + )}`; + } + + if (!config.debug) return undefined; + return debugInfo(active.length === 0 ? "no enabled providers available" : "no entries"); +} + +function formatQuotaToastDebugInfo(params: { + trigger: string; + reason: string; + config: QuotaToastConfig; + configMeta: Pick; + currentModel?: string; + availability?: Array<{ id: string; ok: boolean }>; +}): string { + const availability = params.availability + ? params.availability.map((item) => `${item.id}=${item.ok ? "ok" : "no"}`).join(" ") + : "unknown"; + + const providers = + params.config.enabledProviders === "auto" + ? "(auto)" + : params.config.enabledProviders.length > 0 + ? params.config.enabledProviders.join(",") + : "(none)"; + + const modelPart = params.currentModel ? ` model=${params.currentModel}` : ""; + const paths = params.configMeta.paths.length > 0 ? params.configMeta.paths.join(" | ") : "(none)"; + + return [ + "Quota Toast Debug (opencode-quota)", + `trigger=${params.trigger} reason=${params.reason}`, + `configSource=${params.configMeta.source} paths=${paths}`, + `enabled=${params.config.enabled} providers=${providers}${modelPart}`, + `available=${availability}`, + ].join("\n"); +} + +async function getHomeAnnouncementText(runtime: QuotaRuntimeContext): Promise { + const announcements = BUNDLED_MAINTAINER_ANNOUNCEMENTS; + const targetProviderIds = new Set(getMaintainerAnnouncementTargetProviderIds({ announcements })); + const announcementProviders = runtime.providers.filter((provider) => { + const shape = getQuotaProviderShape(normalizeQuotaProviderId(provider.id)); + return shape ? targetProviderIds.has(shape.id) : false; + }); + const providerIds = await collectConcreteEnabledProviderIds({ + providers: announcementProviders, + ctx: createQuotaProviderRuntimeContext(runtime), + enabledProviders: runtime.config.enabledProviders, + }); + const summary = getMaintainerAnnouncementsSummary({ + enabledProviders: providerIds, + announcements, + }); + return formatMaintainerAnnouncementHomeCountLine(summary.activeCount); +} + +export async function getQuotaFooter( + host: QuotaSurfaceHost, + sessionID: string | undefined, + surface: "prompt" | "home", +): Promise { + // OpenCode 2 also mounts the prompt footer under the Home prompt, without a + // session. As in v4, the prompt line belongs to session prompts only. + if (surface === "prompt" && !sessionID) return []; + const resolvedRuntime = await resolveQuotaRuntimeContext({ + client: host.client, + roots: host.roots, + sessionID, + resolveSessionMeta: host.resolveSessionMeta, + includeSessionMeta: (config) => config.onlyCurrentModel && surface === "prompt", + }); + // Home has no session: as in v4, it shows every enabled provider and no session + // tokens, and shares its cache keys with the export (createExportProviderContext). + const runtime: QuotaRuntimeContext = + surface === "home" + ? { + ...resolvedRuntime, + config: { ...resolvedRuntime.config, onlyCurrentModel: false, showSessionTokens: false }, + session: {}, + } + : resolvedRuntime; + const config = runtime.config; + if (!config.enabled) return []; + if ( + surface === "prompt" && + !config.tuiPromptBar.enabled && + !(config.tuiCompactStatus.enabled && config.tuiCompactStatus.sessionPrompt) + ) + return []; + const announcementEnabled = + surface === "home" && + config.maintainerAnnouncements.enabled && + config.maintainerAnnouncements.home; + const homeCompactEnabled = config.tuiCompactStatus.enabled && config.tuiCompactStatus.homeBottom; + if (surface === "home" && !announcementEnabled && !homeCompactEnabled) return []; + const announcement = announcementEnabled ? await getHomeAnnouncementText(runtime) : ""; + if (surface === "home" && !homeCompactEnabled) return announcement ? [announcement] : []; + const rootFormatStyle = resolveQuotaFormatStyle(config.formatStyle); + const compactFormatStyle = config.tuiCompactStatus.formatStyle + ? resolveQuotaFormatStyle(config.tuiCompactStatus.formatStyle) + : rootFormatStyle; + const result = await collectQuotaRenderData({ + client: runtime.client, + resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, + config, + configMeta: runtime.configMeta, + request: createQuotaRuntimeRequestContext(runtime), + surfaceExplicitProviderIssues: true, + formatStyle: rootFormatStyle, + providers: runtime.providers, + includeAllWindowsData: true, + }); + const data = result.data; + if (!data) return announcement ? [announcement] : []; + if (surface === "prompt" && config.tuiPromptBar.enabled) { + const entry = pickPromptBarEntry(data); + if (!entry) return []; + const promptBar = sanitizeDisplayText( + [ + resolvePromptBarLabel(entry), + entry.percentRemaining === undefined + ? "" + : formatPromptBarPercentMeta({ + percentRemaining: entry.percentRemaining, + percentDisplayMode: config.percentDisplayMode, + resetTimeIso: entry.resetTimeIso, + resetTimeDecimals: config.resetTimeDecimals, + resetTimeSpaced: config.resetTimeSpaced, + runway: entry.runway, + }), + ] + .filter(Boolean) + .join(" | "), + ); + return promptBar ? [promptBar] : []; + } + const compactData = + compactFormatStyle === "allWindows" && result.allWindowsData + ? result.allWindowsData + : compactFormatStyle === "singleWindow" && result.singleWindowData !== undefined + ? result.singleWindowData + : data; + const compact = compactData + ? buildCompactQuotaStatusLine({ + data: compactData, + maxWidth: config.tuiCompactStatus.maxWidth, + percentDisplayMode: config.percentDisplayMode, + accountingDetail: config.accountingDetail, + resetTimeSpaced: config.resetTimeSpaced, + }) + : ""; + return [announcement, compact].filter(Boolean); +} + +/** + * Writes the quota export file if `config.export.enabled` is true and returns whether it + * wrote the file. Errors propagate. + */ +export async function writeQuotaExportIfEnabled(host: QuotaSurfaceHost): Promise { + const runtime = await resolveQuotaRuntimeContext({ + client: host.client, + roots: host.roots, + }); + if (!runtime.config.enabled || !runtime.config.export.enabled) return false; + + const exportData = await buildQuotaExport({ + providers: runtime.providers, + ctx: createExportProviderContext(runtime), + ttlMs: runtime.config.minIntervalMs, + fromCache: true, + }); + await writeQuotaExport(exportData, resolveExportPath(runtime.config.export.path)); + return true; +} diff --git a/src/plugin.ts b/src/plugin.ts index b7c89f28..4a1bb811 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,13 +1,16 @@ -/** OpenCode V2 server plugin: quota slash commands for every client, plus a diagnostics tool. */ +/** + * OpenCode V2 server plugin: quota slash commands for every client, a diagnostics tool, and + * the quota RPC that computes the TUI surfaces. + */ import { Plugin } from "@opencode/plugin"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { reconcileDetectedProvidersInGlobalConfig } from "./lib/opencode-config-providers.js"; import { - buildQuotaDialogCommandOutput, QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, -} from "./lib/quota-dialog-commands.js"; +} from "./lib/quota-dialog-command-specs.js"; +import { buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js"; import { containsQuotaReport, formatQuotaReportMessage, @@ -15,6 +18,14 @@ import { type QuotaReportMetadata, removeQuotaReports, } from "./lib/quota-report-message.js"; +import type { QuotaSessionModelContext } from "./lib/quota-runtime-context.js"; +import { + getQuotaFooter, + getQuotaMessage, + type QuotaSurfaceHost, + writeQuotaExportIfEnabled, +} from "./lib/quota-surface-data.js"; +import { QuotaRpc, type QuotaRpcCommandOutput } from "./rpc.js"; type ModelMessage = { readonly role: string; @@ -63,10 +74,13 @@ export const QuotaToastPlugin = Plugin.define({ }, }; + // In auto mode, /quota_status reports the providers it detected. The slash commands and + // the tool add them to the global OpenCode config; the TUI palette (via RPC) never writes it. const buildOutput = ( command: QuotaDialogCommandId, - sessionID: string, - argumentsText?: string, + sessionID: string | undefined, + argumentsText: string | undefined, + options: { reconcileDetectedProviders: boolean }, ) => buildQuotaDialogCommandOutput({ command, @@ -78,19 +92,62 @@ export const QuotaToastPlugin = Plugin.define({ const session = await ctx.session.get({ sessionID: id }); return { modelID: session.model?.id, providerID: session.model?.providerID }; }, - onDetectedProviderIds: async (providerIds) => { - if (providerIds.length === 0) return; - try { - await reconcileDetectedProvidersInGlobalConfig({ - configRootDir: roots.configRoot, - detectedProviderIds: providerIds, - }); - } catch (error) { - console.warn("Failed to add detected providers to global OpenCode config", error); - } - }, + onDetectedProviderIds: options.reconcileDetectedProviders + ? async (providerIds) => { + if (providerIds.length === 0) return; + try { + await reconcileDetectedProvidersInGlobalConfig({ + configRootDir: roots.configRoot, + detectedProviderIds: providerIds, + }); + } catch (error) { + console.warn("Failed to add detected providers to global OpenCode config", error); + } + } + : undefined, }); + // The TUI reads the session model from its local store, which has nothing for an + // unknown session. The RPC handlers match that: a failed lookup means no model. + const resolveSessionMeta = async (id: string): Promise => { + try { + const session = await ctx.session.get({ sessionID: id }); + return { modelID: session.model?.id, providerID: session.model?.providerID }; + } catch { + return {}; + } + }; + const surfaceHost: QuotaSurfaceHost = { client, roots, resolveSessionMeta }; + + // Awaited so the plugin is active only once the RPC is registered: OpenCode waits for + // activation before it routes an RPC call, so the first TUI call cannot miss it. + await ctx.rpc.register(QuotaRpc, { + surface: async (input) => ({ + quota: (await getQuotaMessage(surfaceHost, input.sessionID, input.surface)) ?? null, + }), + footer: async (input) => ({ + lines: await getQuotaFooter(surfaceHost, input.sessionID, input.surface), + }), + writeExport: async () => ({ written: await writeQuotaExportIfEnabled(surfaceHost) }), + command: async (input): Promise => { + try { + return await buildOutput(input.command, input.sessionID, input.arguments, { + reconcileDetectedProviders: false, + }); + } catch (error) { + // Return the reason as output so the TUI shows it instead of OpenCode's rpc.internal. + const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.id === input.command)!; + return { + state: "output", + command: input.command, + title: spec.title, + output: sanitizeDisplayText(error instanceof Error ? error.message : String(error)), + dialogSize: spec.dialogSize, + }; + } + }, + }); + await ctx.tool.transform((editor) => { editor.add({ name: "quota_status", @@ -101,7 +158,9 @@ export const QuotaToastPlugin = Plugin.define({ additionalProperties: false, }, async execute(_input, context) { - const result = await buildOutput("quota_status", context.sessionID); + const result = await buildOutput("quota_status", context.sessionID, undefined, { + reconcileDetectedProviders: true, + }); return { content: result.state === "output" ? sanitizeDisplayText(result.output) : "" }; }, }); @@ -120,6 +179,7 @@ export const QuotaToastPlugin = Plugin.define({ spec.id, invocation.sessionID, invocation.prompt.text.trim() || undefined, + { reconcileDetectedProviders: true }, ); if (result.state === "noop") return; // A message admitted while the AI works is delivered into that turn at its next diff --git a/src/rpc.ts b/src/rpc.ts new file mode 100644 index 00000000..b75565d8 --- /dev/null +++ b/src/rpc.ts @@ -0,0 +1,117 @@ +/** + * The plugin RPC between OpenCode Quota's server plugin and its clients. The server + * registers it in its setup and computes quota there; clients call it by this definition. + * Imports from `@opencode/*` stay type-only, so loading this file loads no OpenCode package. + */ +import type { Rpc } from "@opencode/plugin"; +import { + isQuotaDialogCommand, + type QuotaDialogCommandId, + type QuotaDialogCommandOutputResult, +} from "./lib/quota-dialog-command-specs.js"; + +export const QUOTA_RPC_ID = "slkiser.opencode-quota"; + +export type QuotaRpcSurfaceInput = { + surface: "sidebar" | "idle" | "compacted" | "question"; + sessionID: string; +}; +export type QuotaRpcSurfaceOutput = { + quota: { + message: string; + duration: number; + activeProviderCount: number; + resetNotification?: string; + } | null; +}; +export type QuotaRpcFooterInput = { surface: "prompt" | "home"; sessionID?: string }; +export type QuotaRpcFooterOutput = { lines: string[] }; +export type QuotaRpcWriteExportInput = Record; +export type QuotaRpcWriteExportOutput = { written: boolean }; +export type QuotaRpcCommandInput = { + command: QuotaDialogCommandId; + arguments?: string; + sessionID?: string; +}; +export type QuotaRpcCommandOutput = QuotaDialogCommandOutputResult; + +type StandardValueSchema = Extract; +type QuotaRpcSchema = StandardValueSchema & { "~standard": { types?: { input: T; output: T } } }; + +/** + * A Standard Schema that runs `check` and passes the value through unchanged. OpenCode + * validates every input and output with it. `types` only carries T for TypeScript. + */ +function schema(check: (value: unknown) => string | null): QuotaRpcSchema { + return { + "~standard": { + version: 1, + vendor: "opencode-quota", + validate: (value) => { + const issue = check(value); + return issue ? { issues: [{ message: issue }] } : { value: value as T }; + }, + }, + }; +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function checkOutput(value: unknown): string | null { + return isObject(value) ? null : "output must be an object"; +} + +function checkOptionalString(input: Record, field: string): string | null { + return input[field] === undefined || typeof input[field] === "string" + ? null + : `${field} must be a string`; +} + +const SURFACES = ["sidebar", "idle", "compacted", "question"]; +const FOOTER_SURFACES = ["prompt", "home"]; + +export const QuotaRpc = { + id: QUOTA_RPC_ID, + events: {}, + methods: { + surface: { + input: schema((value) => { + if (!isObject(value)) return "input must be an object"; + if (typeof value.surface !== "string" || !SURFACES.includes(value.surface)) { + return `surface must be one of ${SURFACES.join(", ")}`; + } + if (typeof value.sessionID !== "string") return "sessionID must be a string"; + return null; + }), + output: schema(checkOutput), + }, + footer: { + input: schema((value) => { + if (!isObject(value)) return "input must be an object"; + if (typeof value.surface !== "string" || !FOOTER_SURFACES.includes(value.surface)) { + return `surface must be one of ${FOOTER_SURFACES.join(", ")}`; + } + return checkOptionalString(value, "sessionID"); + }), + output: schema(checkOutput), + }, + writeExport: { + input: schema((value) => + isObject(value) ? null : "input must be an object", + ), + output: schema(checkOutput), + }, + command: { + input: schema((value) => { + if (!isObject(value)) return "input must be an object"; + if (typeof value.command !== "string" || !isQuotaDialogCommand(value.command)) { + return "command must be a quota command id"; + } + return checkOptionalString(value, "arguments") ?? checkOptionalString(value, "sessionID"); + }), + output: schema(checkOutput), + }, + }, +} satisfies Rpc.PortableDefinition; diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 4ad3dd37..4219ebb6 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -8,51 +8,19 @@ import { createSignal, onCleanup, Show } from "solid-js"; import { loadConfig } from "./lib/config.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; -import { formatQuotaRows } from "./lib/format.js"; import { - BUNDLED_MAINTAINER_ANNOUNCEMENTS, - formatMaintainerAnnouncementHomeCountLine, - getMaintainerAnnouncementsSummary, - getMaintainerAnnouncementTargetProviderIds, -} from "./lib/maintainer-announcements.js"; -import { getQuotaProviderShape, normalizeQuotaProviderId } from "./lib/provider-metadata.js"; -import { - buildQuotaDialogCommandOutput, QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, -} from "./lib/quota-dialog-commands.js"; -import { - buildQuotaExport, - createExportProviderContext, - resolveExportPath, - writeQuotaExport, -} from "./lib/quota-export.js"; -import { resolveQuotaFormatStyle } from "./lib/quota-format-style.js"; -import { - type CollectQuotaRenderDataResult, - collectConcreteEnabledProviderIds, - collectQuotaRenderData, -} from "./lib/quota-render-data.js"; +} from "./lib/quota-dialog-command-specs.js"; +import { buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js"; import { readQuotaReport, readQuotaReportMetadata } from "./lib/quota-report-message.js"; +import type { QuotaSessionModelContext } from "./lib/quota-runtime-context.js"; import { - formatQuotaResetNotification, - observeQuotaResetNotifications, -} from "./lib/quota-reset-notifications.js"; -import { - createQuotaProviderRuntimeContext, - createQuotaRuntimeRequestContext, - type QuotaRuntimeContext, - type QuotaSessionModelContext, - resolveQuotaRuntimeContext, -} from "./lib/quota-runtime-context.js"; -import { buildCompactQuotaStatusLine } from "./lib/tui-compact-format.js"; -import { - formatPromptBarPercentMeta, - pickPromptBarEntry, - resolvePromptBarLabel, -} from "./lib/tui-prompt-bar-format.js"; -import { buildSidebarQuotaPanelLines } from "./lib/tui-sidebar-format.js"; -import type { QuotaToastConfig } from "./lib/types.js"; + getQuotaFooter, + getQuotaMessage, + type QuotaSurfaceHost, + writeQuotaExportIfEnabled, +} from "./lib/quota-surface-data.js"; const terminalForeground = RGBA.defaultForeground(); const REFRESH_INTERVAL_MS = 60_000; @@ -154,305 +122,12 @@ async function getSessionModelMeta( return model ? { modelID: model.id, providerID: model.providerID } : {}; } -async function getQuotaMessage( - context: TuiContext, - sessionID: string, - surface: "sidebar" | "idle" | "compacted" | "question", -): Promise< - | { message: string; duration: number; activeProviderCount: number; resetNotification?: string } - | undefined -> { - const runtime = await resolveQuotaRuntimeContext({ - client: quotaClient(context), - roots: quotaRoots(context), - sessionID, - resolveSessionMeta: (id) => getSessionModelMeta(context, id), - includeSessionMeta: (config) => config.onlyCurrentModel, - }); - const config = runtime.config; - if (!config.enabled) return; - if (surface === "sidebar") { - if (!config.tuiSidebarPanel.enabled) return; - } else if (!config.enableToast) { - return; - } - if ( - (surface === "idle" && !config.showOnIdle) || - (surface === "compacted" && !config.showOnCompact) || - (surface === "question" && !config.showOnQuestion) - ) { - return; - } - - if ( - surface !== "sidebar" && - config.debug && - config.enabledProviders !== "auto" && - config.enabledProviders.length === 0 - ) { - return { - message: sanitizeDisplayText( - formatQuotaToastDebugInfo({ - trigger: surface, - reason: "enabledProviders empty", - config, - configMeta: runtime.configMeta, - }), - ), - duration: config.toastDurationMs, - activeProviderCount: 0, - }; - } - - const rootFormatStyle = resolveQuotaFormatStyle(config.formatStyle); - const formatStyle = - surface === "sidebar" && config.tuiSidebarPanel.formatStyle - ? resolveQuotaFormatStyle(config.tuiSidebarPanel.formatStyle) - : rootFormatStyle; - const result = await collectQuotaRenderData({ - client: runtime.client, - resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, - config, - configMeta: runtime.configMeta, - request: createQuotaRuntimeRequestContext(runtime), - surfaceExplicitProviderIssues: true, - formatStyle, - providers: runtime.providers, - }); - let resetNotification: string | undefined; - if ( - surface !== "sidebar" && - config.resetNotifications.enabled && - result.providerResults.length > 0 - ) { - try { - const notices = await observeQuotaResetNotifications({ - providers: result.providerResults, - windows: config.resetNotifications.windows, - }); - resetNotification = formatQuotaResetNotification(notices) ?? undefined; - } catch (error) { - const reason = error instanceof Error ? error.message : String(error); - console.warn(`[opencode-quota] failed to observe quota reset transitions: ${reason}`); - } - } - const data = result.data; - const message = - surface === "sidebar" - ? data - ? buildSidebarQuotaPanelLines({ - data, - config: { - ...config, - formatStyle, - }, - }).join("\n") - : undefined - : getToastMessage({ trigger: surface, runtime, result, style: rootFormatStyle }); - return message - ? { - message: sanitizeDisplayText(message), - duration: config.toastDurationMs, - activeProviderCount: result.active.length, - resetNotification, - } - : undefined; -} - -function getToastMessage(params: { - trigger: string; - runtime: QuotaRuntimeContext; - result: CollectQuotaRenderDataResult; - style: ReturnType; -}): string | undefined { - const config = params.runtime.config; - const { availability, active, hasExplicitProviderIssues, data } = params.result; - const debugInfo = (reason: string) => - formatQuotaToastDebugInfo({ - trigger: params.trigger, - reason, - config, - configMeta: params.runtime.configMeta, - currentModel: params.result.selection?.currentModel, - availability: availability.map((item) => ({ id: item.provider.id, ok: item.ok })), - }); - - if (data?.entries.length || data?.sessionTokens) { - const formatted = formatQuotaRows({ - version: "2.0.0", - layout: config.layout, - entries: data?.entries ?? [], - errors: data?.errors ?? [], - style: params.style, - percentDisplayMode: config.percentDisplayMode, - resetTimeDecimals: config.resetTimeDecimals, - sessionTokens: data?.sessionTokens, - }); - if (!config.debug) return formatted; - const debugFooter = `\n\n[debug] src=${params.runtime.configMeta.source} providers=${config.enabledProviders === "auto" ? "(auto)" : config.enabledProviders.join(",") || "(none)"} avail=${availability - .map((item) => `${item.provider.id}:${item.ok ? "ok" : "no"}`) - .join(" ")}`; - return formatted + debugFooter; - } - - if (config.showOnBothFail && data?.errors.length) { - const errorLines = data.errors.map((error) => `${error.label}: ${error.message}`).join("\n"); - if (!config.debug) return errorLines; - return `${errorLines}\n\n${debugInfo( - hasExplicitProviderIssues ? "providers missing/unavailable" : "all providers failed", - )}`; - } - - if (!config.debug) return undefined; - return debugInfo(active.length === 0 ? "no enabled providers available" : "no entries"); -} - -function formatQuotaToastDebugInfo(params: { - trigger: string; - reason: string; - config: QuotaToastConfig; - configMeta: Pick; - currentModel?: string; - availability?: Array<{ id: string; ok: boolean }>; -}): string { - const availability = params.availability - ? params.availability.map((item) => `${item.id}=${item.ok ? "ok" : "no"}`).join(" ") - : "unknown"; - - const providers = - params.config.enabledProviders === "auto" - ? "(auto)" - : params.config.enabledProviders.length > 0 - ? params.config.enabledProviders.join(",") - : "(none)"; - - const modelPart = params.currentModel ? ` model=${params.currentModel}` : ""; - const paths = params.configMeta.paths.length > 0 ? params.configMeta.paths.join(" | ") : "(none)"; - - return [ - "Quota Toast Debug (opencode-quota)", - `trigger=${params.trigger} reason=${params.reason}`, - `configSource=${params.configMeta.source} paths=${paths}`, - `enabled=${params.config.enabled} providers=${providers}${modelPart}`, - `available=${availability}`, - ].join("\n"); -} - -async function getHomeAnnouncementText(runtime: QuotaRuntimeContext): Promise { - const announcements = BUNDLED_MAINTAINER_ANNOUNCEMENTS; - const targetProviderIds = new Set(getMaintainerAnnouncementTargetProviderIds({ announcements })); - const announcementProviders = runtime.providers.filter((provider) => { - const shape = getQuotaProviderShape(normalizeQuotaProviderId(provider.id)); - return shape ? targetProviderIds.has(shape.id) : false; - }); - const providerIds = await collectConcreteEnabledProviderIds({ - providers: announcementProviders, - ctx: createQuotaProviderRuntimeContext(runtime), - enabledProviders: runtime.config.enabledProviders, - }); - const summary = getMaintainerAnnouncementsSummary({ - enabledProviders: providerIds, - announcements, - }); - return formatMaintainerAnnouncementHomeCountLine(summary.activeCount); -} - -async function getQuotaFooter( - context: TuiContext, - sessionID: string | undefined, - surface: "prompt" | "home", -): Promise { - // OpenCode 2 also mounts the prompt footer under the Home prompt, without a - // session. As in v4, the prompt line belongs to session prompts only. - if (surface === "prompt" && !sessionID) return []; - const resolvedRuntime = await resolveQuotaRuntimeContext({ +function quotaSurfaceHost(context: TuiContext): QuotaSurfaceHost { + return { client: quotaClient(context), roots: quotaRoots(context), - sessionID, resolveSessionMeta: (id) => getSessionModelMeta(context, id), - includeSessionMeta: (config) => config.onlyCurrentModel && surface === "prompt", - }); - // Home has no session: as in v4, it shows every enabled provider and no session - // tokens, and shares its cache keys with the export (createExportProviderContext). - const runtime: QuotaRuntimeContext = - surface === "home" - ? { - ...resolvedRuntime, - config: { ...resolvedRuntime.config, onlyCurrentModel: false, showSessionTokens: false }, - session: {}, - } - : resolvedRuntime; - const config = runtime.config; - if (!config.enabled) return []; - if ( - surface === "prompt" && - !config.tuiPromptBar.enabled && - !(config.tuiCompactStatus.enabled && config.tuiCompactStatus.sessionPrompt) - ) - return []; - const announcementEnabled = - surface === "home" && - config.maintainerAnnouncements.enabled && - config.maintainerAnnouncements.home; - const homeCompactEnabled = config.tuiCompactStatus.enabled && config.tuiCompactStatus.homeBottom; - if (surface === "home" && !announcementEnabled && !homeCompactEnabled) return []; - const announcement = announcementEnabled ? await getHomeAnnouncementText(runtime) : ""; - if (surface === "home" && !homeCompactEnabled) return announcement ? [announcement] : []; - const rootFormatStyle = resolveQuotaFormatStyle(config.formatStyle); - const compactFormatStyle = config.tuiCompactStatus.formatStyle - ? resolveQuotaFormatStyle(config.tuiCompactStatus.formatStyle) - : rootFormatStyle; - const result = await collectQuotaRenderData({ - client: runtime.client, - resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, - config, - configMeta: runtime.configMeta, - request: createQuotaRuntimeRequestContext(runtime), - surfaceExplicitProviderIssues: true, - formatStyle: rootFormatStyle, - providers: runtime.providers, - includeAllWindowsData: true, - }); - const data = result.data; - if (!data) return announcement ? [announcement] : []; - if (surface === "prompt" && config.tuiPromptBar.enabled) { - const entry = pickPromptBarEntry(data); - if (!entry) return []; - const promptBar = sanitizeDisplayText( - [ - resolvePromptBarLabel(entry), - entry.percentRemaining === undefined - ? "" - : formatPromptBarPercentMeta({ - percentRemaining: entry.percentRemaining, - percentDisplayMode: config.percentDisplayMode, - resetTimeIso: entry.resetTimeIso, - resetTimeDecimals: config.resetTimeDecimals, - resetTimeSpaced: config.resetTimeSpaced, - runway: entry.runway, - }), - ] - .filter(Boolean) - .join(" | "), - ); - return promptBar ? [promptBar] : []; - } - const compactData = - compactFormatStyle === "allWindows" && result.allWindowsData - ? result.allWindowsData - : compactFormatStyle === "singleWindow" && result.singleWindowData !== undefined - ? result.singleWindowData - : data; - const compact = compactData - ? buildCompactQuotaStatusLine({ - data: compactData, - maxWidth: config.tuiCompactStatus.maxWidth, - percentDisplayMode: config.percentDisplayMode, - accountingDetail: config.accountingDetail, - resetTimeSpaced: config.resetTimeSpaced, - }) - : ""; - return [announcement, compact].filter(Boolean); + }; } /** @@ -506,13 +181,13 @@ function QuotaFooter(props: { }): JSX.Element { const [lines, setLines] = createSignal([]); const view = createViewRefresh( - () => getQuotaFooter(props.context, props.sessionID, props.surface), + () => getQuotaFooter(quotaSurfaceHost(props.context), props.sessionID, props.surface), (next) => { setLines(next); if (props.surface !== "home") return; // Fire-and-forget: write the export file if enabled. A failed write // must never affect rendering, so log a warning and continue. - void writeQuotaExportIfEnabled(props.context).catch((error) => { + void writeQuotaExportIfEnabled(quotaSurfaceHost(props.context)).catch((error) => { console.warn(`[opencode-quota] quota export write failed: ${String(error)}`); }); }, @@ -538,23 +213,6 @@ function QuotaFooter(props: { ); } -/** Writes the quota export file if `config.export.enabled` is true. Errors propagate. */ -async function writeQuotaExportIfEnabled(context: TuiContext): Promise { - const runtime = await resolveQuotaRuntimeContext({ - client: quotaClient(context), - roots: quotaRoots(context), - }); - if (!runtime.config.enabled || !runtime.config.export.enabled) return; - - const exportData = await buildQuotaExport({ - providers: runtime.providers, - ctx: createExportProviderContext(runtime), - ttlMs: runtime.config.minIntervalMs, - fromCache: true, - }); - await writeQuotaExport(exportData, resolveExportPath(runtime.config.export.path)); -} - function reportFailure(error: unknown): void { const message = error instanceof Error ? error.message : String(error); console.warn(`[opencode-quota] failed to load quota: ${message}`); @@ -761,7 +419,7 @@ function SidebarQuotaView(props: { context: TuiContext; sessionID: string }): JS const lines = () => quota()?.message.split("\n") ?? []; const expandable = () => lines().length > 2; const view = createViewRefresh( - () => getQuotaMessage(props.context, props.sessionID, "sidebar"), + () => getQuotaMessage(quotaSurfaceHost(props.context), props.sessionID, "sidebar"), setQuota, ); view.refresh(); @@ -821,7 +479,7 @@ const plugin = Plugin.define({ if (!sessionID) return; // As in v4, subagent (child) sessions never show quota toasts. if (api.data.session.get(sessionID)?.parentID) return; - void getQuotaMessage(api, sessionID, reason) + void getQuotaMessage(quotaSurfaceHost(api), sessionID, reason) .then((quota) => { if (!quota) return; api.ui.toast.show({ diff --git a/tests/lib.quota-surface-data.test.ts b/tests/lib.quota-surface-data.test.ts new file mode 100644 index 00000000..2fa8f5ea --- /dev/null +++ b/tests/lib.quota-surface-data.test.ts @@ -0,0 +1,223 @@ +import { rm } from "node:fs/promises"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { QuotaSurfaceHost } from "../src/lib/quota-surface-data.js"; +import { + createAlibabaAuthModuleMock, + createConfigModuleMock, + createPluginRuntimePathsMockModule, + createPricingModuleMock, + createProvidersRegistryModuleMock, + makeQuotaToastTestConfig, + seedDefaultPluginBootstrapMocks, +} from "./helpers/plugin-test-harness.js"; + +const TEST_RUNTIME_ROOT = "/tmp/opencode-quota-lib-quota-surface-data-tests"; +const TEST_ACCOUNTING = { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", +} as const; +const ANNOUNCEMENT_HOME_MESSAGE = + "Notice: Maintainer announcement available. Run /quota_announcements."; + +const TEST_ANNOUNCEMENT = vi.hoisted(() => ({ + id: "copilot-credits", + message: "If you use Copilot, GitHub billing is moving to AI Credits.", + url: "https://github.blog/example", + providerIds: ["copilot"], +})); + +const mocks = vi.hoisted(() => ({ + loadConfig: vi.fn(), + getProviders: vi.fn(), + maybeRefreshPricingSnapshot: vi.fn(), + getPricingSnapshotMeta: vi.fn(), + getPricingSnapshotSource: vi.fn(), + getRuntimePricingRefreshStatePath: vi.fn(), + getRuntimePricingSnapshotPath: vi.fn(), + setPricingSnapshotAutoRefresh: vi.fn(), + setPricingSnapshotSelection: vi.fn(), + resolveAlibabaCodingPlanAuthCached: vi.fn(), + getMaintainerAnnouncementsSummary: vi.fn(), + formatMaintainerAnnouncementHomeCountLine: vi.fn(), +})); + +vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); +vi.mock("../src/providers/registry.js", () => + createProvidersRegistryModuleMock(mocks.getProviders), +); +vi.mock("../src/lib/modelsdev-pricing.js", () => createPricingModuleMock(mocks)); +vi.mock("../src/lib/alibaba-auth.js", () => + createAlibabaAuthModuleMock(mocks.resolveAlibabaCodingPlanAuthCached), +); +vi.mock("../src/lib/opencode-runtime-paths.js", () => + createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT), +); +vi.mock("../src/lib/maintainer-announcements.js", () => ({ + BUNDLED_MAINTAINER_ANNOUNCEMENTS: [TEST_ANNOUNCEMENT], + formatMaintainerAnnouncementHomeCountLine: mocks.formatMaintainerAnnouncementHomeCountLine, + getMaintainerAnnouncementsSummary: mocks.getMaintainerAnnouncementsSummary, + getMaintainerAnnouncementTargetProviderIds: () => ["copilot"], +})); + +function createHost(): QuotaSurfaceHost { + return { + client: { + config: { + get: async () => ({ data: {} }), + providers: async () => ({ data: { providers: [{ id: "copilot" }] } }), + }, + }, + roots: { + workspaceRoot: process.cwd(), + configRoot: process.cwd(), + fallbackDirectory: process.cwd(), + }, + resolveSessionMeta: async () => ({}), + }; +} + +function makeCopilotProvider() { + return { + id: "copilot", + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 81 }], + errors: [], + }), + }; +} + +describe("quota surface data", () => { + beforeEach(async () => { + seedDefaultPluginBootstrapMocks(mocks, { resetPluginState: true }); + mocks.getMaintainerAnnouncementsSummary.mockReturnValue({ + source: "bundled_only", + network: false, + bundledCount: 1, + activeCount: 1, + futureCount: 0, + expiredCount: 0, + activeAnnouncements: [{ announcement: TEST_ANNOUNCEMENT, active: true, reasons: [] }], + evaluations: [], + }); + mocks.formatMaintainerAnnouncementHomeCountLine.mockImplementation((activeCount: number) => + activeCount === 1 ? ANNOUNCEMENT_HOME_MESSAGE : "", + ); + await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); + }); + + afterEach(async () => { + vi.restoreAllMocks(); + await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); + }); + + it("projects all sidebar windows before formatting when it overrides singleWindow", async () => { + const runtimeModule = await import("../src/lib/quota-runtime-context.js"); + const renderDataModule = await import("../src/lib/quota-render-data.js"); + const sidebarModule = await import("../src/lib/tui-sidebar-format.js"); + vi.spyOn(runtimeModule, "resolveQuotaRuntimeContext").mockResolvedValue({ + client: {}, + config: { + enabled: true, + enableToast: true, + formatStyle: "singleWindow", + percentDisplayMode: "remaining", + resetTimeDecimals: undefined, + toastDurationMs: 5000, + tuiSidebarPanel: { enabled: true, formatStyle: "allWindows" }, + }, + configMeta: {}, + providers: [], + resolveRuntimeProviderIds: vi.fn(), + session: { sessionID: "session-1" }, + } as never); + const collect = vi.spyOn(renderDataModule, "collectQuotaRenderData").mockImplementation( + async ({ formatStyle }) => + ({ + active: [{ id: "copilot" }], + data: { + entries: + formatStyle === "allWindows" + ? [ + { name: "Copilot 5h", percentRemaining: 50 }, + { name: "Copilot Weekly", percentRemaining: 80 }, + ] + : [{ name: "Copilot 5h", percentRemaining: 50 }], + errors: [], + }, + }) as never, + ); + const buildSidebarQuotaPanelLines = vi + .spyOn(sidebarModule, "buildSidebarQuotaPanelLines") + .mockImplementation(({ data }) => data.entries.map((entry) => entry.name)); + const { getQuotaMessage } = await import("../src/lib/quota-surface-data.js"); + + const quota = await getQuotaMessage(createHost(), "session-1", "sidebar"); + + expect(quota).toEqual({ + message: "Copilot 5h\nCopilot Weekly", + duration: 5000, + activeProviderCount: 1, + resetNotification: undefined, + }); + expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + entries: [ + expect.objectContaining({ name: "Copilot 5h" }), + expect.objectContaining({ name: "Copilot Weekly" }), + ], + }), + config: expect.objectContaining({ formatStyle: "allWindows" }), + }), + ); + expect(collect).toHaveBeenCalledWith(expect.objectContaining({ formatStyle: "allWindows" })); + }); + + it("computes the count-only Home announcement from the enabled providers", async () => { + mocks.loadConfig.mockResolvedValue( + makeQuotaToastTestConfig({ + enabled: true, + enabledProviders: ["copilot"], + minIntervalMs: 0, + maintainerAnnouncements: { enabled: true, home: true }, + }), + ); + mocks.getProviders.mockReturnValue([makeCopilotProvider()]); + const { getQuotaFooter } = await import("../src/lib/quota-surface-data.js"); + + await expect(getQuotaFooter(createHost(), undefined, "home")).resolves.toEqual([ + ANNOUNCEMENT_HOME_MESSAGE, + ]); + expect(mocks.getMaintainerAnnouncementsSummary).toHaveBeenCalledWith( + expect.objectContaining({ enabledProviders: ["copilot"] }), + ); + }); + + it("skips the Home announcement when maintainerAnnouncements.home is off", async () => { + const copilot = makeCopilotProvider(); + mocks.loadConfig.mockResolvedValue( + makeQuotaToastTestConfig({ + enabled: true, + enabledProviders: ["copilot"], + minIntervalMs: 0, + maintainerAnnouncements: { enabled: true, home: false }, + tuiCompactStatus: { enabled: true, homeBottom: true, sessionPrompt: false, maxWidth: 96 }, + }), + ); + mocks.getProviders.mockReturnValue([copilot]); + const { getQuotaFooter } = await import("../src/lib/quota-surface-data.js"); + + const lines = await getQuotaFooter(createHost(), undefined, "home"); + + expect(copilot.fetch).toHaveBeenCalled(); + expect(lines).toHaveLength(1); + expect(lines[0]).toContain("Copilot"); + expect(mocks.getMaintainerAnnouncementsSummary).not.toHaveBeenCalled(); + expect(mocks.formatMaintainerAnnouncementHomeCountLine).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/plugin-rpc.test.ts b/tests/plugin-rpc.test.ts new file mode 100644 index 00000000..06c3cf5b --- /dev/null +++ b/tests/plugin-rpc.test.ts @@ -0,0 +1,287 @@ +import { existsSync } from "node:fs"; +import { rm } from "node:fs/promises"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { DEFAULT_CONFIG } from "../src/lib/types.js"; +import { + createAlibabaAuthModuleMock, + createConfigModuleMock, + createPluginRuntimePathsMockModule, + createPricingModuleMock, + createProvidersRegistryModuleMock, + createSessionTokensModuleMock, + makeQuotaToastTestConfig, + seedDefaultPluginBootstrapMocks, +} from "./helpers/plugin-test-harness.js"; + +const TEST_RUNTIME_ROOT = "/tmp/opencode-quota-plugin-rpc-tests"; +const TEST_ACCOUNTING = { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", +} as const; + +const mocks = vi.hoisted(() => ({ + loadConfig: vi.fn(), + getProviders: vi.fn(), + maybeRefreshPricingSnapshot: vi.fn(), + getPricingSnapshotMeta: vi.fn(), + getPricingSnapshotSource: vi.fn(), + getRuntimePricingRefreshStatePath: vi.fn(), + getRuntimePricingSnapshotPath: vi.fn(), + setPricingSnapshotAutoRefresh: vi.fn(), + setPricingSnapshotSelection: vi.fn(), + resolveAlibabaCodingPlanAuthCached: vi.fn(), + fetchSessionTokensForDisplay: vi.fn(), + reconcileDetectedProvidersInGlobalConfig: vi.fn(), +})); + +vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); +vi.mock("../src/providers/registry.js", () => + createProvidersRegistryModuleMock(mocks.getProviders), +); +vi.mock("../src/lib/modelsdev-pricing.js", () => createPricingModuleMock(mocks)); +vi.mock("../src/lib/session-tokens.js", () => + createSessionTokensModuleMock(mocks.fetchSessionTokensForDisplay), +); +vi.mock("../src/lib/alibaba-auth.js", () => + createAlibabaAuthModuleMock(mocks.resolveAlibabaCodingPlanAuthCached), +); +vi.mock("../src/lib/opencode-runtime-paths.js", () => + createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT), +); +vi.mock("../src/lib/opencode-config-providers.js", () => ({ + reconcileDetectedProvidersInGlobalConfig: mocks.reconcileDetectedProvidersInGlobalConfig, +})); + +type Handler = (input: unknown, context: unknown) => Promise; +type RegisteredCommand = { + name: string; + execute: (invocation: { sessionID: string; prompt: { text: string } }) => Promise; +}; + +const callContext = { + signal: new AbortController().signal, + error: (type: string, message: string, data?: unknown) => ({ type, message, data }), +}; + +function useConfig(overrides: Partial): void { + mocks.loadConfig.mockResolvedValue( + makeQuotaToastTestConfig({ + enabled: true, + enabledProviders: ["copilot"], + minIntervalMs: 0, + showSessionTokens: false, + maintainerAnnouncements: { enabled: false, home: false }, + ...overrides, + }), + ); +} + +async function setupServer(sessionGet = vi.fn().mockResolvedValue({})) { + const { default: server } = await import("../src/plugin.js"); + const register = vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })); + const commands: RegisteredCommand[] = []; + const ctx = { + location: { directory: process.cwd() }, + provider: { list: vi.fn().mockResolvedValue({ data: [{ id: "copilot" }] }) }, + session: { get: sessionGet, wait: vi.fn(), prompt: vi.fn(), hook: vi.fn() }, + tool: { transform: vi.fn() }, + command: { + transform: vi.fn(async (callback) => { + callback({ add: (command: RegisteredCommand) => commands.push(command) }); + }), + }, + rpc: { register }, + }; + await server.setup(ctx as never); + expect(register).toHaveBeenCalledOnce(); + const [definition, handlers] = register.mock.calls[0] as unknown as [ + { id: string; methods: Record }, + Record, + ]; + const call = async (method: string, input: unknown) => { + const output = await handlers[method](input, callContext); + // Outputs travel over HTTP to every client; they must never carry login secrets. + expect(JSON.stringify(output)).not.toMatch(/"(access|refresh|apiKey)"/); + return output; + }; + return { ctx, definition, handlers, commands, call }; +} + +describe("server quota RPC", () => { + beforeEach(async () => { + seedDefaultPluginBootstrapMocks(mocks, { resetPluginState: true }); + mocks.getProviders.mockReturnValue([ + { + id: "copilot", + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 81 }], + errors: [], + }), + }, + ]); + mocks.reconcileDetectedProvidersInGlobalConfig.mockResolvedValue({ changed: false }); + await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); + const { __resetQuotaStateForTests } = await import("../src/lib/quota-state.js"); + __resetQuotaStateForTests(); + }); + + afterEach(async () => { + vi.restoreAllMocks(); + await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); + }); + + it("registers the quota RPC with its four methods during setup", async () => { + const { QuotaRpc } = await import("../src/rpc.js"); + const { definition, handlers } = await setupServer(); + + expect(definition).toBe(QuotaRpc); + expect(definition.id).toBe("slkiser.opencode-quota"); + expect(Object.keys(definition.methods)).toEqual([ + "surface", + "footer", + "writeExport", + "command", + ]); + expect(Object.keys(handlers)).toEqual(Object.keys(definition.methods)); + }); + + it("serves the sidebar panel and the idle toast", async () => { + useConfig({ enableToast: true, showOnIdle: true, toastDurationMs: 7000 }); + const { call } = await setupServer(); + + const sidebar = (await call("surface", { surface: "sidebar", sessionID: "session-1" })) as { + quota: { message: string }; + }; + expect(sidebar).toEqual({ + quota: expect.objectContaining({ duration: 7000, activeProviderCount: 1 }), + }); + expect(sidebar.quota.message).toContain("Copilot"); + + const idle = (await call("surface", { surface: "idle", sessionID: "session-1" })) as { + quota: { message: string }; + }; + expect(idle).toEqual({ + quota: expect.objectContaining({ duration: 7000, activeProviderCount: 1 }), + }); + expect(idle.quota.message).toContain("Copilot"); + }); + + it("returns a null quota when the surface is turned off", async () => { + useConfig({ enableToast: true, showOnIdle: false }); + const { call } = await setupServer(); + + await expect(call("surface", { surface: "idle", sessionID: "session-1" })).resolves.toEqual({ + quota: null, + }); + }); + + it("serves the session prompt line and the Home compact line", async () => { + useConfig({ + tuiPromptBar: { enabled: true }, + tuiCompactStatus: { enabled: true, homeBottom: true, sessionPrompt: false, maxWidth: 96 }, + }); + const { call } = await setupServer(); + + const prompt = (await call("footer", { surface: "prompt", sessionID: "session-1" })) as { + lines: string[]; + }; + expect(prompt.lines).toHaveLength(1); + expect(prompt.lines[0]).toContain("Copilot"); + await expect(call("footer", { surface: "prompt" })).resolves.toEqual({ lines: [] }); + + const home = (await call("footer", { surface: "home" })) as { lines: string[] }; + expect(home.lines).toHaveLength(1); + expect(home.lines[0]).toContain("Copilot"); + }); + + it("writes the export file only when the export is enabled", async () => { + const exportPath = `${TEST_RUNTIME_ROOT}/quota-export.json`; + useConfig({ export: { enabled: false, path: exportPath } }); + const disabled = await setupServer(); + await expect(disabled.call("writeExport", {})).resolves.toEqual({ written: false }); + expect(existsSync(exportPath)).toBe(false); + + useConfig({ export: { enabled: true, path: exportPath } }); + const enabled = await setupServer(); + await expect(enabled.call("writeExport", {})).resolves.toEqual({ written: true }); + expect(existsSync(exportPath)).toBe(true); + }); + + it("runs palette commands on the server and returns their output", async () => { + useConfig({}); + const { call } = await setupServer(); + + const output = (await call("command", { command: "quota", sessionID: "session-1" })) as { + output: string; + }; + expect(output).toEqual( + expect.objectContaining({ + state: "output", + command: "quota", + title: "OpenCode Quota", + dialogSize: "xlarge", + }), + ); + expect(output.output).toContain("Copilot"); + }); + + it("never writes detected providers to the global config from the palette", async () => { + useConfig({ enabledProviders: "auto" }); + const dialogModule = await import("../src/lib/quota-dialog-commands.js"); + vi.spyOn(dialogModule, "buildQuotaDialogCommandOutput").mockImplementation(async (params) => { + await params.onDetectedProviderIds?.(["copilot"]); + return { + state: "output", + command: params.command, + title: "OpenCode Quota Status", + output: "status", + dialogSize: "xlarge", + }; + }); + const { call, commands } = await setupServer(); + + await call("command", { command: "quota_status", sessionID: "session-1" }); + expect(mocks.reconcileDetectedProvidersInGlobalConfig).not.toHaveBeenCalled(); + + const slash = commands.find((command) => command.name === "quota_status"); + await slash?.execute({ sessionID: "session-1", prompt: { text: "" } }); + expect(mocks.reconcileDetectedProvidersInGlobalConfig).toHaveBeenCalledWith({ + configRootDir: process.cwd(), + detectedProviderIds: ["copilot"], + }); + }); + + it("returns a failed palette command as output instead of an RPC error", async () => { + useConfig({}); + const dialogModule = await import("../src/lib/quota-dialog-commands.js"); + vi.spyOn(dialogModule, "buildQuotaDialogCommandOutput").mockRejectedValue( + new Error("quota \u001b[31mbroke"), + ); + const { call } = await setupServer(); + + await expect(call("command", { command: "quota" })).resolves.toEqual({ + state: "output", + command: "quota", + title: "OpenCode Quota", + output: "quota broke", + dialogSize: "xlarge", + }); + }); + + it("treats a session the server cannot find as a session without a model", async () => { + useConfig({ onlyCurrentModel: true }); + const input = { surface: "sidebar", sessionID: "missing" }; + const withoutModel = await setupServer(vi.fn().mockResolvedValue({})); + const expected = await withoutModel.call("surface", input); + const sessionGet = vi.fn().mockRejectedValue(new Error("Session not found")); + const missing = await setupServer(sessionGet); + + await expect(missing.call("surface", input)).resolves.toEqual(expected); + expect(sessionGet).toHaveBeenCalledWith({ sessionID: "missing" }); + }); +}); diff --git a/tests/plugin-v2.test.ts b/tests/plugin-v2.test.ts index 9e444be6..47e434a2 100644 --- a/tests/plugin-v2.test.ts +++ b/tests/plugin-v2.test.ts @@ -6,7 +6,7 @@ vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ buildQuotaDialogCommandOutput: buildOutput, })); -import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-commands.js"; +import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-command-specs.js"; import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; import plugin from "../src/plugin.js"; @@ -59,6 +59,7 @@ function createContext() { callback({ add: (command: RegisteredCommand) => commands.push(command) }); }), }, + rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, }; return { ctx, tools, commands, hooks, calls }; } diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 6b18eab6..57e41a7a 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { resolveOpenCodeLocationRoots } from "../src/lib/config-file-utils.js"; -import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-commands.js"; +import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-command-specs.js"; import tuiPlugin from "../src/tui-v2.js"; const mocks = vi.hoisted(() => ({ build: vi.fn() })); @@ -151,6 +151,9 @@ describe("V2 CLI command boundary", () => { command: { transform: commandTransform }, session: { hook: vi.fn() }, provider: { list: vi.fn() }, + rpc: { + register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })), + }, } as never); expect(transform).toHaveBeenCalledOnce(); expect(commandTransform).toHaveBeenCalledOnce(); diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 3fee7fc5..1bd98173 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -221,7 +221,7 @@ describe("maintainer announcement plugin integration", () => { }; mocks.getProviders.mockReturnValue([provider]); - const { QUOTA_DIALOG_COMMANDS } = await import("../src/lib/quota-dialog-commands.js"); + const { QUOTA_DIALOG_COMMANDS } = await import("../src/lib/quota-dialog-command-specs.js"); const announcementCommand = QUOTA_DIALOG_COMMANDS.find( (command) => command.id === "quota_announcements", ); diff --git a/tests/plugin.quota-command.test.ts b/tests/plugin.quota-command.test.ts index 57b64fd2..748add39 100644 --- a/tests/plugin.quota-command.test.ts +++ b/tests/plugin.quota-command.test.ts @@ -70,6 +70,7 @@ async function createV2StatusTool(directory: string) { provider: { list: vi.fn().mockResolvedValue({ data: [] }) }, session: { get: vi.fn().mockResolvedValue({}), hook: vi.fn() }, command: { transform: vi.fn() }, + rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, tool: { transform: async ( register: (editor: { add: (tool: { execute: typeof execute }) => void }) => void, @@ -202,6 +203,9 @@ describe("/quota command behavior", () => { hook: vi.fn(), }, command: { transform: vi.fn() }, + rpc: { + register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })), + }, }; await server.setup(ctx as never); expect(tool?.name).toBe("quota_status"); diff --git a/tests/plugin.tokens-session-all.test.ts b/tests/plugin.tokens-session-all.test.ts index 7a1ddbfb..3b5adf71 100644 --- a/tests/plugin.tokens-session-all.test.ts +++ b/tests/plugin.tokens-session-all.test.ts @@ -113,7 +113,7 @@ describe("/tokens_session_all command", () => { it("registers /tokens_session_all in the V2 TUI command palette", async () => { const { default: plugin } = await import("../src/tui-v2.js"); - const { QUOTA_DIALOG_COMMANDS } = await import("../src/lib/quota-dialog-commands.js"); + const { QUOTA_DIALOG_COMMANDS } = await import("../src/lib/quota-dialog-command-specs.js"); const tokensSessionAllCommand = QUOTA_DIALOG_COMMANDS.find( (command) => command.id === "tokens_session_all", ); diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index b5f41097..e21a16da 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -122,6 +122,7 @@ async function runQuotaStatus(sessionID: string): Promise { hook: vi.fn(), }, command: { transform: vi.fn() }, + rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, tool: { transform: async (callback: (editor: { add: (tool: typeof quotaTool) => void }) => void) => callback({ diff --git a/tests/rpc-definition.test.ts b/tests/rpc-definition.test.ts new file mode 100644 index 00000000..88115224 --- /dev/null +++ b/tests/rpc-definition.test.ts @@ -0,0 +1,144 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-command-specs.js"; +import { QUOTA_RPC_ID, QuotaRpc } from "../src/rpc.js"; + +type ValidateResult = { value?: unknown; issues?: ReadonlyArray<{ message: string }> }; +type StandardSchema = { + "~standard": { + version: number; + vendor: string; + types?: unknown; + validate: (value: unknown) => unknown; + }; +}; + +async function validate(schema: StandardSchema, value: unknown): Promise { + return (await schema["~standard"].validate(value)) as ValidateResult; +} + +async function expectAccepted(schema: StandardSchema, value: unknown): Promise { + const result = await validate(schema, value); + expect(result.issues).toBeUndefined(); + expect(result.value).toBe(value); +} + +async function expectRejected(schema: StandardSchema, value: unknown, message: string) { + const result = await validate(schema, value); + expect(result.issues).toEqual([{ message }]); + expect(result).not.toHaveProperty("value"); +} + +const methods = QuotaRpc.methods; + +describe("quota RPC definition", () => { + it("defines the plain RPC id, no events and four methods", () => { + expect(QUOTA_RPC_ID).toBe("slkiser.opencode-quota"); + expect(QuotaRpc.id).toBe(QUOTA_RPC_ID); + expect(QuotaRpc.events).toEqual({}); + expect(Object.keys(methods)).toEqual(["surface", "footer", "writeExport", "command"]); + }); + + it("uses Standard Schema v1 validators that carry no runtime types", () => { + for (const method of Object.values(methods)) { + for (const schema of [method.input, method.output]) { + expect(schema["~standard"]).toEqual( + expect.objectContaining({ version: 1, vendor: "opencode-quota" }), + ); + expect(schema["~standard"]).not.toHaveProperty("types"); + } + } + }); + + it("validates surface input", async () => { + for (const surface of ["sidebar", "idle", "compacted", "question"]) { + await expectAccepted(methods.surface.input, { surface, sessionID: "session-1" }); + } + await expectRejected(methods.surface.input, null, "input must be an object"); + await expectRejected(methods.surface.input, [], "input must be an object"); + await expectRejected( + methods.surface.input, + { sessionID: "session-1" }, + "surface must be one of sidebar, idle, compacted, question", + ); + await expectRejected( + methods.surface.input, + { surface: "prompt", sessionID: "session-1" }, + "surface must be one of sidebar, idle, compacted, question", + ); + await expectRejected(methods.surface.input, { surface: "idle" }, "sessionID must be a string"); + await expectRejected( + methods.surface.input, + { surface: "idle", sessionID: 1 }, + "sessionID must be a string", + ); + }); + + it("validates footer input", async () => { + await expectAccepted(methods.footer.input, { surface: "prompt", sessionID: "session-1" }); + await expectAccepted(methods.footer.input, { surface: "prompt" }); + await expectAccepted(methods.footer.input, { surface: "home" }); + await expectRejected(methods.footer.input, "home", "input must be an object"); + await expectRejected(methods.footer.input, {}, "surface must be one of prompt, home"); + await expectRejected( + methods.footer.input, + { surface: "sidebar" }, + "surface must be one of prompt, home", + ); + await expectRejected( + methods.footer.input, + { surface: "home", sessionID: null }, + "sessionID must be a string", + ); + }); + + it("validates writeExport input", async () => { + await expectAccepted(methods.writeExport.input, {}); + await expectRejected(methods.writeExport.input, undefined, "input must be an object"); + await expectRejected(methods.writeExport.input, null, "input must be an object"); + }); + + it("validates command input", async () => { + for (const spec of QUOTA_DIALOG_COMMANDS) { + await expectAccepted(methods.command.input, { command: spec.id }); + } + await expectAccepted(methods.command.input, { + command: "tokens_between", + arguments: "2026-01-01 2026-01-15", + sessionID: "session-1", + }); + await expectRejected(methods.command.input, 1, "input must be an object"); + await expectRejected(methods.command.input, {}, "command must be a quota command id"); + await expectRejected( + methods.command.input, + { command: "quota_nope" }, + "command must be a quota command id", + ); + await expectRejected( + methods.command.input, + { command: "quota", arguments: 5 }, + "arguments must be a string", + ); + await expectRejected( + methods.command.input, + { command: "quota", sessionID: 5 }, + "sessionID must be a string", + ); + }); + + it("requires every output to be an object", async () => { + for (const method of Object.values(methods)) { + await expectAccepted(method.output, { any: "object" }); + for (const value of [undefined, null, "text", 1, []]) { + await expectRejected(method.output, value, "output must be an object"); + } + } + }); + + it("ships dist/rpc.js without loading any @opencode package", () => { + const source = readFileSync(new URL("../dist/rpc.js", import.meta.url), "utf8"); + expect(source).not.toMatch(/(?:from|import)\s*\(?\s*["']@opencode\//); + expect(source).toContain('from "./lib/quota-dialog-command-specs.js"'); + }); +}); diff --git a/tests/tui-dist-packaging.test.ts b/tests/tui-dist-packaging.test.ts index 932261a4..2364ba71 100644 --- a/tests/tui-dist-packaging.test.ts +++ b/tests/tui-dist-packaging.test.ts @@ -57,7 +57,7 @@ describe("tui dist packaging", () => { expect(source).toContain("sidebar.content"); expect(source).toContain("prompt.footer"); expect(source).toContain("home.footer.status"); - expect(source).toContain("buildCompactQuotaStatusLine"); + expect(source).toContain("./lib/quota-surface-data.js"); expect(source).toContain("buildQuotaDialogCommandOutput"); expect(source).toContain("registerQuotaCommands"); expect(source).not.toContain("jsx-dev-runtime"); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index e1bf3d65..446e62cf 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -164,6 +164,7 @@ async function setupV2Surfaces(client: ReturnType, provider hook: vi.fn(), }, command: { transform: vi.fn() }, + rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, tool: { transform: vi.fn(async (callback: (editor: { add(value: RegisteredTool): void }) => void) => { callback({ From c7b0b4bd43e700efe06158fddd20c2623ba2d057 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:07:51 +0200 Subject: [PATCH 65/97] fix(server): leave an unset reset notice out of the surface RPC output OpenCode 2.0.16 checks every RPC output against a JSON-value schema before it sends the HTTP response. The surface handler always returned a resetNotification key, which is undefined on the sidebar and on toasts without a reset notice, so every sidebar call failed with HTTP 400 "Expected JSON value at [\"output\"]". The key is now left out when unset. The RPC test helper now requires every handler output to survive a JSON round trip unchanged. --- src/lib/quota-surface-data.ts | 3 ++- tests/plugin-rpc.test.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/quota-surface-data.ts b/src/lib/quota-surface-data.ts index bcffdda1..4d6235a8 100644 --- a/src/lib/quota-surface-data.ts +++ b/src/lib/quota-surface-data.ts @@ -153,7 +153,8 @@ export async function getQuotaMessage( message: sanitizeDisplayText(message), duration: config.toastDurationMs, activeProviderCount: result.active.length, - resetNotification, + // The RPC route accepts only JSON values, so an unset notice leaves the key out. + ...(resetNotification === undefined ? {} : { resetNotification }), } : undefined; } diff --git a/tests/plugin-rpc.test.ts b/tests/plugin-rpc.test.ts index 06c3cf5b..f6af1c3c 100644 --- a/tests/plugin-rpc.test.ts +++ b/tests/plugin-rpc.test.ts @@ -105,6 +105,9 @@ async function setupServer(sessionGet = vi.fn().mockResolvedValue({})) { const output = await handlers[method](input, callContext); // Outputs travel over HTTP to every client; they must never carry login secrets. expect(JSON.stringify(output)).not.toMatch(/"(access|refresh|apiKey)"/); + // OpenCode's RPC route rejects any output that is not a JSON value (HTTP 400), so an + // undefined field or a non-finite number must never reach it. + expect(output).toStrictEqual(JSON.parse(JSON.stringify(output))); return output; }; return { ctx, definition, handlers, commands, call }; From f3498ca95411ede37608f8cafa9ecf28333a9f67 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:30:01 +0200 Subject: [PATCH 66/97] refactor(tui): render quota from the server RPC instead of computing it locally --- src/lib/quota-surface-data.ts | 2 +- src/tui-v2.tsx | 132 ++++++++--- tests/helpers/quota-rpc-bridge.ts | 34 +++ tests/lib.quota-surface-data.test.ts | 42 ++++ tests/plugin.command-handled-boundary.test.ts | 78 +++++-- tests/plugin.maintainer-announcements.test.ts | 37 ++- tests/plugin.question-hook.test.ts | 24 +- tests/plugin.tokens-session-all.test.ts | 4 + tests/tui-dist-import-graph.test.ts | 47 ++++ tests/tui-dist-packaging.test.ts | 5 +- tests/tui-v2-commands.test.ts | 101 +++++--- tests/tui-v2-format-style.test.ts | 94 ++------ tests/tui-v2-refresh.test.ts | 219 +++++++++--------- tests/v4-phase5-cross-surface.test.ts | 15 +- 14 files changed, 538 insertions(+), 296 deletions(-) create mode 100644 tests/helpers/quota-rpc-bridge.ts create mode 100644 tests/tui-dist-import-graph.test.ts diff --git a/src/lib/quota-surface-data.ts b/src/lib/quota-surface-data.ts index 4d6235a8..3ad16f1d 100644 --- a/src/lib/quota-surface-data.ts +++ b/src/lib/quota-surface-data.ts @@ -1,7 +1,7 @@ /** * Computes the text of the TUI quota surfaces: the sidebar panel, the toasts, the * session prompt line and the Home footer, plus the optional export file. The server - * plugin serves these over its RPC; the TUI still calls them in-process. + * plugin serves these to the TUI over its RPC. */ import type { RuntimeContextRootHints } from "./config-file-utils.js"; import { sanitizeDisplayText } from "./display-sanitize.js"; diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 4219ebb6..dbe3dcd3 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -12,18 +12,22 @@ import { QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, } from "./lib/quota-dialog-command-specs.js"; -import { buildQuotaDialogCommandOutput } from "./lib/quota-dialog-commands.js"; import { readQuotaReport, readQuotaReportMetadata } from "./lib/quota-report-message.js"; -import type { QuotaSessionModelContext } from "./lib/quota-runtime-context.js"; import { - getQuotaFooter, - getQuotaMessage, - type QuotaSurfaceHost, - writeQuotaExportIfEnabled, -} from "./lib/quota-surface-data.js"; + QuotaRpc, + type QuotaRpcCommandInput, + type QuotaRpcCommandOutput, + type QuotaRpcFooterInput, + type QuotaRpcFooterOutput, + type QuotaRpcSurfaceInput, + type QuotaRpcSurfaceOutput, + type QuotaRpcWriteExportInput, + type QuotaRpcWriteExportOutput, +} from "./rpc.js"; const terminalForeground = RGBA.defaultForeground(); const REFRESH_INTERVAL_MS = 60_000; +const RPC_TIMEOUT_MS = 60_000; type TuiEvent = { data?: Record }; type Toast = { @@ -44,9 +48,29 @@ type DialogTheme = { text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA } } }; background: { action: { primary: { focused: RGBA } } }; }; +type QuotaRpcCallOptions = { location: { directory: string }; signal: AbortSignal }; +type QuotaRpcClient = { + surface: ( + input: QuotaRpcSurfaceInput, + options: QuotaRpcCallOptions, + ) => Promise; + footer: ( + input: QuotaRpcFooterInput, + options: QuotaRpcCallOptions, + ) => Promise; + writeExport: ( + input: QuotaRpcWriteExportInput, + options: QuotaRpcCallOptions, + ) => Promise; + command: ( + input: QuotaRpcCommandInput, + options: QuotaRpcCallOptions, + ) => Promise; +}; type TuiContext = { location?: { directory: string }; client: { + rpc: (definition: typeof QuotaRpc) => QuotaRpcClient; session: { inbox: { cancel: (input: { sessionID: string; inboxID: string }) => Promise }; }; @@ -55,9 +79,7 @@ type TuiContext = { data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void; session: { - get: ( - sessionID: string, - ) => { parentID?: string; model?: { id: string; providerID: string } } | undefined; + get: (sessionID: string) => { parentID?: string } | undefined; }; location?: { default: () => { directory: string }; @@ -114,22 +136,45 @@ function quotaRoots(context: TuiContext) { return resolveOpenCodeLocationRoots(context.location?.directory ?? process.cwd()); } -async function getSessionModelMeta( - context: TuiContext, - sessionID: string, -): Promise { - const model = context.data.session.get(sessionID)?.model; - return model ? { modelID: model.id, providerID: model.providerID } : {}; +/** + * The server plugin computes every quota surface; the TUI asks it over the quota RPC. The + * RPC client is made per call, never during setup. Each call names the TUI's location, so + * the server uses that folder's settings, and gives up after a minute. + */ +function quotaRpc(context: TuiContext): QuotaRpcClient { + return context.client.rpc(QuotaRpc); } -function quotaSurfaceHost(context: TuiContext): QuotaSurfaceHost { +function quotaRpcOptions(context: TuiContext): QuotaRpcCallOptions { return { - client: quotaClient(context), - roots: quotaRoots(context), - resolveSessionMeta: (id) => getSessionModelMeta(context, id), + location: context.location ?? context.data.location!.default(), + signal: AbortSignal.timeout(RPC_TIMEOUT_MS), }; } +async function getQuotaMessage( + context: TuiContext, + sessionID: string, + surface: QuotaRpcSurfaceInput["surface"], +) { + const output = await quotaRpc(context).surface({ surface, sessionID }, quotaRpcOptions(context)); + return output.quota ?? undefined; +} + +/** RPC calls reject with a plain `{ type, message }` object, not an Error. */ +function rpcErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + if ( + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" + ) { + return error.message; + } + return String(error); +} + /** * One view's refresh loop, as in v4 (tui-refresh-lifecycle.ts): a refresh that * arrives while a load runs is coalesced into one follow-up load, and a result is @@ -181,15 +226,23 @@ function QuotaFooter(props: { }): JSX.Element { const [lines, setLines] = createSignal([]); const view = createViewRefresh( - () => getQuotaFooter(quotaSurfaceHost(props.context), props.sessionID, props.surface), + async () => { + const output = await quotaRpc(props.context).footer( + { surface: props.surface, sessionID: props.sessionID }, + quotaRpcOptions(props.context), + ); + return output.lines; + }, (next) => { setLines(next); if (props.surface !== "home") return; - // Fire-and-forget: write the export file if enabled. A failed write + // Fire-and-forget: the server writes the export file if enabled. A failed write // must never affect rendering, so log a warning and continue. - void writeQuotaExportIfEnabled(quotaSurfaceHost(props.context)).catch((error) => { - console.warn(`[opencode-quota] quota export write failed: ${String(error)}`); - }); + void quotaRpc(props.context) + .writeExport({}, quotaRpcOptions(props.context)) + .catch((error: unknown) => { + console.warn(`[opencode-quota] quota export write failed: ${rpcErrorMessage(error)}`); + }); }, ); view.refresh(); @@ -214,8 +267,15 @@ function QuotaFooter(props: { } function reportFailure(error: unknown): void { - const message = error instanceof Error ? error.message : String(error); - console.warn(`[opencode-quota] failed to load quota: ${message}`); + // OpenCode answers rpc.unavailable when no server plugin registered the quota RPC here. + const hint = + typeof error === "object" && + error !== null && + "type" in error && + error.type === "rpc.unavailable" + ? " (OpenCode Quota's server plugin is not loaded for this folder)" + : ""; + console.warn(`[opencode-quota] failed to load quota: ${rpcErrorMessage(error)}${hint}`); } /** @@ -331,14 +391,10 @@ async function runQuotaCommand( } try { - const result = await buildQuotaDialogCommandOutput({ - command, - arguments: argumentsText, - client: quotaClient(context), - roots: quotaRoots(context), - sessionID, - resolveSessionMeta: (id) => getSessionModelMeta(context, id), - }); + const result = await quotaRpc(context).command( + { command, arguments: argumentsText, sessionID }, + quotaRpcOptions(context), + ); if (result.state === "noop") return; await showQuotaOutputDialog(context, { title: result.title, @@ -349,7 +405,7 @@ async function runQuotaCommand( context.ui.toast.show({ variant: "error", title: "OpenCode Quota", - message: sanitizeDisplayText(error instanceof Error ? error.message : String(error)), + message: sanitizeDisplayText(rpcErrorMessage(error)), }); } } @@ -419,7 +475,7 @@ function SidebarQuotaView(props: { context: TuiContext; sessionID: string }): JS const lines = () => quota()?.message.split("\n") ?? []; const expandable = () => lines().length > 2; const view = createViewRefresh( - () => getQuotaMessage(quotaSurfaceHost(props.context), props.sessionID, "sidebar"), + () => getQuotaMessage(props.context, props.sessionID, "sidebar"), setQuota, ); view.refresh(); @@ -479,7 +535,7 @@ const plugin = Plugin.define({ if (!sessionID) return; // As in v4, subagent (child) sessions never show quota toasts. if (api.data.session.get(sessionID)?.parentID) return; - void getQuotaMessage(quotaSurfaceHost(api), sessionID, reason) + void getQuotaMessage(api, sessionID, reason) .then((quota) => { if (!quota) return; api.ui.toast.show({ diff --git a/tests/helpers/quota-rpc-bridge.ts b/tests/helpers/quota-rpc-bridge.ts new file mode 100644 index 00000000..c0484ae3 --- /dev/null +++ b/tests/helpers/quota-rpc-bridge.ts @@ -0,0 +1,34 @@ +import { expect } from "vitest"; + +type QuotaRpcHandler = ( + input: unknown, + context: { + signal: AbortSignal; + error: (type: string, message: string, data?: unknown) => unknown; + }, +) => Promise; + +type QuotaRpcCallOptions = { location?: { directory: string }; signal?: AbortSignal }; + +/** + * A TUI fake's `client.rpc` that calls the handlers a server fake registered with + * `ctx.rpc.register`. Like OpenCode's HTTP route it carries JSON: input and output make a + * JSON round trip, and an output that is not a pure JSON value fails the call. + */ +export function createQuotaRpcBridge(handlers: Record) { + return (definition: { methods: Record }) => + Object.fromEntries( + Object.keys(definition.methods).map((method) => [ + method, + async (input: unknown, options?: QuotaRpcCallOptions) => { + expect(options?.location).toBeDefined(); + const output = await handlers[method](JSON.parse(JSON.stringify(input)), { + signal: options?.signal ?? new AbortController().signal, + error: (type, message, data) => ({ type, message, data }), + }); + expect(output).toStrictEqual(JSON.parse(JSON.stringify(output))); + return JSON.parse(JSON.stringify(output)); + }, + ]), + ); +} diff --git a/tests/lib.quota-surface-data.test.ts b/tests/lib.quota-surface-data.test.ts index 2fa8f5ea..542ac415 100644 --- a/tests/lib.quota-surface-data.test.ts +++ b/tests/lib.quota-surface-data.test.ts @@ -178,6 +178,48 @@ describe("quota surface data", () => { expect(collect).toHaveBeenCalledWith(expect.objectContaining({ formatStyle: "allWindows" })); }); + it("computes no prompt footer for the Home prompt, which has no session", async () => { + const runtimeModule = await import("../src/lib/quota-runtime-context.js"); + const resolve = vi.spyOn(runtimeModule, "resolveQuotaRuntimeContext"); + const { getQuotaFooter } = await import("../src/lib/quota-surface-data.js"); + + await expect(getQuotaFooter(createHost(), undefined, "prompt")).resolves.toEqual([]); + expect(resolve).not.toHaveBeenCalled(); + }); + + it("loads Home for every enabled provider without a session, even with onlyCurrentModel", async () => { + const runtimeModule = await import("../src/lib/quota-runtime-context.js"); + const renderDataModule = await import("../src/lib/quota-render-data.js"); + vi.spyOn(runtimeModule, "resolveQuotaRuntimeContext").mockResolvedValue({ + client: {}, + config: { + enabled: true, + onlyCurrentModel: true, + showSessionTokens: true, + formatStyle: "singleWindow", + percentDisplayMode: "remaining", + maintainerAnnouncements: { enabled: false, home: false }, + tuiPromptBar: { enabled: false }, + tuiCompactStatus: { enabled: true, homeBottom: true, sessionPrompt: true, maxWidth: 80 }, + }, + configMeta: {}, + providers: [], + resolveRuntimeProviderIds: vi.fn(), + session: { sessionID: "ses_1", sessionMeta: { modelID: "m", providerID: "p" } }, + } as never); + const collect = vi.spyOn(renderDataModule, "collectQuotaRenderData").mockResolvedValue({ + active: [], + data: { entries: [{ name: "Copilot", percentRemaining: 50 }], errors: [] }, + } as never); + const { getQuotaFooter } = await import("../src/lib/quota-surface-data.js"); + + await getQuotaFooter(createHost(), undefined, "home"); + + const params = collect.mock.calls[0][0]; + expect(params.config).toMatchObject({ onlyCurrentModel: false, showSessionTokens: false }); + expect(params.request).toEqual({ sessionID: undefined, sessionMeta: undefined }); + }); + it("computes the count-only Home announcement from the enabled providers", async () => { mocks.loadConfig.mockResolvedValue( makeQuotaToastTestConfig({ diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 57e41a7a..1afa93b5 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -1,14 +1,33 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveOpenCodeLocationRoots } from "../src/lib/config-file-utils.js"; import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-command-specs.js"; import tuiPlugin from "../src/tui-v2.js"; -const mocks = vi.hoisted(() => ({ build: vi.fn() })); +// The TUI must never read logins or build reports itself: the server plugin does both. +const mocks = vi.hoisted(() => ({ + build: vi.fn(), + readAuthFile: vi.fn(), + readAuthFileCached: vi.fn(), + readCredentialRows: vi.fn(), +})); vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ ...(await importOriginal()), buildQuotaDialogCommandOutput: mocks.build, })); +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readAuthFile: mocks.readAuthFile, + readAuthFileCached: mocks.readAuthFileCached, + readCredentialRows: mocks.readCredentialRows, +})); + +// Canned answers of the server plugin's quota RPC. +const rpc = { + surface: vi.fn(), + footer: vi.fn(), + writeExport: vi.fn(), + command: vi.fn(), +}; function startTui() { const commands = new Map Promise; slash?: unknown }>(); @@ -20,6 +39,7 @@ function startTui() { const slots = new Map unknown }>(); const context = { location: { directory: process.cwd() }, + client: { rpc: vi.fn(() => rpc) }, data: { session: { get: vi.fn() }, on: vi.fn((name: string, callback: (event: { data?: Record }) => void) => { @@ -57,8 +77,14 @@ function startTui() { describe("V2 CLI command boundary", () => { beforeEach(() => { - mocks.build.mockReset().mockResolvedValue({ + rpc.surface.mockReset().mockResolvedValue({ + quota: { message: "Copilot 81%", duration: 5000, activeProviderCount: 1 }, + }); + rpc.footer.mockReset().mockResolvedValue({ lines: ["Copilot 81%"] }); + rpc.writeExport.mockReset().mockResolvedValue({ written: false }); + rpc.command.mockReset().mockResolvedValue({ state: "output", + command: "quota", title: "Quota", output: "Quota ready", dialogSize: "large", @@ -80,12 +106,9 @@ describe("V2 CLI command boundary", () => { it("isolates commands from the model transcript and presents quota output locally", async () => { const tui = startTui(); await tui.commands.get("quota.quota")?.run(); - expect(mocks.build).toHaveBeenCalledWith( - expect.objectContaining({ - command: "quota", - sessionID: undefined, - roots: resolveOpenCodeLocationRoots(process.cwd()), - }), + expect(rpc.command).toHaveBeenCalledWith( + { command: "quota", sessionID: undefined, arguments: undefined }, + expect.objectContaining({ location: { directory: process.cwd() } }), ); expect(tui.show).toHaveBeenCalledOnce(); expect(tui.set).toHaveBeenCalledWith({ size: "large" }); @@ -97,11 +120,12 @@ describe("V2 CLI command boundary", () => { const tui = startTui(); tui.prompt.mockResolvedValue("not-a-date-range"); await tui.commands.get("quota.tokens_between")?.run(); - expect(mocks.build).toHaveBeenCalledWith( + expect(rpc.command).toHaveBeenCalledWith( expect.objectContaining({ command: "tokens_between", arguments: "not-a-date-range", }), + expect.anything(), ); tui.dispose?.(); }); @@ -112,12 +136,12 @@ describe("V2 CLI command boundary", () => { expect(tui.prompt).toHaveBeenCalledWith( expect.objectContaining({ placeholder: "YYYY-MM-DD YYYY-MM-DD" }), ); - expect(mocks.build).not.toHaveBeenCalled(); + expect(rpc.command).not.toHaveBeenCalled(); tui.dispose?.(); }); it("returns without a dialog when quota is disabled", async () => { - mocks.build.mockResolvedValue({ state: "noop", command: "quota", reason: "disabled" }); + rpc.command.mockResolvedValue({ state: "noop", command: "quota", reason: "disabled" }); const tui = startTui(); await tui.commands.get("quota.quota")?.run(); expect(tui.show).not.toHaveBeenCalled(); @@ -126,7 +150,7 @@ describe("V2 CLI command boundary", () => { }); it("shows command failures as sanitized TUI error toasts rather than injecting a message", async () => { - mocks.build.mockRejectedValue(new Error("quota unavailable")); + rpc.command.mockRejectedValue({ type: "rpc.internal", message: "quota unavailable" }); const tui = startTui(); await tui.commands.get("quota.quota")?.run(); expect(tui.toast).toHaveBeenCalledWith( @@ -139,6 +163,34 @@ describe("V2 CLI command boundary", () => { tui.dispose?.(); }); + it("never reads logins or builds reports in the TUI during setup, surfaces, and palette runs", async () => { + vi.stubGlobal("React", { + createElement: (type: unknown, props: Record | null) => + typeof type === "function" ? type(props ?? {}) : { type, props }, + }); + const tui = startTui(); + tui.slots.get("sidebar.content")?.render({ sessionID: "ses_1" }); + tui.slots.get("prompt.footer")?.render({ sessionID: "ses_1" }); + tui.slots.get("home.footer.status")?.render(); + tui.listeners.get("session.execution.succeeded")?.({ data: { sessionID: "ses_1" } }); + for (const command of tui.commands.values()) await command.run(); + await vi.waitFor(() => expect(tui.toast).toHaveBeenCalledOnce()); + await vi.waitFor(() => expect(rpc.writeExport).toHaveBeenCalledOnce()); + + expect(rpc.surface.mock.calls.map(([input]) => input)).toEqual([ + { surface: "sidebar", sessionID: "ses_1" }, + { surface: "idle", sessionID: "ses_1" }, + ]); + expect(rpc.footer).toHaveBeenCalledTimes(2); + // /tokens_between stops at its date prompt, which this fake cancels. + expect(rpc.command).toHaveBeenCalledTimes(QUOTA_DIALOG_COMMANDS.length - 1); + expect(mocks.readAuthFile).not.toHaveBeenCalled(); + expect(mocks.readAuthFileCached).not.toHaveBeenCalled(); + expect(mocks.readCredentialRows).not.toHaveBeenCalled(); + expect(mocks.build).not.toHaveBeenCalled(); + tui.dispose?.(); + }); + it("does not offer V1 slash interception or agent-config normalization", async () => { // V2 TUI commands are local keymap registrations. The server plugin registers a tool and // V2 server commands for Web and Desktop; it has no V1 command.execute.before hook. diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 1bd98173..8a28e9e9 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -11,6 +11,7 @@ import { makeQuotaToastTestConfig, seedDefaultPluginBootstrapMocks, } from "./helpers/plugin-test-harness.js"; +import { createQuotaRpcBridge } from "./helpers/quota-rpc-bridge.js"; const TEST_RUNTIME_ROOT = "/tmp/opencode-quota-plugin-announcements-tests"; const TEST_ACCOUNTING = { @@ -58,6 +59,19 @@ vi.mock("../src/lib/alibaba-auth.js", () => vi.mock("../src/lib/opencode-runtime-paths.js", () => createPluginRuntimePathsMockModule(TEST_RUNTIME_ROOT), ); +// Records the getters of the TUI's Solid signals, so a test can read what a view shows. +const signals = vi.hoisted(() => [] as Array<() => unknown>); +vi.mock("solid-js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createSignal: ((...args: Parameters) => { + const signal = actual.createSignal(...args); + signals.push(signal[0]); + return signal; + }) as typeof actual.createSignal, + }; +}); vi.mock("../src/lib/maintainer-announcements.js", () => ({ BUNDLED_MAINTAINER_ANNOUNCEMENTS: [TEST_ANNOUNCEMENT], formatMaintainerAnnouncementHomeCountLine: @@ -118,24 +132,39 @@ function configureQuestionQuotaToast( ]); } +/** Starts the server plugin and the TUI plugin, joined by the quota RPC. */ async function startCli() { vi.stubGlobal("React", { createElement: (type: unknown, props: Record | null) => typeof type === "function" ? type(props ?? {}) : { type, props }, }); + const { default: server } = await import("../src/plugin.js"); + const register = vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })); + await server.setup({ + location: { directory: process.cwd() }, + provider: { list: vi.fn(async () => ({ data: [{ id: "copilot" }] })) }, + session: { get: vi.fn(async () => ({})), hook: vi.fn() }, + command: { transform: vi.fn() }, + tool: { transform: vi.fn() }, + rpc: { register }, + } as never); + const [, handlers] = register.mock.calls[0] as unknown as [ + unknown, + Parameters[0], + ]; const { default: plugin } = await import("../src/tui-v2.js"); const listeners = new Map }) => void>(); const renderers = new Map unknown>(); const toast = vi.fn(); const context = { location: { directory: process.cwd() }, + client: { rpc: createQuotaRpcBridge(handlers) }, data: { session: { get: vi.fn(() => undefined) }, on: vi.fn((name: string, listener: (event: { data: Record }) => void) => { listeners.set(name, listener); return () => listeners.delete(name); }), - location: { provider: { list: () => [{ id: "copilot" }] } }, }, keymap: { layer: vi.fn() }, ui: { @@ -205,6 +234,7 @@ describe("maintainer announcement plugin integration", () => { if (activeCount === 1) return ANNOUNCEMENT_HOME_MESSAGE; return `Notice: ${activeCount} maintainer announcements available. Run /quota_announcements.`; }); + signals.length = 0; await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); @@ -303,10 +333,9 @@ describe("maintainer announcement plugin integration", () => { configureQuestionQuotaToast(); const cli = await startCli(); cli.renderHome(); + // The Home footer shows the lines the server returns over the RPC. await vi.waitFor(() => - expect(announcementMocks.formatMaintainerAnnouncementHomeCountLine).toHaveReturnedWith( - ANNOUNCEMENT_HOME_MESSAGE, - ), + expect(signals.map((read) => read())).toContainEqual([ANNOUNCEMENT_HOME_MESSAGE]), ); expect(announcementMocks.getMaintainerAnnouncementsSummary).toHaveBeenCalledWith( expect.objectContaining({ enabledProviders: ["copilot"] }), diff --git a/tests/plugin.question-hook.test.ts b/tests/plugin.question-hook.test.ts index 8078e7d0..9a46720a 100644 --- a/tests/plugin.question-hook.test.ts +++ b/tests/plugin.question-hook.test.ts @@ -1,26 +1,22 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const resolveQuotaRuntimeContext = vi.hoisted(() => vi.fn()); -vi.mock("../src/lib/quota-runtime-context.js", async (importOriginal) => ({ - ...(await importOriginal()), - resolveQuotaRuntimeContext, -})); - import plugin from "../src/tui-v2.tsx"; describe("V2 CLI question-tool accounting boundary", () => { const handlers = new Map }) => void>(); const session = { get: vi.fn() }; const toast = vi.fn(); + // With showOnQuestion off, the server answers the question surface with no quota. + const rpc = { surface: vi.fn() }; beforeEach(() => { handlers.clear(); session.get.mockReset(); toast.mockReset(); - resolveQuotaRuntimeContext.mockReset().mockResolvedValue({ - config: { enabled: true, enableToast: true, showOnQuestion: false }, - }); + rpc.surface.mockReset().mockResolvedValue({ quota: null }); plugin.setup({ + client: { rpc: () => rpc }, + location: { directory: process.cwd() }, data: { session, on: (name: string, handler: (event: { data: Record }) => void) => { @@ -42,8 +38,12 @@ describe("V2 CLI question-tool accounting boundary", () => { it("does not treat a successful question-tool execution as a completed model request", async () => { handlers.get("session.tool.input.started")?.({ data: { name: "question", id: "call-1" } }); handlers.get("session.tool.success")?.({ data: { sessionID: "session-1", id: "call-1" } }); - await vi.waitFor(() => expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1)); - // Only the subagent check reads the session; no model lookup follows. + await vi.waitFor(() => expect(rpc.surface).toHaveBeenCalledTimes(1)); + expect(rpc.surface).toHaveBeenCalledWith( + { surface: "question", sessionID: "session-1" }, + expect.anything(), + ); + // Only the subagent check reads the session; the server looks up the model. expect(session.get).toHaveBeenCalledExactlyOnceWith("session-1"); expect(toast).not.toHaveBeenCalled(); }); @@ -51,7 +51,7 @@ describe("V2 CLI question-tool accounting boundary", () => { it("does not use question-tool failure metadata as accounting authority", () => { handlers.get("session.tool.input.started")?.({ data: { name: "question", id: "call-2" } }); handlers.get("session.tool.failed")?.({ data: { sessionID: "session-1", id: "call-2" } }); - expect(resolveQuotaRuntimeContext).not.toHaveBeenCalled(); + expect(rpc.surface).not.toHaveBeenCalled(); expect(session.get).not.toHaveBeenCalled(); expect(toast).not.toHaveBeenCalled(); }); diff --git a/tests/plugin.tokens-session-all.test.ts b/tests/plugin.tokens-session-all.test.ts index 3b5adf71..67741c63 100644 --- a/tests/plugin.tokens-session-all.test.ts +++ b/tests/plugin.tokens-session-all.test.ts @@ -118,7 +118,9 @@ describe("/tokens_session_all command", () => { (command) => command.id === "tokens_session_all", ); let commands: Array<{ id: string; title: string }> = []; + const rpc = vi.fn(); plugin.setup({ + client: { rpc }, keymap: { layer: (build: () => { commands: typeof commands }) => { commands = build().commands; @@ -136,6 +138,8 @@ describe("/tokens_session_all command", () => { expect(commands.find((command) => command.id === "quota.tokens_session_all")).toEqual( expect.objectContaining({ title: tokensSessionAllCommand?.title }), ); + // Registering the palette makes no RPC call; each command makes its RPC client when run. + expect(rpc).not.toHaveBeenCalled(); }); it("aggregates the current session tree for /tokens_session_all", async () => { diff --git a/tests/tui-dist-import-graph.test.ts b/tests/tui-dist-import-graph.test.ts new file mode 100644 index 00000000..0d9f1862 --- /dev/null +++ b/tests/tui-dist-import-graph.test.ts @@ -0,0 +1,47 @@ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const distDir = fileURLToPath(new URL("../dist/", import.meta.url)); + +/** Every dist file that dist/tui.js loads through relative `import` and `export ... from`. */ +function reachableFrom(entry: string): string[] { + const reached = new Set(); + const pending = [entry]; + for (let file = pending.pop(); file !== undefined; file = pending.pop()) { + if (reached.has(file)) continue; + reached.add(file); + const source = readFileSync(file, "utf8"); + for (const match of source.matchAll( + /\b(?:import|export)\b[^"';]*?["'](\.{1,2}\/[^"']+)["']/g, + )) { + pending.push(resolve(dirname(file), match[1])); + } + } + return [...reached].map((file) => relative(distDir, file)).sort(); +} + +describe("tui dist import graph", () => { + // Run after `pnpm build`. The TUI asks the server plugin for quota over the RPC, so it + // must load no login reader and no provider code. + it("reaches no credential or provider module from dist/tui.js", () => { + const entry = resolve(distDir, "tui.js"); + expect(existsSync(entry)).toBe(true); + + const reached = reachableFrom(entry); + + expect(reached).toContain("rpc.js"); + expect(reached).not.toContain("lib/opencode-auth.js"); + expect(reached.filter((file) => file.startsWith("providers/"))).toEqual([]); + // node:sqlite is allowed: the session and token-history storage (opencode-storage.js -> + // opencode-sqlite.js) is reachable through config.js -> quota-providers.js -> + // quota-stats.js, but it opens a database only when called, and the TUI never calls it. + // It reads no logins; only the login reader's SQL names the credential table. + expect( + reached.filter((file) => + readFileSync(resolve(distDir, file), "utf8").includes("FROM credential"), + ), + ).toEqual([]); + }); +}); diff --git a/tests/tui-dist-packaging.test.ts b/tests/tui-dist-packaging.test.ts index 2364ba71..afd74250 100644 --- a/tests/tui-dist-packaging.test.ts +++ b/tests/tui-dist-packaging.test.ts @@ -57,8 +57,9 @@ describe("tui dist packaging", () => { expect(source).toContain("sidebar.content"); expect(source).toContain("prompt.footer"); expect(source).toContain("home.footer.status"); - expect(source).toContain("./lib/quota-surface-data.js"); - expect(source).toContain("buildQuotaDialogCommandOutput"); + expect(source).toContain("./rpc.js"); + expect(source).toContain("client.rpc("); + expect(source).toContain("writeExport"); expect(source).toContain("registerQuotaCommands"); expect(source).not.toContain("jsx-dev-runtime"); }); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 98115776..843ee517 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -3,11 +3,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; -const build = vi.hoisted(() => vi.fn()); -vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ - ...(await importOriginal()), - buildQuotaDialogCommandOutput: build, -})); vi.mock("@opentui/solid", () => ({ useTerminalDimensions: () => () => ({ width: 120, height: 40 }), })); @@ -21,6 +16,9 @@ vi.mock("../src/lib/config.js", async (importOriginal) => { import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; import plugin from "../src/tui-v2.tsx"; +// The server plugin's quota RPC runs palette commands; the TUI shows their output. +const rpc = vi.hoisted(() => ({ command: vi.fn() })); + type Node = { type: string; props: Record }; function findNode(node: unknown, type: string): Node | undefined { @@ -53,7 +51,10 @@ function startTui( const listeners = new Map(); const context = { location, - client: { session: { inbox: { cancel: vi.fn().mockResolvedValue(undefined) } } }, + client: { + rpc: vi.fn(() => rpc), + session: { inbox: { cancel: vi.fn().mockResolvedValue(undefined) } }, + }, theme: { surface: vi.fn(() => ({ text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, @@ -65,13 +66,8 @@ function startTui( listeners.set(name, listener); return vi.fn(); }), - session: { - get: vi.fn((sessionID: string) => - sessionID === "ses_known" - ? { model: { id: "claude-sonnet-4-5", providerID: "anthropic" } } - : undefined, - ), - }, + session: { get: vi.fn() }, + location: { default: vi.fn(() => ({ directory: "/work/default" })) }, }, keymap: { layer: vi.fn((next) => { @@ -102,8 +98,9 @@ function startTui( describe("V2 quota TUI commands", () => { beforeEach(() => { - build.mockReset().mockResolvedValue({ + rpc.command.mockReset().mockResolvedValue({ state: "output", + command: "tokens_today", title: "Tokens", output: "ok", dialogSize: "large", @@ -113,6 +110,8 @@ describe("V2 quota TUI commands", () => { it("registers every quota command in the palette and leaves slash commands to the server", () => { const { context, commands } = startTui(); + // The RPC client is made per call, never during setup. + expect(context.client.rpc).not.toHaveBeenCalled(); expect(commands.map((command) => command.id)).toEqual([ "quota.quota", "quota.quota_status", @@ -154,12 +153,13 @@ describe("V2 quota TUI commands", () => { await command("tokens_between").run(); expect(context.ui.dialog.prompt).toHaveBeenCalledOnce(); - expect(build).not.toHaveBeenCalled(); + expect(rpc.command).not.toHaveBeenCalled(); context.ui.dialog.prompt.mockResolvedValue(" 2026-09-01 2026-09-25 "); await command("tokens_between").run(); - expect(build).toHaveBeenCalledWith( - expect.objectContaining({ command: "tokens_between", arguments: "2026-09-01 2026-09-25" }), + expect(rpc.command).toHaveBeenCalledWith( + { command: "tokens_between", arguments: "2026-09-01 2026-09-25", sessionID: undefined }, + expect.anything(), ); }); @@ -170,7 +170,7 @@ describe("V2 quota TUI commands", () => { await command("pricing_refresh").run(); expect(context.ui.dialog.prompt).not.toHaveBeenCalled(); - expect(build.mock.calls.map(([params]) => [params.command, params.arguments])).toEqual([ + expect(rpc.command.mock.calls.map(([input]) => [input.command, input.arguments])).toEqual([ ["quota_announcements", undefined], ["pricing_refresh", undefined], ]); @@ -271,7 +271,7 @@ describe("V2 quota TUI commands", () => { props: Record; }; expect(render().props).toMatchObject({ title: "OpenCode Quota", message: "openai 42%" }); - expect(build).not.toHaveBeenCalled(); + expect(rpc.command).not.toHaveBeenCalled(); }); it("inline mode leaves the report in the chat and opens no dialog", async () => { @@ -297,20 +297,30 @@ describe("V2 quota TUI commands", () => { }); }); - it("reads project config from the location's Git worktree root, like the server plugin", async () => { + it("reads tuiCommandDisplay from the location's Git worktree root, like the server plugin", async () => { const repo = realpathSync(mkdtempSync(join(tmpdir(), "opencode-quota-tui-roots-"))); try { mkdirSync(join(repo, ".git")); mkdirSync(join(repo, "packages", "app"), { recursive: true }); - const { command } = startTui({ directory: join(repo, "packages", "app") }); - - await command("quota").run(); + const { emit } = startTui( + { directory: join(repo, "packages", "app") }, + { type: "session", sessionID: "ses_open" }, + ); - expect(build.mock.calls[0][0].roots).toEqual({ - workspaceRoot: repo, - configRoot: repo, - fallbackDirectory: join(repo, "packages", "app"), + emit("session.inbox.enqueued", { + sessionID: "ses_open", + inboxID: "msg_report", + item: { + type: "user", + payload: { + text: formatQuotaReportMessage("openai 42%"), + metadata: { opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1 } }, + }, + }, }); + await vi.waitFor(() => expect(loadConfig).toHaveBeenCalledOnce()); + + expect(loadConfig.mock.calls[0][2]).toEqual({ configRootDir: repo }); } finally { rmSync(repo, { recursive: true, force: true }); } @@ -322,21 +332,37 @@ describe("V2 quota TUI commands", () => { .run(); await startTui(undefined, { type: "home" }).command("tokens_session").run(); - expect(build.mock.calls.map(([params]) => params.sessionID)).toEqual(["ses_open", undefined]); + expect(rpc.command.mock.calls.map(([input]) => input.sessionID)).toEqual([ + "ses_open", + undefined, + ]); }); - it("reads the session model from the OpenCode 2 session store", async () => { + it("runs commands at the TUI location, else the default location, with a timeout", async () => { + await startTui({ directory: "/work/project" }).command("quota").run(); const { context, command } = startTui(); - await command("quota_status").run(); - const { resolveSessionMeta } = build.mock.calls[0][0]; - await expect(resolveSessionMeta("ses_known")).resolves.toEqual({ - modelID: "claude-sonnet-4-5", - providerID: "anthropic", + expect(rpc.command.mock.calls.map(([, options]) => options)).toEqual([ + { location: { directory: "/work/project" }, signal: expect.any(AbortSignal) }, + { location: { directory: "/work/default" }, signal: expect.any(AbortSignal) }, + ]); + // The server looks up the session model; the palette never reads it. + expect(context.data.session.get).not.toHaveBeenCalled(); + }); + + it("shows the message of a failed RPC call in the error toast", async () => { + rpc.command.mockRejectedValue({ type: "rpc.internal", message: "server \u001b[31mbroke" }); + const { context, command } = startTui(); + + await command("quota").run(); + + expect(context.ui.toast.show).toHaveBeenCalledExactlyOnceWith({ + variant: "error", + title: "OpenCode Quota", + message: "server broke", }); - await expect(resolveSessionMeta("ses_unknown")).resolves.toEqual({}); - expect(context.data.session.get).toHaveBeenCalledWith("ses_known"); + expect(context.ui.dialog.show).not.toHaveBeenCalled(); }); it("shows command output in a scrollable dialog that fits the terminal", async () => { @@ -351,8 +377,9 @@ describe("V2 quota TUI commands", () => { }, }); const output = Array.from({ length: 80 }, (_, index) => `line ${index + 1}`).join("\n"); - build.mockResolvedValue({ + rpc.command.mockResolvedValue({ state: "output", + command: "quota_status", title: "Quota Status", output, dialogSize: "xlarge", diff --git a/tests/tui-v2-format-style.test.ts b/tests/tui-v2-format-style.test.ts index a4121a85..1a410cf5 100644 --- a/tests/tui-v2-format-style.test.ts +++ b/tests/tui-v2-format-style.test.ts @@ -1,79 +1,30 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -const { buildSidebarQuotaPanelLines, collectQuotaRenderData, resolveQuotaRuntimeContext } = - vi.hoisted(() => ({ - buildSidebarQuotaPanelLines: vi.fn(), - collectQuotaRenderData: vi.fn(), - resolveQuotaRuntimeContext: vi.fn(), - })); - -vi.mock("../src/lib/quota-render-data.js", async () => { - const actual = await vi.importActual( - "../src/lib/quota-render-data.js", - ); - return { ...actual, collectQuotaRenderData }; -}); - -vi.mock("../src/lib/quota-runtime-context.js", async () => { - const actual = await vi.importActual( - "../src/lib/quota-runtime-context.js", - ); - return { ...actual, resolveQuotaRuntimeContext }; -}); - -vi.mock("../src/lib/tui-sidebar-format.js", async () => { - const actual = await vi.importActual( - "../src/lib/tui-sidebar-format.js", - ); - return { ...actual, buildSidebarQuotaPanelLines }; -}); - import plugin from "../src/tui-v2.tsx"; describe("V2 sidebar format style", () => { afterEach(() => vi.unstubAllGlobals()); - it("projects all sidebar windows before formatting when it overrides singleWindow", async () => { + // The server applies tuiSidebarPanel.formatStyle (tests/lib.quota-surface-data.test.ts). + it("asks the server for the sidebar surface of its session and formats nothing itself", async () => { vi.stubGlobal("React", { createElement: (type: unknown, props: Record) => typeof type === "function" ? type(props) : { type, props }, }); - resolveQuotaRuntimeContext.mockResolvedValue({ - client: {}, - config: { - enabled: true, - enableToast: true, - formatStyle: "singleWindow", - percentDisplayMode: "remaining", - resetTimeDecimals: undefined, - toastDurationMs: 5000, - tuiSidebarPanel: { enabled: true, formatStyle: "allWindows" }, - }, - configMeta: {}, - providers: [], - resolveRuntimeProviderIds: vi.fn(), - session: { sessionID: "session-1" }, - }); - collectQuotaRenderData.mockImplementation(async ({ formatStyle }) => ({ - active: [{ id: "copilot" }], - data: { - entries: - formatStyle === "allWindows" - ? [ - { name: "Copilot 5h", percentRemaining: 50 }, - { name: "Copilot Weekly", percentRemaining: 80 }, - ] - : [{ name: "Copilot 5h", percentRemaining: 50 }], - errors: [], - }, - })); - buildSidebarQuotaPanelLines.mockImplementation(({ data }) => - data.entries.map((entry: { name: string }) => entry.name), - ); + const rpc = { + surface: vi.fn().mockResolvedValue({ + quota: { + message: "Copilot 5h\nCopilot Weekly", + duration: 5000, + activeProviderCount: 1, + }, + }), + }; let sidebarRender: ((props: { sessionID: string }) => unknown) | undefined; plugin.setup({ - client: {}, + client: { rpc: () => rpc }, + location: { directory: "/work/project" }, data: { on: vi.fn(() => vi.fn()) }, keymap: { layer: vi.fn() }, ui: { @@ -89,21 +40,10 @@ describe("V2 sidebar format style", () => { sidebarRender?.({ sessionID: "session-1" }); - await vi.waitFor(() => { - expect(buildSidebarQuotaPanelLines).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - entries: [ - expect.objectContaining({ name: "Copilot 5h" }), - expect.objectContaining({ name: "Copilot Weekly" }), - ], - }), - config: expect.objectContaining({ formatStyle: "allWindows" }), - }), - ); - }); - expect(collectQuotaRenderData).toHaveBeenCalledWith( - expect.objectContaining({ formatStyle: "allWindows" }), + await vi.waitFor(() => expect(rpc.surface).toHaveBeenCalledOnce()); + expect(rpc.surface).toHaveBeenCalledWith( + { surface: "sidebar", sessionID: "session-1" }, + { location: { directory: "/work/project" }, signal: expect.any(AbortSignal) }, ); }); }); diff --git a/tests/tui-v2-refresh.test.ts b/tests/tui-v2-refresh.test.ts index 27c93f86..387d743e 100644 --- a/tests/tui-v2-refresh.test.ts +++ b/tests/tui-v2-refresh.test.ts @@ -1,35 +1,6 @@ import { createRoot } from "solid-js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const { buildQuotaExport, collectQuotaRenderData, resolveQuotaRuntimeContext, writeQuotaExport } = - vi.hoisted(() => ({ - buildQuotaExport: vi.fn(), - collectQuotaRenderData: vi.fn(), - resolveQuotaRuntimeContext: vi.fn(), - writeQuotaExport: vi.fn(), - })); - -vi.mock("../src/lib/quota-render-data.js", async () => { - const actual = await vi.importActual( - "../src/lib/quota-render-data.js", - ); - return { ...actual, collectQuotaRenderData }; -}); - -vi.mock("../src/lib/quota-runtime-context.js", async () => { - const actual = await vi.importActual( - "../src/lib/quota-runtime-context.js", - ); - return { ...actual, resolveQuotaRuntimeContext }; -}); - -vi.mock("../src/lib/quota-export.js", async () => { - const actual = await vi.importActual( - "../src/lib/quota-export.js", - ); - return { ...actual, buildQuotaExport, createExportProviderContext: vi.fn(), writeQuotaExport }; -}); - import plugin from "../src/tui-v2.tsx"; const REFRESH_INTERVAL_MS = 60_000; @@ -38,12 +9,23 @@ async function flushPromises(): Promise { for (let i = 0; i < 20; i += 1) await Promise.resolve(); } +// Canned answers of the server plugin's quota RPC. +const rpc = { + surface: vi.fn(), + footer: vi.fn(), + writeExport: vi.fn(), + command: vi.fn(), +}; +const client = { rpc: vi.fn(() => rpc) }; + function setupFooterSlots( handlers = new Map void>(), ): Map unknown> { const renderers = new Map unknown>(); plugin.setup({ + client, data: { + location: { default: () => ({ directory: "/work/default" }) }, on: vi.fn((event: string, handler: (event: unknown) => void) => { handlers.set(event, handler); return vi.fn(); @@ -88,33 +70,9 @@ describe("V2 footer refresh timer", () => { createElement: (type: unknown, props: Record | null) => typeof type === "function" ? type(props ?? {}) : { type, props }, }); - resolveQuotaRuntimeContext.mockReset(); - collectQuotaRenderData.mockReset(); - buildQuotaExport.mockReset(); - writeQuotaExport.mockReset(); - resolveQuotaRuntimeContext.mockResolvedValue({ - client: {}, - config: { - enabled: true, - formatStyle: "singleWindow", - percentDisplayMode: "remaining", - minIntervalMs: 60_000, - maintainerAnnouncements: { enabled: false, home: false }, - tuiPromptBar: { enabled: false }, - tuiCompactStatus: { enabled: true, homeBottom: true, sessionPrompt: true, maxWidth: 80 }, - export: { enabled: true, path: "/tmp/opencode-quota-refresh-test.json" }, - }, - configMeta: {}, - providers: [], - resolveRuntimeProviderIds: vi.fn(), - session: {}, - }); - collectQuotaRenderData.mockResolvedValue({ - active: [], - data: { entries: [{ name: "Copilot", percentRemaining: 50 }], errors: [] }, - }); - buildQuotaExport.mockResolvedValue({ version: 2 }); - writeQuotaExport.mockResolvedValue(undefined); + rpc.surface.mockReset().mockResolvedValue({ quota: null }); + rpc.footer.mockReset().mockResolvedValue({ lines: ["Copilot 50%"] }); + rpc.writeExport.mockReset().mockResolvedValue({ written: true }); }); afterEach(() => { @@ -126,39 +84,40 @@ describe("V2 footer refresh timer", () => { const renderers = setupFooterSlots(); const dispose = mountSlot(renderers.get("home.footer.status")); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); - expect(writeQuotaExport).toHaveBeenCalledTimes(1); + expect(rpc.footer).toHaveBeenCalledTimes(1); + expect(rpc.writeExport).toHaveBeenCalledTimes(1); + expect(rpc.writeExport).toHaveBeenCalledWith({}, expect.anything()); await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS - 1); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + expect(rpc.footer).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(1); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); - expect(writeQuotaExport).toHaveBeenCalledTimes(2); + expect(rpc.footer).toHaveBeenCalledTimes(2); + expect(rpc.writeExport).toHaveBeenCalledTimes(2); dispose(); await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS * 3); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); - expect(writeQuotaExport).toHaveBeenCalledTimes(2); + expect(rpc.footer).toHaveBeenCalledTimes(2); + expect(rpc.writeExport).toHaveBeenCalledTimes(2); }); it("refreshes the prompt footer every minute without writing the export", async () => { const renderers = setupFooterSlots(); const dispose = mountSlot(renderers.get("prompt.footer"), { sessionID: "ses_1" }); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + expect(rpc.footer).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); + expect(rpc.footer).toHaveBeenCalledTimes(2); dispose(); await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS * 3); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); - expect(writeQuotaExport).not.toHaveBeenCalled(); + expect(rpc.footer).toHaveBeenCalledTimes(2); + expect(rpc.writeExport).not.toHaveBeenCalled(); }); it("loads the prompt footer for the session its composer passes in", async () => { @@ -166,21 +125,21 @@ describe("V2 footer refresh timer", () => { const renderers = setupFooterSlots(handlers); const dispose = mountSlot(renderers.get("prompt.footer"), { sessionID: "ses_2" }); await flushPromises(); - expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1); - expect(resolveQuotaRuntimeContext.mock.calls[0][0].sessionID).toBe("ses_2"); + expect(rpc.footer).toHaveBeenCalledTimes(1); + expect(rpc.footer.mock.calls[0][0]).toEqual({ surface: "prompt", sessionID: "ses_2" }); handlers.get("session.step.ended")?.({ data: { sessionID: "ses_1" } }); await flushPromises(); - expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1); + expect(rpc.footer).toHaveBeenCalledTimes(1); handlers.get("session.step.ended")?.({ data: { sessionID: "ses_2" } }); await flushPromises(); - expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(2); - expect(resolveQuotaRuntimeContext.mock.calls[1][0].sessionID).toBe("ses_2"); + expect(rpc.footer).toHaveBeenCalledTimes(2); + expect(rpc.footer.mock.calls[1][0]).toEqual({ surface: "prompt", sessionID: "ses_2" }); dispose(); }); - it("renders nothing under the Home prompt, which has no session", async () => { + it("asks the server for the Home prompt footer without a session", async () => { const renderers = setupFooterSlots(); const dispose = mountSlot(renderers.get("prompt.footer"), { sessionID: undefined, @@ -188,11 +147,13 @@ describe("V2 footer refresh timer", () => { showDetails: true, }); await flushPromises(); - await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS); - await flushPromises(); - expect(resolveQuotaRuntimeContext).not.toHaveBeenCalled(); - expect(collectQuotaRenderData).not.toHaveBeenCalled(); + // The server answers a prompt footer without a session with no lines. + expect(rpc.footer).toHaveBeenCalledExactlyOnceWith( + { surface: "prompt", sessionID: undefined }, + expect.anything(), + ); + expect(rpc.writeExport).not.toHaveBeenCalled(); dispose(); }); @@ -207,48 +168,92 @@ describe("V2 footer refresh timer", () => { }); handlers.get("session.tool.success")?.({ data: { sessionID: "ses_child", id: "call_1" } }); await flushPromises(); - expect(resolveQuotaRuntimeContext).not.toHaveBeenCalled(); + expect(rpc.surface).not.toHaveBeenCalled(); handlers.get("session.execution.succeeded")?.({ data: { sessionID: "ses_parent" } }); await flushPromises(); - expect(resolveQuotaRuntimeContext).toHaveBeenCalledTimes(1); - expect(resolveQuotaRuntimeContext.mock.calls[0][0].sessionID).toBe("ses_parent"); + expect(rpc.surface).toHaveBeenCalledExactlyOnceWith( + { surface: "idle", sessionID: "ses_parent" }, + expect.anything(), + ); }); - it("loads Home for every enabled provider without a session, even with onlyCurrentModel", async () => { - const runtime = await resolveQuotaRuntimeContext(); - resolveQuotaRuntimeContext.mockResolvedValue({ - ...runtime, - config: { ...runtime.config, onlyCurrentModel: true, showSessionTokens: true }, - session: { sessionID: "ses_1", sessionMeta: { modelID: "m", providerID: "p" } }, - }); + it("asks the server for Home without a session", async () => { const renderers = setupFooterSlots(); const dispose = mountSlot(renderers.get("home.footer.status")); await flushPromises(); - const params = collectQuotaRenderData.mock.calls[0][0]; - expect(params.config).toMatchObject({ onlyCurrentModel: false, showSessionTokens: false }); - expect(params.request).toEqual({ sessionID: undefined, sessionMeta: undefined }); + expect(rpc.footer).toHaveBeenCalledExactlyOnceWith( + { surface: "home", sessionID: undefined }, + expect.anything(), + ); dispose(); }); + it("calls the RPC at the TUI location with a one-minute timeout", async () => { + const timeout = vi.spyOn(AbortSignal, "timeout"); + const handlers = new Map void>(); + const renderers = setupFooterSlots(handlers); + expect(client.rpc).not.toHaveBeenCalled(); + mountSlot(renderers.get("app")); + const dispose = mountSlot(renderers.get("home.footer.status")); + handlers.get("session.execution.succeeded")?.({ data: { sessionID: "ses_parent" } }); + await flushPromises(); + + const { QuotaRpc } = await import("../src/rpc.js"); + expect(client.rpc).toHaveBeenCalledWith(QuotaRpc); + const calls = [ + ...rpc.footer.mock.calls, + ...rpc.writeExport.mock.calls, + ...rpc.surface.mock.calls, + ]; + expect(calls).toHaveLength(3); + for (const [, options] of calls) { + expect(options).toEqual({ + location: { directory: "/work/default" }, + signal: expect.any(AbortSignal), + }); + } + expect(timeout).toHaveBeenCalledTimes(3); + expect(timeout).toHaveBeenCalledWith(60_000); + dispose(); + }); + + it("logs a failed load with the RPC error message", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + rpc.footer.mockRejectedValueOnce({ type: "rpc.internal", message: "server broke" }); + rpc.footer.mockRejectedValueOnce({ type: "rpc.unavailable", message: "no rpc" }); + const renderers = setupFooterSlots(); + mountSlot(renderers.get("home.footer.status"))(); + mountSlot(renderers.get("home.footer.status"))(); + await flushPromises(); + + expect(warn.mock.calls).toEqual([ + ["[opencode-quota] failed to load quota: server broke"], + [ + "[opencode-quota] failed to load quota: no rpc (OpenCode Quota's server plugin is not loaded for this folder)", + ], + ]); + expect(rpc.writeExport).not.toHaveBeenCalled(); + }); + it("drops a Home result and skips the export when the view unmounts mid-load", async () => { const load = deferred(); - collectQuotaRenderData.mockReturnValueOnce(load.promise); + rpc.footer.mockReturnValueOnce(load.promise); const renderers = setupFooterSlots(); const dispose = mountSlot(renderers.get("home.footer.status")); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + expect(rpc.footer).toHaveBeenCalledTimes(1); dispose(); - load.resolve({ active: [], data: { entries: [], errors: [] } }); + load.resolve({ lines: [] }); await flushPromises(); - expect(writeQuotaExport).not.toHaveBeenCalled(); + expect(rpc.writeExport).not.toHaveBeenCalled(); }); it("coalesces refreshes that arrive while a footer load is running into one follow-up load", async () => { const load = deferred(); - collectQuotaRenderData.mockReturnValueOnce(load.promise); + rpc.footer.mockReturnValueOnce(load.promise); const handlers = new Map void>(); const renderers = setupFooterSlots(handlers); const dispose = mountSlot(renderers.get("home.footer.status")); @@ -257,34 +262,32 @@ describe("V2 footer refresh timer", () => { handlers.get("session.step.ended")?.({ data: { sessionID: "ses_1" } }); handlers.get("session.step.ended")?.({ data: { sessionID: "ses_1" } }); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + expect(rpc.footer).toHaveBeenCalledTimes(1); - load.resolve({ active: [], data: { entries: [], errors: [] } }); + load.resolve({ lines: [] }); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(2); - expect(writeQuotaExport).toHaveBeenCalledTimes(2); + expect(rpc.footer).toHaveBeenCalledTimes(2); + expect(rpc.writeExport).toHaveBeenCalledTimes(2); dispose(); }); it("stops sidebar refreshes and drops their results after unmount", async () => { - const runtime = await resolveQuotaRuntimeContext(); - resolveQuotaRuntimeContext.mockResolvedValue({ - ...runtime, - config: { ...runtime.config, tuiSidebarPanel: { enabled: true } }, - }); const load = deferred(); - collectQuotaRenderData.mockReturnValueOnce(load.promise); + rpc.surface.mockReturnValueOnce(load.promise); const handlers = new Map void>(); const renderers = setupFooterSlots(handlers); const dispose = mountSlot(renderers.get("sidebar.content"), { sessionID: "ses_1" }); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + expect(rpc.surface).toHaveBeenCalledExactlyOnceWith( + { surface: "sidebar", sessionID: "ses_1" }, + expect.anything(), + ); handlers.get("session.step.ended")?.({ data: { sessionID: "ses_1" } }); dispose(); - load.resolve({ active: [], data: undefined }); + load.resolve({ quota: null }); await vi.advanceTimersByTimeAsync(REFRESH_INTERVAL_MS * 2); await flushPromises(); - expect(collectQuotaRenderData).toHaveBeenCalledTimes(1); + expect(rpc.surface).toHaveBeenCalledTimes(1); }); }); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 446e62cf..87aaa17d 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -23,6 +23,7 @@ import { makeQuotaToastTestConfig, seedDefaultPluginBootstrapMocks, } from "./helpers/plugin-test-harness.js"; +import { createQuotaRpcBridge } from "./helpers/quota-rpc-bridge.js"; const TEST_RUNTIME_ROOT = "/tmp/opencode-quota-v4-phase5-cross-surface"; const POSIX_IDENTITY_STORAGE = process.platform !== "win32" && typeof process.getuid === "function"; @@ -156,6 +157,7 @@ type RegisteredTool = { async function setupV2Surfaces(client: ReturnType, providerIds: string[]) { let tool: RegisteredTool | undefined; const { default: serverPlugin } = await import("../src/plugin.js"); + const register = vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })); await serverPlugin.setup({ location: { directory: process.cwd() }, provider: { list: vi.fn(async () => ({ data: providerIds.map((id) => ({ id })) })) }, @@ -164,7 +166,7 @@ async function setupV2Surfaces(client: ReturnType, provider hook: vi.fn(), }, command: { transform: vi.fn() }, - rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, + rpc: { register }, tool: { transform: vi.fn(async (callback: (editor: { add(value: RegisteredTool): void }) => void) => { callback({ @@ -176,6 +178,11 @@ async function setupV2Surfaces(client: ReturnType, provider }, } as never); expect(tool?.name).toBe("quota_status"); + // The TUI computes nothing itself: every surface reaches the server's RPC handlers. + const [, handlers] = register.mock.calls[0] as unknown as [ + unknown, + Parameters[0], + ]; const events = new Map void>>(); const emit = (event: string, sessionID: string) => { @@ -215,7 +222,7 @@ async function setupV2Surfaces(client: ReturnType, provider }); const { default: tuiPlugin } = await import("../src/tui-v2.js"); const dispose = tuiPlugin.setup({ - client, + client: { ...client, rpc: createQuotaRpcBridge(handlers) }, location: { directory: process.cwd() }, theme: { surface: () => ({ @@ -597,8 +604,8 @@ describe("v4 Phase 5 cross-surface release evidence", () => { ); const exportContext = createExportProviderContext(runtime); const { collectQuotaRenderData } = await import("../src/lib/quota-render-data.js"); - // The aggregate's process-local cache is scoped to the client object; V2 CLI - // constructs its own client adapter, so prime the export reader's context. + // The aggregate's process-local cache is scoped to the client object; the server + // plugin builds its own client adapter, so prime the export reader's context. await collectQuotaRenderData({ client: runtime.client, resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, From 713845c19cf864f0a348a60d9e64584582c472d1 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:37:26 +0200 Subject: [PATCH 67/97] feat(cli): read quota through the running OpenCode service `opencode-quota show` and `status` no longer compute quota in the terminal process. They find the OpenCode background service through its registration file ($XDG_STATE_HOME/opencode/service.json, checked against /api/info like OpenCode's own clients), then call the new `cli` method of the slkiser.opencode-quota RPC at the home folder with plain fetch. The CLI loads no @opencode/* package and never starts OpenCode. The server builds the same text, JSON and exit codes 0/1/2 as before with the report functions moved to src/lib/cli-reports.ts. When OpenCode is not running or the plugin cannot be reached, the command exits 3 with the reason on stderr and an empty stdout. The CLI's own provider detection (createCliQuotaClient) is gone: it now uses OpenCode's provider list, like every other surface. Docs: README, external integration (exit code 3, Starship reads the export file, the export file is written by the OpenCode service) and troubleshooting. The packed-package smoke checks the report function in-process and the packed CLI's exit 3 without a service. --- README.md | 8 +- docs/readme/external-integration.md | 14 +- docs/readme/troubleshooting.md | 6 +- scripts/fixtures/smoke-packed-real-otel.mjs | 27 +- scripts/smoke-packed-package.mjs | 29 +- src/bin/opencode-quota.ts | 6 +- src/lib/cli-reports.ts | 218 +++++ src/lib/cli-show.ts | 308 ++---- src/lib/cli-status.ts | 54 +- src/lib/opencode-service.ts | 70 ++ src/lib/quota-rpc-client.ts | 87 ++ src/plugin.ts | 42 +- src/rpc.ts | 25 + tests/lib.cli-reports.test.ts | 669 +++++++++++++ tests/lib.cli-show.test.ts | 993 +++----------------- tests/lib.cli-status.test.ts | 316 ++----- tests/lib.opencode-service.test.ts | 147 +++ tests/lib.quota-rpc-client.test.ts | 173 ++++ tests/plugin-rpc.test.ts | 88 +- tests/rpc-definition.test.ts | 39 +- 20 files changed, 1901 insertions(+), 1418 deletions(-) create mode 100644 src/lib/cli-reports.ts create mode 100644 src/lib/opencode-service.ts create mode 100644 src/lib/quota-rpc-client.ts create mode 100644 tests/lib.cli-reports.test.ts create mode 100644 tests/lib.opencode-service.test.ts create mode 100644 tests/lib.quota-rpc-client.test.ts diff --git a/README.md b/README.md index 9aed32f9..d4cbb057 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Upgrading from 4.x? Read [what changed in 5.0](#breaking-changes-in-500). After installation: 1. Restart OpenCode. -2. Run `/quota` in the OpenCode TUI, or `opencode-quota show` in a terminal. +2. Run `/quota` in the OpenCode TUI, or `opencode-quota show` in a terminal while OpenCode is running. 3. If you enabled the sidebar, open the session sidebar and look for `Quota`. 4. If you enabled the compact status line, look at the bottom of Home or below the message input. @@ -104,7 +104,7 @@ The updater always shows a preview first. `--yes` applies only the safe edits it More ways to use it: -- **Terminal:** run `npx @slkiser/opencode-quota show`. In Web and Desktop, slash commands post the report in the chat, and the AI can call the `quota_status` tool. +- **Terminal:** run `npx @slkiser/opencode-quota show` while OpenCode is running; it asks OpenCode's background service and exits with code `3` if OpenCode is not running. In Web and Desktop, slash commands post the report in the chat, and the AI can call the `quota_status` tool. - **Several logins:** each login for a provider gets its own rows; `*` marks the active one. - **Scripts and CI:** JSON output and optional OpenTelemetry metrics. See [External integration](docs/readme/external-integration.md). - **Display:** a quota bar under the prompt ([`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings)), OpenCode Go's collapsed-sidebar row ([`tuiSidebarPanel.opencodeGoPreferredWindow`](docs/readme/configuration.md#tui-settings)), reset countdown style ([`resetTimeSpaced`](docs/readme/configuration.md#common-changes), [`resetTimeDecimals`](docs/readme/configuration.md#common-changes)), bare `81%` labels ([`percentLabelStyle`](docs/readme/configuration.md#common-changes)), and extra accounting rows ([`accountingDetail`](docs/readme/configuration.md#show-accounting-detail)). @@ -147,7 +147,7 @@ Use the CLI for setup, updates, terminal checks, and custom providers. | `npx @slkiser/opencode-quota@latest status` | Check configuration and provider problems | | `npx @slkiser/opencode-quota@latest update` | Update an existing installation | -Run `npx @slkiser/opencode-quota@latest --help` for command options. See [External integration](docs/readme/external-integration.md#1-get-json-from-a-command) for JSON, scripts, and CI examples. +`show` and `status` need OpenCode running: they ask its background service, using your global quota settings. If OpenCode is not running, they exit with code `3`. Run `npx @slkiser/opencode-quota@latest --help` for command options. See [External integration](docs/readme/external-integration.md#1-get-json-from-a-command) for JSON, scripts, and CI examples. ## Providers @@ -246,7 +246,7 @@ The guided setup previews the change before saving. See the [custom-provider gui If quota or token data looks wrong: -1. Run `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal for the same diagnostics. Use `opencode-quota show` for a quick quota glance. +1. Run `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal for the same diagnostics (OpenCode must be running). Use `opencode-quota show` for a quick quota glance. 2. Confirm the expected provider appears in the detected provider list. If it is missing, log in to it again in OpenCode 2. 3. Confirm companion auth plugins are before `@slkiser/opencode-quota` in `opencode.json`. 4. If token reports are empty, start OpenCode once so it creates `opencode.db`, then run a session with model usage. diff --git a/docs/readme/external-integration.md b/docs/readme/external-integration.md index 81c5e77c..bfc1114a 100644 --- a/docs/readme/external-integration.md +++ b/docs/readme/external-integration.md @@ -20,6 +20,8 @@ Use this for scripts and CI: opencode-quota show --json ``` +The command asks OpenCode's background service for the data, so OpenCode must be running. It never starts OpenCode. It always runs for your home folder, so it uses your global quota settings, not a project's. + Useful variations: ```bash @@ -37,6 +39,9 @@ Threshold exit codes: | `0` | Quota is available and above the threshold | | `1` | At least one comparable cached percentage is below the threshold | | `2` | Results were incomplete or no comparable percentage was found | +| `3` | OpenCode is not running or the plugin could not be reached | + +Exit code `3` can come from any `show` or `status` command. The reason goes to stderr and stdout stays empty. ### CI example @@ -44,6 +49,8 @@ Threshold exit codes: npx @slkiser/opencode-quota show --json --threshold 5 ``` +This needs OpenCode running on the same machine and user account. Otherwise the command exits with code `3`. A tool that only needs recent numbers can read the [export file](#2-read-an-export-file) instead. + ### Read Copilot's percentage with `jq` Some Copilot results contain values instead of percentages. Select a percentage row instead of assuming the first row is one: @@ -73,9 +80,9 @@ The file is normally written here: ~/.cache/opencode/quota-export.json ``` -If you set `XDG_CACHE_HOME`, the file is written to `$XDG_CACHE_HOME/opencode/quota-export.json` instead. +If OpenCode runs with `XDG_CACHE_HOME` set, the file is written to `$XDG_CACHE_HOME/opencode/quota-export.json` instead. -The TUI refreshes the file about once a minute while the Home screen is open. A write error is logged, but it does not break the TUI. +OpenCode's background service rewrites the file about once a minute while the TUI Home screen is open. A write error is logged, but it does not break the TUI. ### tmux example @@ -92,7 +99,7 @@ Add this to `starship.toml`: ```toml [custom.quota] -command = "opencode-quota show --json 2>/dev/null | jq -r '[.providers|to_entries[]|select(.value.status==\"ok\")|first(.value.entries[]?|select(.renderType==\"percent\" and .percentRemaining!=null))|(.percentRemaining|floor|tostring)+\"%\"]|join(\" \")'" +command = "jq -r '[.providers|to_entries[]|select(.value.status==\"ok\")|first(.value.entries[]?|select(.renderType==\"percent\" and .percentRemaining!=null))|(.percentRemaining|floor|tostring)+\"%\"]|join(\" \")' ~/.cache/opencode/quota-export.json 2>/dev/null" when = "true" interval = 60 ``` @@ -262,6 +269,7 @@ Each summary is exactly `id`, effective `providerId`, coarse `status`, and `entr - All options use data collected during normal OpenCode Quota activity. - The command and export file read cached data instead of contacting providers. +- The command and the export file both come from OpenCode's background service. The command needs OpenCode running; the file keeps its last contents when OpenCode stops. - The OpenTelemetry integration reads in-memory results and never starts its own refresh loop. - OpenTelemetry metric labels are limited to safe provider, result type, and quota-window values. - Display names, account IDs, source IDs, credentials, paths, URLs, errors, and raw responses are never metric labels. diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index d7a6d544..2cce3fb8 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -2,11 +2,11 @@ # Troubleshooting -Start with `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal. Both show which config, providers, authentication, and local files OpenCode Quota found. +Start with `/quota_status` inside OpenCode, or `opencode-quota status` from a terminal while OpenCode is running. Both show which config, providers, authentication, and local files OpenCode Quota found. ## First checks -1. Run `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal. +1. Run `/quota_status` inside OpenCode. From a terminal, `opencode-quota status` shows the same report; it needs OpenCode running and uses your global quota settings. 2. Find the provider or feature that is failing. 3. Follow the matching fix below. 4. Restart OpenCode after changing config or authentication. @@ -28,6 +28,8 @@ OpenCode Quota reads logins only from OpenCode 2's `opencode.db`, never `auth.js | Token reports are empty | Start OpenCode once, then use a model so `opencode.db` contains usage. | | Pricing looks old | Run `/pricing_refresh`. | | Web report columns do not line up | Expected: Web uses a proportional font. Use the TUI or run `npx @slkiser/opencode-quota show` in a terminal. | +| Terminal command says `OpenCode is not running` (exit 3) | Open OpenCode or run `opencode service start`, then try again. The command never starts OpenCode itself. | +| Terminal command says the server plugin is not loaded (exit 3) | Check the plugin entry above, then run `opencode service restart`. | ## Update safely diff --git a/scripts/fixtures/smoke-packed-real-otel.mjs b/scripts/fixtures/smoke-packed-real-otel.mjs index 979fe2c4..585bce6d 100644 --- a/scripts/fixtures/smoke-packed-real-otel.mjs +++ b/scripts/fixtures/smoke-packed-real-otel.mjs @@ -434,14 +434,21 @@ async function runFailingInfrastructure() { } async function runSeedProductionOpenRouter() { - const { createCliQuotaClient, resolveCliRoots } = await packageImport("cli-show.js"); + const { buildCliShowJson } = await packageImport("cli-reports.js"); + const { resolveOpenCodeLocationRoots } = await packageImport("config-file-utils.js"); const { createExportProviderContext } = await packageImport("quota-export.js"); const { resolveQuotaRuntimeContext } = await packageImport("quota-runtime-context.js"); const secret = process.env.OPENROUTER_API_KEY; assert.ok(secret); - const roots = resolveCliRoots(process.cwd()); - const client = createCliQuotaClient({ configRootDir: roots.configRoot }); + // The same configuration client and roots the server plugin builds for its location. + const roots = resolveOpenCodeLocationRoots(process.cwd()); + const client = { + config: { + get: async () => ({ data: {} }), + providers: async () => ({ data: { providers: [] } }), + }, + }; const runtime = await resolveQuotaRuntimeContext({ client, roots, @@ -468,6 +475,20 @@ async function runSeedProductionOpenRouter() { assert.equal(exported.providers.openrouter.status, "ok"); assert.equal(exported.providers.openrouter.entries[0].percentRemaining, 67); assert.ok(!JSON.stringify(exported).includes(secret)); + + // `opencode-quota show --json` gets this report from the server plugin's `cli` RPC method. + const report = await buildCliShowJson({ runtime, providerId: "openrouter" }); + assert.equal(report.exitCode, 0); + assert.equal(report.stderr, ""); + const cachedOpenRouter = JSON.parse(report.stdout); + assert.equal(cachedOpenRouter.fromCache, true); + assert.equal(cachedOpenRouter.providers.openrouter.status, "ok"); + assert.equal( + cachedOpenRouter.providers.openrouter.entries[0].name, + "Packed production OpenRouter", + ); + assert.equal(cachedOpenRouter.providers.openrouter.entries[0].percentRemaining, 67); + process.stdout.write(report.stdout); } try { diff --git a/scripts/smoke-packed-package.mjs b/scripts/smoke-packed-package.mjs index 39b52884..615645e8 100644 --- a/scripts/smoke-packed-package.mjs +++ b/scripts/smoke-packed-package.mjs @@ -125,9 +125,12 @@ try { const openRouterSecret = "packed-openrouter-secret-canary"; const openRouterEnv = { ...isolatedRuntimeEnv, OPENROUTER_API_KEY: openRouterSecret }; - run(process.execPath, [realOtelFixture, "seed-production-openrouter"], workdir, { - env: openRouterEnv, - }); + const cachedOpenRouterOutput = run( + process.execPath, + [realOtelFixture, "seed-production-openrouter"], + workdir, + { env: openRouterEnv }, + ); const cliPath = path.join( workdir, @@ -150,20 +153,17 @@ try { } } - const cachedOpenRouterOutput = run( + // The isolated state folder has no OpenCode service registration, so the packed CLI must + // report that OpenCode is not running: exit 3, a stderr message and an empty stdout. + const offlineShow = spawnSync( process.execPath, [cliPath, "show", "--json", "--provider", "openrouter"], - workdir, - { env: openRouterEnv }, - ); - const cachedOpenRouter = JSON.parse(cachedOpenRouterOutput); - assert.equal(cachedOpenRouter.fromCache, true); - assert.equal(cachedOpenRouter.providers.openrouter.status, "ok"); - assert.equal( - cachedOpenRouter.providers.openrouter.entries[0].name, - "Packed production OpenRouter", + { cwd: workdir, encoding: "utf8", env: { ...process.env, ...openRouterEnv } }, ); - assert.equal(cachedOpenRouter.providers.openrouter.entries[0].percentRemaining, 67); + if (offlineShow.error) throw offlineShow.error; + assert.equal(offlineShow.status, 3); + assert.equal(offlineShow.stdout, ""); + assert.match(offlineShow.stderr, /OpenCode is not running\. Start OpenCode and try again\./); const readTree = async (directory) => { const chunks = []; @@ -176,6 +176,7 @@ try { }; const privateArtifacts = [ cachedOpenRouterOutput, + offlineShow.stderr, ...(await readTree(path.join(workdir, "runtime"))), ].join("\n"); assert.ok(!privateArtifacts.includes(openRouterSecret)); diff --git a/src/bin/opencode-quota.ts b/src/bin/opencode-quota.ts index d1c2e825..b21acf80 100644 --- a/src/bin/opencode-quota.ts +++ b/src/bin/opencode-quota.ts @@ -19,11 +19,11 @@ const USAGE = [ " init Run the interactive quota installer", " --dry-run Preview validated changes without writing files", " --sync-legacy-config also writes experimental.quotaToast", - " show Print a quick quota glance", + " show Print a quick quota glance (needs OpenCode running)", " --json Machine-readable JSON output (reads from cache)", " --threshold With --json, exit 1 if below %, 2 if incomplete/not comparable", " --provider Filter to one provider", - " status Print Quota Status diagnostics (same data as /quota_status)", + " status Print Quota Status diagnostics (same data as /quota_status; needs OpenCode running)", " --json Machine-readable JSON output", " --provider Filter to one provider", " update Preview safe setting/package-cache changes and report legacy credential findings", @@ -33,6 +33,8 @@ const USAGE = [ " --yes Apply only safe setting/cache work after printing the preview", " provider add Add or update one global quotaProviders definition", " --dry-run Preview the exact global OpenCode config without writing", + "", + "show and status exit with code 3 when OpenCode is not running or the plugin could not be reached.", ].join("\n"); function printUsage(): void { diff --git a/src/lib/cli-reports.ts b/src/lib/cli-reports.ts new file mode 100644 index 00000000..1c0d41de --- /dev/null +++ b/src/lib/cli-reports.ts @@ -0,0 +1,218 @@ +/** + * The reports behind `opencode-quota show` and `status`. The server plugin builds them for + * the `cli` RPC method; the terminal command only prints what they return. + */ +import { sanitizeQuotaRenderData } from "./display-sanitize.js"; +import { formatQuotaRows } from "./format.js"; +import { formatQuotaModeHeading } from "./format-utils.js"; +import { buildStatusReportData } from "./quota-dialog-commands.js"; +import { buildQuotaExport, createExportProviderContext } from "./quota-export.js"; +import { resolveQuotaFormatStyle } from "./quota-format-style.js"; +import { collectQuotaRenderData } from "./quota-render-data.js"; +import { + createQuotaRuntimeRequestContext, + type QuotaRuntimeContext, +} from "./quota-runtime-context.js"; +import type { QuotaToastConfig } from "./types.js"; +import { getPackageVersion } from "./version.js"; + +export type CliReport = { exitCode: number; stdout: string; stderr: string }; + +function line(message: string): string { + return message.endsWith("\n") ? message : `${message}\n`; +} + +const QUOTA_DISABLED: CliReport = { + exitCode: 1, + stdout: "", + stderr: line("Quota disabled in config (enabled: false)."), +}; + +function cloneCliConfig(config: QuotaToastConfig): QuotaToastConfig { + return { + ...config, + enabledProviders: Array.isArray(config.enabledProviders) + ? [...config.enabledProviders] + : config.enabledProviders, + opencodeGoWindows: [...config.opencodeGoWindows], + pricingSnapshot: { ...config.pricingSnapshot }, + layout: { ...config.layout }, + showSessionTokens: false, + }; +} + +/** `show`: live quota rows as text. Exit 1 when no provider returned a row. */ +export async function buildCliShowText(params: { + runtime: QuotaRuntimeContext; + providerId?: string; +}): Promise { + const { runtime, providerId } = params; + + if (!runtime.config.enabled) { + return QUOTA_DISABLED; + } + + const config = cloneCliConfig(runtime.config); + if (providerId) { + config.enabledProviders = [providerId]; + } + + const result = await collectQuotaRenderData({ + client: runtime.client, + resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, + config, + configMeta: runtime.configMeta, + request: createQuotaRuntimeRequestContext(runtime), + surfaceExplicitProviderIssues: true, + formatStyle: resolveQuotaFormatStyle(config.formatStyle), + providers: runtime.providers, + }); + + if (!result.data) { + return { exitCode: 1, stdout: "", stderr: line("No provider data available.") }; + } + + const data = sanitizeQuotaRenderData(result.data); + const version = (await getPackageVersion()) ?? ""; + const output = formatQuotaRows({ + version, + layout: config.layout, + entries: data.entries, + errors: data.errors, + style: resolveQuotaFormatStyle(config.formatStyle), + percentDisplayMode: config.percentDisplayMode, + percentLabelStyle: config.percentLabelStyle, + accountingDetail: config.accountingDetail, + resetTimeDecimals: config.resetTimeDecimals, + resetTimeSpaced: config.resetTimeSpaced, + }); + + if (!output.trim()) { + return { exitCode: 1, stdout: "", stderr: line("No provider data available.") }; + } + + return { + exitCode: data.entries.length > 0 ? 0 : 1, + stdout: line( + config.percentLabelStyle === "bare" + ? `${formatQuotaModeHeading(config.percentDisplayMode)}\n\n${output}` + : output, + ), + stderr: "", + }; +} + +/** + * `show --json`: cached quota as export JSON. With a threshold, exit 1 when a comparable + * percentage is below it and 2 when the data is incomplete or has no comparable percentage. + */ +export async function buildCliShowJson(params: { + runtime: QuotaRuntimeContext; + providerId?: string; + threshold?: number; +}): Promise { + const { runtime, providerId, threshold } = params; + + if (!runtime.config.enabled) { + return QUOTA_DISABLED; + } + + const config = cloneCliConfig(runtime.config); + if (providerId) { + config.enabledProviders = [providerId]; + } + + const allProviders = runtime.providers.filter((p) => { + if (config.enabledProviders === "auto") return true; + return config.enabledProviders.includes(p.id); + }); + + // Read cached quota through the shared export context so the cache key + // matches the one the TUI background writer used. Without this, a user with + // onlyCurrentModel:true would compute a different key and every provider + // would read back as "unavailable". + const ctx = createExportProviderContext(runtime); + const exportData = await buildQuotaExport({ + providers: allProviders, + ctx, + ttlMs: config.minIntervalMs, + fromCache: true, + }); + + const report = (exitCode: number): CliReport => ({ + exitCode, + stdout: line(JSON.stringify(exportData, null, 2)), + stderr: "", + }); + + if (threshold !== undefined) { + const providerResults = Object.values(exportData.providers); + if (providerResults.some((provider) => provider.status !== "ok")) { + return report(2); + } + + const okProviders = providerResults.filter( + (p): p is Extract => p.status === "ok", + ); + + if (okProviders.length === 0) { + // No cached quota to compare against: distinct from "below threshold" (1). + return report(2); + } + + let hasComparablePercent = false; + for (const provider of okProviders) { + const percents = provider.entries + .filter((entry) => entry.renderType === "percent") + .map((entry) => entry.percentRemaining); + if (percents.length === 0) continue; + hasComparablePercent = true; + const minPercent = Math.min(...percents); + if (minPercent < threshold) { + return report(1); + } + } + + if (!hasComparablePercent) { + return report(2); + } + } + + return report(0); +} + +/** + * `status`: the Quota Status report as text, or its payload as JSON. With JSON, exit 2 when + * no provider produced comparable data. + */ +export async function buildCliStatus(params: { + runtime: QuotaRuntimeContext; + providerId?: string; + json: boolean; +}): Promise { + const { runtime, providerId, json } = params; + + if (!runtime.config.enabled) { + return QUOTA_DISABLED; + } + + const data = await buildStatusReportData({ + runtime, + generatedAtMs: Date.now(), + providerFilterId: providerId, + }); + + if (!data.output || !data.payload) { + return QUOTA_DISABLED; + } + + if (json) { + return { + exitCode: data.hasComparableProviderData ? 0 : 2, + stdout: line(JSON.stringify(data.payload, null, 2)), + stderr: "", + }; + } + + return { exitCode: 0, stdout: line(data.output), stderr: "" }; +} diff --git a/src/lib/cli-show.ts b/src/lib/cli-show.ts index e74f9ffd..fe76247c 100644 --- a/src/lib/cli-show.ts +++ b/src/lib/cli-show.ts @@ -1,33 +1,11 @@ -import { resolve } from "path"; -import { hasAnthropicCredentialsConfigured } from "./anthropic.js"; -import { findGitWorktreeRoot } from "./config-file-utils.js"; -import { sanitizeQuotaRenderData } from "./display-sanitize.js"; -import { formatQuotaRows } from "./format.js"; -import { formatQuotaModeHeading } from "./format-utils.js"; -import { - DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, - resolveKimiCnAuthCached, - resolveKimiGlobalAuthCached, -} from "./kimi-auth.js"; -import { - loadConfiguredOpenCodeConfig, - loadConfiguredProviderIds, -} from "./opencode-config-providers.js"; +import { homedir } from "node:os"; +import type { QuotaRpcCliInput, QuotaRpcCliOutput } from "../rpc.js"; +import { discoverOpenCodeService } from "./opencode-service.js"; import { getQuotaProviderShape } from "./provider-metadata.js"; -import { buildQuotaExport, createExportProviderContext } from "./quota-export.js"; -import { resolveQuotaFormatStyle } from "./quota-format-style.js"; -import { collectQuotaRenderData } from "./quota-render-data.js"; -import type { QuotaRuntimeClient } from "./quota-runtime-context.js"; -import { - createQuotaRuntimeRequestContext, - resolveQuotaRuntimeContext, -} from "./quota-runtime-context.js"; -import type { QuotaToastConfig } from "./types.js"; -import { getPackageVersion } from "./version.js"; +import { callQuotaRpc } from "./quota-rpc-client.js"; export interface RunCliShowCommandOptions { argv?: string[]; - cwd?: string; stdout?: Pick; stderr?: Pick; } @@ -40,12 +18,16 @@ const SHOW_USAGE = [ "Usage:", " npx @slkiser/opencode-quota show [--provider ] [--json] [--threshold ]", "", + "Needs OpenCode running: the quota comes from its background service.", + "", "Options:", " --provider Show quota for one provider", " --json Machine-readable JSON output (reads from cache)", " --threshold With --json, exit 1 if any complete cached percentage is below", " % remaining (exit 2 if data is incomplete or not comparable)", " --help, -h Show help", + "", + "Exit code 3: OpenCode is not running or the plugin could not be reached.", ].join("\n"); function parseShowArgs(argv: string[]): ParsedShowArgs { @@ -126,166 +108,57 @@ function parseShowArgs(argv: string[]): ParsedShowArgs { return { ok: true, providerId, help: false, json, threshold }; } -function cloneCliConfig(config: QuotaToastConfig): QuotaToastConfig { - return { - ...config, - enabledProviders: Array.isArray(config.enabledProviders) - ? [...config.enabledProviders] - : config.enabledProviders, - opencodeGoWindows: [...config.opencodeGoWindows], - pricingSnapshot: { ...config.pricingSnapshot }, - layout: { ...config.layout }, - showSessionTokens: false, - }; -} - -export function resolveCliRoots(cwd: string): { - workspaceRoot: string; - configRoot: string; - fallbackDirectory: string; -} { - const fallbackDirectory = resolve(cwd); - const worktreeRoot = findGitWorktreeRoot(fallbackDirectory) ?? fallbackDirectory; - return { - workspaceRoot: worktreeRoot, - configRoot: worktreeRoot, - fallbackDirectory, - }; -} - -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} - -async function loadCliAuthenticatedProviderIds(config: Record): Promise { - const experimental = isRecord(config.experimental) ? config.experimental : undefined; - const quotaToast = isRecord(experimental?.quotaToast) ? experimental.quotaToast : undefined; - const anthropicBinaryPath = - typeof quotaToast?.anthropicBinaryPath === "string" - ? quotaToast.anthropicBinaryPath - : undefined; - const [anthropicConfigured, kimiGlobalAuth, kimiCnAuth] = await Promise.all([ - hasAnthropicCredentialsConfigured({ binaryPath: anthropicBinaryPath }), - resolveKimiGlobalAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }), - resolveKimiCnAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }), - ]); - - return [ - ...(anthropicConfigured ? ["anthropic"] : []), - ...(kimiGlobalAuth.state === "configured" ? ["kimi-code-plan-global"] : []), - ...(kimiCnAuth.state === "configured" ? ["kimi-code-plan-cn"] : []), - ]; -} - -export function createCliQuotaClient(params: { configRootDir: string }): QuotaRuntimeClient { - let configPromise: Promise> | undefined; - let providerIdsPromise: Promise | undefined; - - return { - config: { - get: async () => { - configPromise ??= loadConfiguredOpenCodeConfig({ - configRootDir: params.configRootDir, - }); - return { - data: (await configPromise) as { - experimental?: { quotaToast?: Partial }; - model?: string; - }, - }; - }, - providers: async () => { - providerIdsPromise ??= (async () => { - configPromise ??= loadConfiguredOpenCodeConfig({ - configRootDir: params.configRootDir, - }); - const [configuredIds, authenticatedIds] = await Promise.all([ - loadConfiguredProviderIds({ configRootDir: params.configRootDir }), - configPromise.then(loadCliAuthenticatedProviderIds), - ]); - return [...new Set([...configuredIds, ...authenticatedIds])]; - })(); - const ids = await providerIdsPromise; - return { - data: { - providers: ids.map((id) => ({ id })), - }, - }; - }, - }, - }; -} - function writeLine(stream: Pick, message: string): void { stream.write(message.endsWith("\n") ? message : `${message}\n`); } -async function runCliShowJsonOutput(params: { - runtime: Awaited>; - providerId?: string; - threshold?: number; +function isCliOutput(value: unknown): value is QuotaRpcCliOutput { + if (typeof value !== "object" || value === null) return false; + const output = value as Record; + return ( + Number.isInteger(output.exitCode) && + typeof output.stdout === "string" && + typeof output.stderr === "string" + ); +} + +/** + * Runs a `show` or `status` report inside the running OpenCode service, which computes it + * for the home folder, and prints what it returns. Exit code 3 means OpenCode or the plugin + * could not be reached; stdout then stays empty so JSON readers never parse an error. + */ +export async function runCliReportInOpenCode(params: { + input: QuotaRpcCliInput; stdout: Pick; + stderr: Pick; }): Promise { - const { runtime, providerId, threshold, stdout } = params; - - const config = cloneCliConfig(runtime.config); - if (providerId) { - config.enabledProviders = [providerId]; + const endpoint = await discoverOpenCodeService(); + if (!endpoint) { + writeLine(params.stderr, "OpenCode is not running. Start OpenCode and try again."); + return 3; } - const allProviders = runtime.providers.filter((p) => { - if (config.enabledProviders === "auto") return true; - return config.enabledProviders.includes(p.id); - }); - - // Read cached quota through the shared export context so the cache key - // matches the one the TUI background writer used. Without this, a user with - // onlyCurrentModel:true would compute a different key and every provider - // would read back as "unavailable". - const ctx = createExportProviderContext(runtime); - const exportData = await buildQuotaExport({ - providers: allProviders, - ctx, - ttlMs: config.minIntervalMs, - fromCache: true, - }); - - writeLine(stdout, JSON.stringify(exportData, null, 2)); - - if (threshold !== undefined) { - const providerResults = Object.values(exportData.providers); - if (providerResults.some((provider) => provider.status !== "ok")) { - return 2; - } - - const okProviders = providerResults.filter( - (p): p is Extract => p.status === "ok", + let output: unknown; + try { + output = await callQuotaRpc(endpoint, "cli", params.input, { + directory: homedir(), + timeoutMs: 60_000, + }); + } catch (error) { + writeLine(params.stderr, error instanceof Error ? error.message : String(error)); + return 3; + } + if (!isCliOutput(output)) { + writeLine( + params.stderr, + "OpenCode Quota's server plugin returned an unexpected result. Update the plugin and restart OpenCode.", ); - - if (okProviders.length === 0) { - // No cached quota to compare against: distinct from "below threshold" (1). - return 2; - } - - let hasComparablePercent = false; - for (const provider of okProviders) { - const percents = provider.entries - .filter((entry) => entry.renderType === "percent") - .map((entry) => entry.percentRemaining); - if (percents.length === 0) continue; - hasComparablePercent = true; - const minPercent = Math.min(...percents); - if (minPercent < threshold) { - return 1; - } - } - - if (!hasComparablePercent) { - return 2; - } + return 3; } - return 0; + params.stdout.write(output.stdout); + params.stderr.write(output.stderr); + return output.exitCode; } export async function runCliShowCommand(options: RunCliShowCommandOptions = {}): Promise { @@ -311,80 +184,13 @@ export async function runCliShowCommand(options: RunCliShowCommandOptions = {}): return 1; } - try { - const roots = resolveCliRoots(options.cwd ?? process.cwd()); - const client = createCliQuotaClient({ configRootDir: roots.configRoot }); - const runtime = await resolveQuotaRuntimeContext({ - client, - roots, - includeSessionMeta: false, - }); - - if (!runtime.config.enabled) { - writeLine(stderr, "Quota disabled in config (enabled: false)."); - return 1; - } - - if (parsed.json) { - return runCliShowJsonOutput({ - runtime, - providerId, - threshold: parsed.threshold, - stdout, - }); - } - - const config = cloneCliConfig(runtime.config); - if (providerId) { - config.enabledProviders = [providerId]; - } - - const result = await collectQuotaRenderData({ - client: runtime.client, - resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, - config, - configMeta: runtime.configMeta, - request: createQuotaRuntimeRequestContext(runtime), - surfaceExplicitProviderIssues: true, - formatStyle: resolveQuotaFormatStyle(config.formatStyle), - providers: runtime.providers, - }); - - if (!result.data) { - writeLine(stderr, "No provider data available."); - return 1; - } - - const data = sanitizeQuotaRenderData(result.data); - const version = (await getPackageVersion()) ?? ""; - const output = formatQuotaRows({ - version, - layout: config.layout, - entries: data.entries, - errors: data.errors, - style: resolveQuotaFormatStyle(config.formatStyle), - percentDisplayMode: config.percentDisplayMode, - percentLabelStyle: config.percentLabelStyle, - accountingDetail: config.accountingDetail, - resetTimeDecimals: config.resetTimeDecimals, - resetTimeSpaced: config.resetTimeSpaced, - }); - - if (!output.trim()) { - writeLine(stderr, "No provider data available."); - return 1; - } - - writeLine( - stdout, - config.percentLabelStyle === "bare" - ? `${formatQuotaModeHeading(config.percentDisplayMode)}\n\n${output}` - : output, - ); - return data.entries.length > 0 ? 0 : 1; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - writeLine(stderr, `Failed to show quota: ${message}`); - return 1; - } + return runCliReportInOpenCode({ + input: { + command: parsed.json ? "show-json" : "show", + providerId, + threshold: parsed.threshold, + }, + stdout, + stderr, + }); } diff --git a/src/lib/cli-status.ts b/src/lib/cli-status.ts index 2b61e7f0..45ee65da 100644 --- a/src/lib/cli-status.ts +++ b/src/lib/cli-status.ts @@ -1,13 +1,8 @@ -import { createCliQuotaClient, resolveCliRoots } from "./cli-show.js"; - +import { runCliReportInOpenCode } from "./cli-show.js"; import { getQuotaProviderShape } from "./provider-metadata.js"; -import { buildStatusReportData } from "./quota-dialog-commands.js"; -import type { QuotaRuntimeClient } from "./quota-runtime-context.js"; -import { resolveQuotaRuntimeContext } from "./quota-runtime-context.js"; export interface RunCliStatusCommandOptions { argv?: string[]; - cwd?: string; stdout?: Pick; stderr?: Pick; } @@ -20,8 +15,8 @@ const STATUS_USAGE = [ "Usage:", " npx @slkiser/opencode-quota status [--provider ] [--json]", "", - "Print the same Quota Status diagnostics as the /quota_status slash command,", - "without launching OpenCode.", + "Print the same Quota Status diagnostics as the /quota_status slash command.", + "Needs OpenCode running: the report comes from its background service.", "", "Options:", " --provider Restrict provider availability and live probes to one provider", @@ -33,6 +28,7 @@ const STATUS_USAGE = [ " 0 success", " 1 error or quota disabled (enabled: false)", " 2 no comparable provider data (with --json only)", + " 3 OpenCode is not running or the plugin could not be reached", ].join("\n"); const THRESHOLD_REDIRECT = @@ -122,41 +118,9 @@ export async function runCliStatusCommand( return 1; } - try { - const roots = resolveCliRoots(options.cwd ?? process.cwd()); - const client: QuotaRuntimeClient = createCliQuotaClient({ configRootDir: roots.configRoot }); - const runtime = await resolveQuotaRuntimeContext({ - client, - roots, - includeSessionMeta: false, - }); - - if (!runtime.config.enabled) { - writeLine(stderr, "Quota disabled in config (enabled: false)."); - return 1; - } - - const data = await buildStatusReportData({ - runtime, - generatedAtMs: Date.now(), - providerFilterId: providerId, - }); - - if (!data.output || !data.payload) { - writeLine(stderr, "Quota disabled in config (enabled: false)."); - return 1; - } - - if (parsed.json) { - writeLine(stdout, JSON.stringify(data.payload, null, 2)); - return data.hasComparableProviderData ? 0 : 2; - } - - writeLine(stdout, data.output); - return 0; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - writeLine(stderr, `Failed to generate quota status: ${message}`); - return 1; - } + return runCliReportInOpenCode({ + input: { command: parsed.json ? "status-json" : "status", providerId }, + stdout, + stderr, + }); } diff --git a/src/lib/opencode-service.ts b/src/lib/opencode-service.ts new file mode 100644 index 00000000..31f27a91 --- /dev/null +++ b/src/lib/opencode-service.ts @@ -0,0 +1,70 @@ +/** + * Finds the running OpenCode background service the way OpenCode's own clients do + * (`@opencode/client` `Service.discover`): read its registration file, then check that the + * registered process answers `/api/info`. It never starts OpenCode. + */ +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +export type OpenCodeServiceEndpoint = { url: string; headers: Record }; + +const INFO_TIMEOUT_MS = 2000; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isProcessId(value: unknown): value is number { + return typeof value === "number" && Number.isInteger(value) && value >= 0; +} + +/** + * Returns the service URL and its auth headers, or undefined when no healthy service is + * registered: no or unreadable registration file, no answer within 2 seconds, a non-2xx + * answer, or an answer from a different process or version than the file names. + */ +export async function discoverOpenCodeService(): Promise { + const file = join( + process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), + "opencode", + "service.json", + ); + const text = await readFile(file, "utf8").catch(() => undefined); + if (text === undefined) return undefined; + + let registration: unknown; + try { + registration = JSON.parse(text); + } catch { + return undefined; + } + if (!isRecord(registration)) return undefined; + const { url, pid, password, version } = registration; + if (typeof url !== "string" || !isProcessId(pid)) return undefined; + if (password !== undefined && typeof password !== "string") return undefined; + + const headers: Record = + password === undefined + ? {} + : { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }; + + let info: unknown; + try { + const response = await fetch(new URL("/api/info", url), { + headers, + signal: AbortSignal.timeout(INFO_TIMEOUT_MS), + }); + if (!response.ok) return undefined; + info = await response.json(); + } catch { + return undefined; + } + if (!isRecord(info) || typeof info.version !== "string" || !isProcessId(info.pid)) { + return undefined; + } + if (info.pid !== pid) return undefined; + if (version !== undefined && info.version !== version) return undefined; + + return { url, headers }; +} diff --git a/src/lib/quota-rpc-client.ts b/src/lib/quota-rpc-client.ts new file mode 100644 index 00000000..b3af5a84 --- /dev/null +++ b/src/lib/quota-rpc-client.ts @@ -0,0 +1,87 @@ +/** + * Calls OpenCode Quota's plugin RPC over the OpenCode service's HTTP API with plain `fetch`, + * so the terminal command loads no `@opencode/*` package. + */ +import { QUOTA_RPC_ID, type QuotaRpc } from "../rpc.js"; +import { sanitizeSingleLineDisplayText } from "./display-sanitize.js"; +import type { OpenCodeServiceEndpoint } from "./opencode-service.js"; + +/** OpenCode or the plugin could not be reached, or the call failed before a result came back. */ +export class QuotaRpcTransportError extends Error { + override name = "QuotaRpcTransportError"; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * POSTs `{ input }` to `/api/rpc//` in the given location and returns the + * `output` of the answer. Throws `QuotaRpcTransportError` with a message for the user when + * the call fails. + */ +export async function callQuotaRpc( + endpoint: OpenCodeServiceEndpoint, + method: keyof typeof QuotaRpc.methods, + input: unknown, + options: { directory: string; timeoutMs: number }, +): Promise { + let response: Response; + let text: string; + try { + response = await fetch( + new URL(`/api/rpc/${encodeURIComponent(QUOTA_RPC_ID)}/${method}`, endpoint.url), + { + method: "POST", + headers: { + ...endpoint.headers, + "content-type": "application/json", + "x-opencode-directory": encodeURIComponent(options.directory), + }, + body: JSON.stringify({ input }), + signal: AbortSignal.timeout(options.timeoutMs), + }, + ); + text = await response.text(); + } catch (error) { + if (error instanceof Error && error.name === "TimeoutError") { + throw new QuotaRpcTransportError( + `OpenCode did not answer within ${options.timeoutMs / 1000} s. Try again.`, + ); + } + // Node's fetch reports "fetch failed" and keeps the reason (for example ECONNREFUSED) in `cause`. + const cause = error instanceof Error && error.cause instanceof Error ? error.cause : error; + const message = cause instanceof Error ? cause.message : String(cause); + throw new QuotaRpcTransportError( + `Could not reach OpenCode: ${sanitizeSingleLineDisplayText(message)}`, + ); + } + let body: unknown; + try { + body = JSON.parse(text); + } catch { + body = undefined; + } + + if (response.status === 401) { + throw new QuotaRpcTransportError( + "OpenCode's service password changed. Restart OpenCode and try again.", + ); + } + if (!response.ok) { + if (isRecord(body) && body.type === "rpc.unavailable") { + throw new QuotaRpcTransportError( + "OpenCode Quota's server plugin is not loaded. Check the plugin entry in opencode.json and restart OpenCode.", + ); + } + const message = + isRecord(body) && typeof body.message === "string" + ? sanitizeSingleLineDisplayText(body.message) + : `HTTP ${response.status}`; + throw new QuotaRpcTransportError(`OpenCode could not run OpenCode Quota: ${message}`); + } + if (!isRecord(body) || !("output" in body)) { + throw new QuotaRpcTransportError("OpenCode returned an answer without an output."); + } + return body.output; +} diff --git a/src/plugin.ts b/src/plugin.ts index 4a1bb811..bb1f2c94 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,8 +1,9 @@ /** * OpenCode V2 server plugin: quota slash commands for every client, a diagnostics tool, and - * the quota RPC that computes the TUI surfaces. + * the quota RPC that computes the TUI surfaces and the terminal command's reports. */ import { Plugin } from "@opencode/plugin"; +import { buildCliShowJson, buildCliShowText, buildCliStatus } from "./lib/cli-reports.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { reconcileDetectedProvidersInGlobalConfig } from "./lib/opencode-config-providers.js"; @@ -18,14 +19,17 @@ import { type QuotaReportMetadata, removeQuotaReports, } from "./lib/quota-report-message.js"; -import type { QuotaSessionModelContext } from "./lib/quota-runtime-context.js"; +import { + type QuotaSessionModelContext, + resolveQuotaRuntimeContext, +} from "./lib/quota-runtime-context.js"; import { getQuotaFooter, getQuotaMessage, type QuotaSurfaceHost, writeQuotaExportIfEnabled, } from "./lib/quota-surface-data.js"; -import { QuotaRpc, type QuotaRpcCommandOutput } from "./rpc.js"; +import { QuotaRpc, type QuotaRpcCliOutput, type QuotaRpcCommandOutput } from "./rpc.js"; type ModelMessage = { readonly role: string; @@ -146,6 +150,38 @@ export const QuotaToastPlugin = Plugin.define({ }; } }, + // `opencode-quota show` and `status` in a terminal. The command calls this at the home + // folder and prints the result, so a failure is returned as the command's stderr. + cli: async (input): Promise => { + try { + const runtime = await resolveQuotaRuntimeContext({ + client, + roots, + includeSessionMeta: false, + }); + switch (input.command) { + case "show": + return await buildCliShowText({ runtime, providerId: input.providerId }); + case "show-json": + return await buildCliShowJson({ + runtime, + providerId: input.providerId, + threshold: input.threshold, + }); + case "status": + return await buildCliStatus({ runtime, providerId: input.providerId, json: false }); + case "status-json": + return await buildCliStatus({ runtime, providerId: input.providerId, json: true }); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const failure = + input.command === "status" || input.command === "status-json" + ? "Failed to generate quota status" + : "Failed to show quota"; + return { exitCode: 1, stdout: "", stderr: `${failure}: ${message}\n` }; + } + }, }); await ctx.tool.transform((editor) => { diff --git a/src/rpc.ts b/src/rpc.ts index b75565d8..9e610df6 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -34,6 +34,12 @@ export type QuotaRpcCommandInput = { sessionID?: string; }; export type QuotaRpcCommandOutput = QuotaDialogCommandOutputResult; +export type QuotaRpcCliInput = { + command: "show" | "show-json" | "status" | "status-json"; + providerId?: string; + threshold?: number; +}; +export type QuotaRpcCliOutput = { exitCode: number; stdout: string; stderr: string }; type StandardValueSchema = Extract; type QuotaRpcSchema = StandardValueSchema & { "~standard": { types?: { input: T; output: T } } }; @@ -71,6 +77,7 @@ function checkOptionalString(input: Record, field: string): str const SURFACES = ["sidebar", "idle", "compacted", "question"]; const FOOTER_SURFACES = ["prompt", "home"]; +const CLI_COMMANDS = ["show", "show-json", "status", "status-json"]; export const QuotaRpc = { id: QUOTA_RPC_ID, @@ -113,5 +120,23 @@ export const QuotaRpc = { }), output: schema(checkOutput), }, + cli: { + input: schema((value) => { + if (!isObject(value)) return "input must be an object"; + if (typeof value.command !== "string" || !CLI_COMMANDS.includes(value.command)) { + return `command must be one of ${CLI_COMMANDS.join(", ")}`; + } + if ( + value.threshold !== undefined && + (typeof value.threshold !== "number" || + !Number.isFinite(value.threshold) || + value.threshold <= 0) + ) { + return "threshold must be a positive finite number"; + } + return checkOptionalString(value, "providerId"); + }), + output: schema(checkOutput), + }, }, } satisfies Rpc.PortableDefinition; diff --git a/tests/lib.cli-reports.test.ts b/tests/lib.cli-reports.test.ts new file mode 100644 index 00000000..4809a4bd --- /dev/null +++ b/tests/lib.cli-reports.test.ts @@ -0,0 +1,669 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const TEST_ACCOUNTING = { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", +} as const; + +const { mockProviders, runtimeDirs, statusData } = vi.hoisted(() => ({ + mockProviders: [] as any[], + runtimeDirs: { + value: { + dataDir: "/tmp/opencode-quota-cli-reports-data", + configDir: "/tmp/opencode-quota-cli-reports-config", + cacheDir: "/tmp/opencode-quota-cli-reports-cache", + stateDir: "/tmp/opencode-quota-cli-reports-state", + }, + }, + statusData: { + value: null as null | { + output: string; + payload: Record; + hasComparableProviderData?: boolean; + }, + }, +})); + +vi.mock("../src/providers/registry.js", () => ({ + getProviders: () => mockProviders, +})); + +vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ + getOpencodeRuntimeDirs: () => runtimeDirs.value, +})); + +vi.mock("../src/lib/quota-dialog-commands.js", () => ({ + buildStatusReportData: vi.fn(async () => { + if (!statusData.value) { + return { output: null, payload: null }; + } + return { + output: statusData.value.output, + payload: statusData.value.payload, + hasComparableProviderData: statusData.value.hasComparableProviderData ?? true, + }; + }), +})); + +import { buildCliShowJson, buildCliShowText, buildCliStatus } from "../src/lib/cli-reports.js"; +import { resolveOpenCodeLocationRoots } from "../src/lib/config-file-utils.js"; +import { buildStatusReportData } from "../src/lib/quota-dialog-commands.js"; +import { resolveQuotaRuntimeContext } from "../src/lib/quota-runtime-context.js"; +import { __resetQuotaStateForTests } from "../src/lib/quota-state.js"; + +function basePayload(overrides: Record = {}) { + return { + version: "3.11.2", + generatedAt: "2026-07-16T00:00:00.000Z", + config: { + configSource: "workspace", + configPaths: ["/tmp/opencode.json"], + enabledProviders: ["synthetic"], + onlyCurrentModel: false, + pricingSnapshotSource: "auto", + }, + providers: [ + { id: "synthetic", enabled: true, available: true, matchesCurrentModel: undefined }, + ], + pricing: { + selection: "auto", + activeSource: "bundled", + snapshot: { + source: "bundled", + generatedAt: "2026-01-01T00:00:00.000Z", + units: "USD per 1M tokens", + }, + snapshotPath: "/tmp/pricing.json", + refreshStatePath: "/tmp/pricing-state.json", + }, + liveProbes: [{ id: "synthetic", ok: true }], + ...overrides, + }; +} + +function useStatusData( + payload = basePayload(), + hasComparableProviderData?: boolean, + output = "report text", +): void { + statusData.value = { output, payload, hasComparableProviderData }; +} + +describe("CLI reports", () => { + let tempDir: string; + let globalConfigDir: string; + let workspaceDir: string; + let savedConfigDir: string | undefined; + + // The server plugin's configuration client: OpenCode's provider list, no SDK config. + async function runtimeFor(providerIds: string[] = []) { + return resolveQuotaRuntimeContext({ + client: { + config: { + get: async () => ({ data: {} }), + providers: async () => ({ data: { providers: providerIds.map((id) => ({ id })) } }), + }, + }, + roots: resolveOpenCodeLocationRoots(workspaceDir), + includeSessionMeta: false, + }); + } + + function writeQuotaConfig(quotaToast: Record): void { + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast } }), + "utf8", + ); + } + + beforeEach(() => { + savedConfigDir = process.env.OPENCODE_CONFIG_DIR; + delete process.env.OPENCODE_CONFIG_DIR; + tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-cli-reports-")); + globalConfigDir = join(tempDir, "global-config", "opencode"); + workspaceDir = join(tempDir, "workspace"); + mkdirSync(globalConfigDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + runtimeDirs.value = { + dataDir: "/tmp/opencode-quota-cli-reports-data", + configDir: globalConfigDir, + cacheDir: join(tempDir, "cache"), + stateDir: "/tmp/opencode-quota-cli-reports-state", + }; + mockProviders.length = 0; + statusData.value = null; + vi.mocked(buildStatusReportData).mockClear(); + __resetQuotaStateForTests(); + }); + + afterEach(() => { + vi.useRealTimers(); + if (savedConfigDir !== undefined) process.env.OPENCODE_CONFIG_DIR = savedConfigDir; + else delete process.env.OPENCODE_CONFIG_DIR; + mockProviders.length = 0; + statusData.value = null; + __resetQuotaStateForTests(); + rmSync(tempDir, { recursive: true, force: true }); + }); + + describe("buildCliShowText", () => { + it("renders a compact quota glance and returns zero when quota rows are available", async () => { + const provider = { + id: "synthetic", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [ + { accounting: TEST_ACCOUNTING, name: "Synthetic Weekly", percentRemaining: 75 }, + ], + errors: [], + }), + }; + mockProviders.push(provider); + writeQuotaConfig({ enabledProviders: ["synthetic"], showSessionTokens: true }); + + const report = await buildCliShowText({ runtime: await runtimeFor() }); + + expect(report.exitCode).toBe(0); + expect(report.stdout).toContain("Synthetic Weekly"); + expect(report.stdout).toContain("75%"); + expect(report.stdout.endsWith("\n")).toBe(true); + expect(report.stderr).toBe(""); + expect(provider.fetch).toHaveBeenCalledOnce(); + }); + + it("adds a Quota mode heading for bare CLI labels and spaces reset units by default", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-15T10:00:00.000Z")); + mockProviders.push({ + id: "synthetic", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [ + { + accounting: TEST_ACCOUNTING, + name: "Synthetic Weekly", + percentRemaining: 81, + resetTimeIso: "2026-01-17T15:14:00.000Z", + }, + ], + errors: [], + }), + }); + writeQuotaConfig({ + enabledProviders: ["synthetic"], + percentDisplayMode: "used", + percentLabelStyle: "bare", + }); + + const report = await buildCliShowText({ runtime: await runtimeFor() }); + + expect(report.exitCode).toBe(0); + expect(report.stdout.startsWith("Quota [Used]\n\n")).toBe(true); + expect(report.stdout).toContain("19%"); + expect(report.stdout).not.toContain("19% used"); + expect(report.stdout).toContain("2d 5h 14m"); + expect(report.stderr).toBe(""); + }); + + it("renders default-off runway in human-readable CLI output only when configured", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-09-09T10:00:00.000Z")); + mockProviders.push({ + id: "synthetic", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [ + { + accounting: { + ...TEST_ACCOUNTING, + observedAtIso: "2026-09-09T10:00:00.000Z", + }, + name: "Synthetic Fixed Window", + percentRemaining: 55, + resetTimeIso: "2026-09-09T14:00:00.000Z", + fixedWindow: { + kind: "fixed_window", + startedAtIso: "2026-09-09T09:20:00.000Z", + observedAtIso: "2026-09-09T10:00:00.000Z", + endsAtIso: "2026-09-09T14:00:00.000Z", + fullReset: true, + }, + }, + ], + errors: [], + }), + }); + + const run = async (quotaProjection?: "runway") => { + writeQuotaConfig({ + enabledProviders: ["synthetic"], + ...(quotaProjection ? { quotaProjection } : {}), + }); + __resetQuotaStateForTests(); + const report = await buildCliShowText({ runtime: await runtimeFor() }); + expect(report.exitCode).toBe(0); + expect(report.stderr).toBe(""); + return report.stdout; + }; + + expect(await run()).not.toContain("Runs out"); + expect(await run("runway")).toContain("Runs out ≈ 49m"); + }); + + it("uses the provider as an invocation override", async () => { + const copilotProvider = { + id: "copilot", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 50 }], + errors: [], + }), + }; + const openAiProvider = { + id: "openai", + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ attempted: true, entries: [], errors: [] }), + }; + mockProviders.push(openAiProvider, copilotProvider); + writeQuotaConfig({ enabledProviders: ["openai"] }); + + const report = await buildCliShowText({ runtime: await runtimeFor(), providerId: "copilot" }); + + expect(report.exitCode).toBe(0); + expect(report.stdout).toContain("Copilot"); + expect(copilotProvider.fetch).toHaveBeenCalledOnce(); + expect(openAiProvider.fetch).not.toHaveBeenCalled(); + expect(report.stderr).toBe(""); + }); + + it("returns non-zero when quota is disabled in config", async () => { + writeQuotaConfig({ enabled: false }); + + await expect(buildCliShowText({ runtime: await runtimeFor() })).resolves.toEqual({ + exitCode: 1, + stdout: "", + stderr: "Quota disabled in config (enabled: false).\n", + }); + }); + + it("renders explicit unavailable provider output but returns non-zero", async () => { + const provider = { + id: "copilot", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(false), + fetch: vi.fn(), + }; + mockProviders.push(provider); + + const report = await buildCliShowText({ runtime: await runtimeFor(), providerId: "copilot" }); + + expect(report.exitCode).toBe(1); + expect(report.stdout).toContain("Copilot: Unavailable (not detected)"); + expect(report.stderr).toBe(""); + expect(provider.fetch).not.toHaveBeenCalled(); + }); + + it("renders Copilot and Gemini CLI success rows", async () => { + const copilotProvider = { + id: "copilot", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [ + { + accounting: TEST_ACCOUNTING, + name: "Copilot", + group: "Copilot (personal)", + label: "Quota:", + right: "0/300", + percentRemaining: 100, + }, + ], + errors: [], + }), + }; + const geminiCliProvider = { + id: "google-gemini-cli", + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [ + { + accounting: TEST_ACCOUNTING, + name: "Gemini Pro", + group: "Gemini CLI", + label: "Gemini Pro:", + right: "840 left", + percentRemaining: 84, + }, + ], + errors: [], + }), + }; + mockProviders.push(copilotProvider, geminiCliProvider); + writeQuotaConfig({ + enabledProviders: ["copilot", "google-gemini-cli"], + formatStyle: "allWindows", + }); + + const report = await buildCliShowText({ runtime: await runtimeFor() }); + + expect(report.exitCode).toBe(0); + expect(report.stdout).toContain("Copilot"); + expect(report.stdout).toContain("Gemini CLI"); + expect(copilotProvider.fetch).toHaveBeenCalledOnce(); + expect(geminiCliProvider.fetch).toHaveBeenCalledOnce(); + expect(report.stderr).toBe(""); + }); + + it("uses OpenCode's provider ids for provider availability", async () => { + const provider = { + id: "copilot", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn(async (ctx: any) => { + const response = await ctx.client.config.providers(); + return response.data.providers.some( + (item: { id: string }) => item.id === "github-copilot", + ); + }), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 88 }], + errors: [], + }), + }; + mockProviders.push(provider); + + const report = await buildCliShowText({ runtime: await runtimeFor(["github-copilot"]) }); + + expect(report.exitCode).toBe(0); + expect(report.stdout).toContain("Copilot"); + expect(provider.fetch).toHaveBeenCalledOnce(); + expect(report.stderr).toBe(""); + }); + }); + + describe("buildCliShowJson", () => { + function syntheticProvider(entries: unknown[], errors: unknown[] = []) { + return { + id: "synthetic", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ attempted: true, entries, errors }), + }; + } + + it("outputs valid JSON with cached provider data", async () => { + const provider = syntheticProvider([ + { accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 75 }, + ]); + mockProviders.push(provider); + writeQuotaConfig({ enabledProviders: ["synthetic"] }); + + // The text report populates the cache. + expect((await buildCliShowText({ runtime: await runtimeFor() })).exitCode).toBe(0); + expect(provider.fetch).toHaveBeenCalledOnce(); + + const report = await buildCliShowJson({ runtime: await runtimeFor() }); + + expect(report.exitCode).toBe(0); + expect(report.stderr).toBe(""); + expect(report.stdout.endsWith("\n")).toBe(true); + const parsed = JSON.parse(report.stdout); + expect(parsed).toHaveProperty("version", 2); + expect(parsed).toHaveProperty("exportedAt"); + expect(parsed).toHaveProperty("fromCache", true); + expect(parsed).toHaveProperty("cacheAgeSeconds"); + expect(parsed.providers).toHaveProperty("synthetic"); + expect(parsed.providers.synthetic.status).toBe("ok"); + expect(parsed.providers.synthetic.entries[0].name).toBe("Synthetic"); + expect(parsed.providers.synthetic.entries[0].percentRemaining).toBe(75); + expect(parsed.providers.synthetic.entries[0].renderType).toBe("percent"); + expect(parsed.providers.synthetic.entries[0]).not.toHaveProperty("unlimited"); + expect(provider.fetch).toHaveBeenCalledTimes(1); // still only called from the text report + }); + + it("reads from cache only and returns unavailable when no cache exists", async () => { + const provider = syntheticProvider([ + { accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 100 }, + ]); + mockProviders.push(provider); + writeQuotaConfig({ enabledProviders: ["synthetic"] }); + + const report = await buildCliShowJson({ runtime: await runtimeFor() }); + + expect(report.exitCode).toBe(0); + expect(report.stderr).toBe(""); + expect(JSON.parse(report.stdout).providers.synthetic.status).toBe("unavailable"); + expect(provider.fetch).not.toHaveBeenCalled(); + }); + + it("returns non-zero when quota is disabled in config", async () => { + writeQuotaConfig({ enabled: false }); + + await expect(buildCliShowJson({ runtime: await runtimeFor() })).resolves.toEqual({ + exitCode: 1, + stdout: "", + stderr: "Quota disabled in config (enabled: false).\n", + }); + }); + + it.each([ + [80, 50, 0], + [30, 50, 1], + ])("--threshold uses percentage rows even when the first row is a quantity: %s vs %s", async (percentRemaining, threshold, expectedCode) => { + mockProviders.push( + syntheticProvider([ + { + kind: "quantity", + accounting: { ...TEST_ACCOUNTING, resultType: "balance" }, + semantic: { + metric: { kind: "component", component: "current_balance" }, + prominence: "primary", + }, + name: "Synthetic balance", + quantity: { decimal: "5", unit: { kind: "currency", code: "USD" } }, + }, + { accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining }, + ]), + ); + writeQuotaConfig({ enabledProviders: ["synthetic"] }); + + await buildCliShowText({ runtime: await runtimeFor() }); + const report = await buildCliShowJson({ runtime: await runtimeFor(), threshold }); + + expect(report.exitCode).toBe(expectedCode); + }); + + it("--threshold exits 2 when no provider is ok", async () => { + mockProviders.push( + syntheticProvider([ + { accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 100 }, + ]), + ); + writeQuotaConfig({ enabledProviders: ["synthetic"] }); + + const report = await buildCliShowJson({ runtime: await runtimeFor(), threshold: 10 }); + + expect(report.exitCode).toBe(2); + }); + + it("--threshold exits 2 when cached ok providers have no percentRemaining values", async () => { + mockProviders.push( + syntheticProvider([ + { + accounting: TEST_ACCOUNTING, + name: "Synthetic", + kind: "value", + value: "$42", + label: "Usage:", + }, + ]), + ); + writeQuotaConfig({ enabledProviders: ["synthetic"] }); + + await buildCliShowText({ runtime: await runtimeFor() }); + const report = await buildCliShowJson({ runtime: await runtimeFor(), threshold: 10 }); + + expect(report.exitCode).toBe(2); + }); + + it("--threshold exits 2 for partial cached results instead of passing incomplete data", async () => { + mockProviders.push( + syntheticProvider( + [{ accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 80 }], + [{ label: "Synthetic secondary", message: "quota endpoint unavailable" }], + ), + ); + writeQuotaConfig({ enabledProviders: ["synthetic"] }); + + await buildCliShowText({ runtime: await runtimeFor() }); + const report = await buildCliShowJson({ runtime: await runtimeFor(), threshold: 50 }); + + expect(report.exitCode).toBe(2); + expect(JSON.parse(report.stdout).providers.synthetic).toMatchObject({ + status: "partial", + entries: [expect.objectContaining({ percentRemaining: 80 })], + errors: [{ label: "Synthetic secondary", message: "quota endpoint unavailable" }], + }); + }); + + it("--provider copilot only includes the copilot key", async () => { + const copilotProvider = { + id: "copilot", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 90 }], + errors: [], + }), + }; + mockProviders.push( + copilotProvider, + syntheticProvider([ + { accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 50 }, + ]), + ); + writeQuotaConfig({ enabledProviders: ["copilot", "synthetic"] }); + + await buildCliShowText({ runtime: await runtimeFor() }); + const report = await buildCliShowJson({ runtime: await runtimeFor(), providerId: "copilot" }); + + expect(report.exitCode).toBe(0); + const parsed = JSON.parse(report.stdout); + expect(Object.keys(parsed.providers)).toEqual(["copilot"]); + expect(parsed.providers.copilot.status).toBe("ok"); + }); + }); + + describe("buildCliStatus", () => { + it("prints a plain-text Quota Status report and returns zero", async () => { + useStatusData( + basePayload(), + undefined, + "Quota Status (opencode-quota v3.11.2)\ntoast:\n- enabledProviders: synthetic", + ); + + const report = await buildCliStatus({ runtime: await runtimeFor(), json: false }); + + expect(report).toEqual({ + exitCode: 0, + stdout: "Quota Status (opencode-quota v3.11.2)\ntoast:\n- enabledProviders: synthetic\n", + stderr: "", + }); + expect(buildStatusReportData).toHaveBeenCalledWith( + expect.objectContaining({ providerFilterId: undefined }), + ); + }); + + it("emits a structured JSON payload and returns zero when live probes exist", async () => { + useStatusData(); + + const report = await buildCliStatus({ runtime: await runtimeFor(), json: true }); + + expect(report.exitCode).toBe(0); + expect(report.stderr).toBe(""); + const parsed = JSON.parse(report.stdout); + expect(parsed).toHaveProperty("version", "3.11.2"); + expect(parsed).toHaveProperty("generatedAt"); + expect(parsed).toHaveProperty("config"); + expect(parsed).toHaveProperty("providers"); + expect(parsed).toHaveProperty("pricing"); + expect(parsed).toHaveProperty("liveProbes"); + expect(parsed.liveProbes).toEqual([{ id: "synthetic", ok: true }]); + }); + + it("exits 2 with JSON when there is no comparable provider data", async () => { + useStatusData(basePayload({ liveProbes: [] }), false); + + const report = await buildCliStatus({ runtime: await runtimeFor(), json: true }); + + expect(report.exitCode).toBe(2); + expect(report.stderr).toBe(""); + // JSON still prints on exit 2. + expect(JSON.parse(report.stdout).liveProbes).toEqual([]); + }); + + it("exits 2 with JSON when probes fail without producing quota entries", async () => { + useStatusData(basePayload({ liveProbes: [{ id: "synthetic", ok: false }] }), false); + + const report = await buildCliStatus({ runtime: await runtimeFor(), json: true }); + + expect(report.exitCode).toBe(2); + expect(JSON.parse(report.stdout).liveProbes).toEqual([{ id: "synthetic", ok: false }]); + }); + + it("succeeds with JSON when a partial probe produced quota entries", async () => { + useStatusData(basePayload({ liveProbes: [{ id: "synthetic", ok: false }] }), true); + + const report = await buildCliStatus({ runtime: await runtimeFor(), json: true }); + + expect(report.exitCode).toBe(0); + }); + + it("forwards the provider filter and still emits JSON", async () => { + useStatusData(); + + const report = await buildCliStatus({ + runtime: await runtimeFor(), + providerId: "synthetic", + json: true, + }); + + expect(report.exitCode).toBe(0); + expect(buildStatusReportData).toHaveBeenCalledWith( + expect.objectContaining({ providerFilterId: "synthetic" }), + ); + const parsed = JSON.parse(report.stdout); + expect(parsed.providers).toHaveLength(1); + expect(parsed).not.toHaveProperty("providerFilterId"); + }); + + it("returns non-zero when quota is disabled in config", async () => { + writeQuotaConfig({ enabled: false }); + + await expect(buildCliStatus({ runtime: await runtimeFor(), json: false })).resolves.toEqual({ + exitCode: 1, + stdout: "", + stderr: "Quota disabled in config (enabled: false).\n", + }); + expect(buildStatusReportData).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/tests/lib.cli-show.test.ts b/tests/lib.cli-show.test.ts index b481642d..4e2d1185 100644 --- a/tests/lib.cli-show.test.ts +++ b/tests/lib.cli-show.test.ts @@ -1,52 +1,20 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const TEST_ACCOUNTING = { - resultType: "quota", - acquisitionMethod: "remote_api", - ownership: "maintained", - authority: "provider_reported", -} as const; - -const { authMocks, mockProviders, runtimeDirs } = vi.hoisted(() => ({ - authMocks: { - anthropicConfigured: false, - kimiGlobalState: "none" as "none" | "configured", - kimiCnState: "none" as "none" | "configured", - }, - mockProviders: [] as any[], - runtimeDirs: { - value: { - dataDir: "/tmp/opencode-quota-cli-show-data", - configDir: "/tmp/opencode-quota-cli-show-config", - cacheDir: "/tmp/opencode-quota-cli-show-cache", - stateDir: "/tmp/opencode-quota-cli-show-state", - }, - }, -})); - -vi.mock("../src/lib/anthropic.js", () => ({ - hasAnthropicCredentialsConfigured: vi.fn(async () => authMocks.anthropicConfigured), -})); +import { homedir } from "node:os"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("../src/lib/kimi-auth.js", () => ({ - DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS: 30_000, - resolveKimiGlobalAuthCached: vi.fn(async () => ({ state: authMocks.kimiGlobalState })), - resolveKimiCnAuthCached: vi.fn(async () => ({ state: authMocks.kimiCnState })), +vi.mock("../src/lib/opencode-service.js", () => ({ + discoverOpenCodeService: vi.fn(), })); -vi.mock("../src/providers/registry.js", () => ({ - getProviders: () => mockProviders, +vi.mock("../src/lib/quota-rpc-client.js", async (importOriginal) => ({ + ...(await importOriginal()), + callQuotaRpc: vi.fn(), })); -vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirs: () => runtimeDirs.value, -})); +import { runCliShowCommand } from "../src/lib/cli-show.js"; +import { discoverOpenCodeService } from "../src/lib/opencode-service.js"; +import { callQuotaRpc, QuotaRpcTransportError } from "../src/lib/quota-rpc-client.js"; -import { createCliQuotaClient, runCliShowCommand } from "../src/lib/cli-show.js"; -import { __resetQuotaStateForTests } from "../src/lib/quota-state.js"; +const ENDPOINT = { url: "http://127.0.0.1:4096", headers: { authorization: "Basic abc" } }; function createCaptureStream() { let output = ""; @@ -63,892 +31,179 @@ function createCaptureStream() { }; } -describe("runCliShowCommand", () => { - let tempDir: string; - let globalConfigDir: string; - let workspaceDir: string; - let savedConfigDir: string | undefined; - - beforeEach(() => { - authMocks.anthropicConfigured = false; - authMocks.kimiGlobalState = "none"; - authMocks.kimiCnState = "none"; - savedConfigDir = process.env.OPENCODE_CONFIG_DIR; - delete process.env.OPENCODE_CONFIG_DIR; - tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-cli-show-")); - globalConfigDir = join(tempDir, "global-config", "opencode"); - workspaceDir = join(tempDir, "workspace"); - mkdirSync(globalConfigDir, { recursive: true }); - mkdirSync(workspaceDir, { recursive: true }); - runtimeDirs.value = { - dataDir: "/tmp/opencode-quota-cli-show-data", - configDir: globalConfigDir, - cacheDir: join(tempDir, "cache"), - stateDir: "/tmp/opencode-quota-cli-show-state", - }; - mockProviders.length = 0; - __resetQuotaStateForTests(); - }); - - afterEach(() => { - vi.useRealTimers(); - if (savedConfigDir !== undefined) process.env.OPENCODE_CONFIG_DIR = savedConfigDir; - else delete process.env.OPENCODE_CONFIG_DIR; - mockProviders.length = 0; - __resetQuotaStateForTests(); - rmSync(tempDir, { recursive: true, force: true }); - }); - - it("synthesizes offline Kimi runtime ids independently by region", async () => { - writeFileSync(join(workspaceDir, "opencode.json"), "{}", "utf8"); - - const unauthenticated = await createCliQuotaClient({ - configRootDir: workspaceDir, - }).config.providers(); - expect(unauthenticated.data?.providers).toEqual([]); - - authMocks.anthropicConfigured = true; - authMocks.kimiCnState = "configured"; - const cnOnly = await createCliQuotaClient({ - configRootDir: workspaceDir, - }).config.providers(); - expect(cnOnly.data?.providers).toEqual([{ id: "anthropic" }, { id: "kimi-code-plan-cn" }]); - - authMocks.kimiGlobalState = "configured"; - const bothRegions = await createCliQuotaClient({ - configRootDir: workspaceDir, - }).config.providers(); - expect(bothRegions.data?.providers).toEqual([ - { id: "anthropic" }, - { id: "kimi-code-plan-global" }, - { id: "kimi-code-plan-cn" }, - ]); +async function runShow(argv: string[]) { + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + const code = await runCliShowCommand({ + argv, + stdout: stdout.stream as any, + stderr: stderr.stream as any, }); + return { code, stdout: stdout.output, stderr: stderr.output }; +} - it("renders a compact quota glance and returns zero when quota rows are available", async () => { - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Synthetic Weekly", percentRemaining: 75 }], - errors: [], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabledProviders: ["synthetic"], - showSessionTokens: true, - }, - }, - }), - "utf8", - ); - - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - - expect(code).toBe(0); - expect(stdout.output).toContain("Synthetic Weekly"); - expect(stdout.output).toContain("75%"); - expect(stderr.output).toBe(""); - expect(provider.fetch).toHaveBeenCalledOnce(); - }); - - it("adds a Quota mode heading for bare CLI labels and spaces reset units by default", async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-01-15T10:00:00.000Z")); - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "Synthetic Weekly", - percentRemaining: 81, - resetTimeIso: "2026-01-17T15:14:00.000Z", - }, - ], - errors: [], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabledProviders: ["synthetic"], - percentDisplayMode: "used", - percentLabelStyle: "bare", - }, - }, - }), - "utf8", - ); - - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const code = await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - - expect(code).toBe(0); - expect(stdout.output.startsWith("Quota [Used]\n\n")).toBe(true); - expect(stdout.output).toContain("19%"); - expect(stdout.output).not.toContain("19% used"); - expect(stdout.output).toContain("2d 5h 14m"); - expect(stderr.output).toBe(""); +describe("runCliShowCommand", () => { + beforeEach(() => { + vi.mocked(discoverOpenCodeService).mockReset().mockResolvedValue(ENDPOINT); + vi.mocked(callQuotaRpc) + .mockReset() + .mockResolvedValue({ exitCode: 0, stdout: "Synthetic Weekly 75%\n", stderr: "" }); }); - it("renders default-off runway in human-readable CLI output only when configured", async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-09-09T10:00:00.000Z")); - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: { - ...TEST_ACCOUNTING, - observedAtIso: "2026-09-09T10:00:00.000Z", - }, - name: "Synthetic Fixed Window", - percentRemaining: 55, - resetTimeIso: "2026-09-09T14:00:00.000Z", - fixedWindow: { - kind: "fixed_window", - startedAtIso: "2026-09-09T09:20:00.000Z", - observedAtIso: "2026-09-09T10:00:00.000Z", - endsAtIso: "2026-09-09T14:00:00.000Z", - fullReset: true, - }, - }, - ], - errors: [], - }), - }; - mockProviders.push(provider); - - const run = async (quotaProjection?: "runway") => { - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabledProviders: ["synthetic"], - ...(quotaProjection ? { quotaProjection } : {}), - }, - }, - }), - "utf8", - ); - __resetQuotaStateForTests(); - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const code = await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - expect(code).toBe(0); - expect(stderr.output).toBe(""); - return stdout.output; - }; - - expect(await run()).not.toContain("Runs out"); - expect(await run("runway")).toContain("Runs out ≈ 49m"); - }); + it("asks the running OpenCode service for the report at the home folder and prints it", async () => { + const result = await runShow([]); - it("normalizes --provider aliases and uses the provider as an invocation override", async () => { - const copilotProvider = { - id: "copilot", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 50 }], - errors: [], - }), - }; - const openAiProvider = { - id: "openai", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ attempted: true, entries: [], errors: [] }), - }; - mockProviders.push(openAiProvider, copilotProvider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ experimental: { quotaToast: { enabledProviders: ["openai"] } } }), - "utf8", + expect(result).toEqual({ code: 0, stdout: "Synthetic Weekly 75%\n", stderr: "" }); + expect(callQuotaRpc).toHaveBeenCalledWith( + ENDPOINT, + "cli", + { command: "show", providerId: undefined, threshold: undefined }, + { directory: homedir(), timeoutMs: 60_000 }, ); - - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: ["--provider=github-copilot"], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - - expect(code).toBe(0); - expect(stdout.output).toContain("Copilot"); - expect(copilotProvider.fetch).toHaveBeenCalledOnce(); - expect(openAiProvider.fetch).not.toHaveBeenCalled(); - expect(stderr.output).toBe(""); }); - it("rejects an unknown provider before probing providers", async () => { - const provider = { - id: "copilot", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn(), - }; - mockProviders.push(provider); - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: ["--provider", "not-a-provider"], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, + it("forwards --json and --threshold and returns the report's exit code", async () => { + vi.mocked(callQuotaRpc).mockResolvedValue({ + exitCode: 2, + stdout: '{\n "version": 2\n}\n', + stderr: "", }); - expect(code).toBe(1); - expect(stdout.output).toBe(""); - expect(stderr.output).toContain("Unknown provider: not-a-provider"); - expect(provider.isAvailable).not.toHaveBeenCalled(); - expect(provider.fetch).not.toHaveBeenCalled(); - }); - - it("rejects missing provider values", async () => { - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: ["--provider"], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); + const result = await runShow(["--json", "--threshold", "5"]); - expect(code).toBe(1); - expect(stdout.output).toBe(""); - expect(stderr.output).toContain("Missing value for --provider"); - expect(stderr.output).toContain("opencode-quota show"); - }); - - it("returns non-zero when quota is disabled in config", async () => { - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), - "utf8", + expect(result).toEqual({ code: 2, stdout: '{\n "version": 2\n}\n', stderr: "" }); + expect(callQuotaRpc).toHaveBeenCalledWith( + ENDPOINT, + "cli", + { command: "show-json", providerId: undefined, threshold: 5 }, + expect.anything(), ); - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - - expect(code).toBe(1); - expect(stdout.output).toBe(""); - expect(stderr.output).toContain("Quota disabled in config"); }); - it("renders explicit unavailable provider output but returns non-zero", async () => { - const provider = { - id: "copilot", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(false), - fetch: vi.fn(), - }; - mockProviders.push(provider); - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: ["--provider", "copilot"], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, + it("prints the report's stderr and exit code when the server reports an error", async () => { + vi.mocked(callQuotaRpc).mockResolvedValue({ + exitCode: 1, + stdout: "", + stderr: "Quota disabled in config (enabled: false).\n", }); - expect(code).toBe(1); - expect(stdout.output).toContain("Copilot: Unavailable (not detected)"); - expect(stderr.output).toBe(""); - expect(provider.fetch).not.toHaveBeenCalled(); - }); + const result = await runShow([]); - it("prefers the git worktree root over a nested cwd for config loading", async () => { - const nestedDir = join(workspaceDir, "packages", "app"); - mkdirSync(nestedDir, { recursive: true }); - mkdirSync(join(workspaceDir, ".git")); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), - "utf8", - ); - writeFileSync( - join(nestedDir, "opencode.json"), - JSON.stringify({ experimental: { quotaToast: { enabled: true } } }), - "utf8", - ); - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: [], - cwd: nestedDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - - expect(code).toBe(1); - expect(stdout.output).toBe(""); - expect(stderr.output).toContain("Quota disabled in config"); - }); - - it("reads worktree root config even when OPENCODE_CONFIG_DIR is set", async () => { - const nestedDir = join(workspaceDir, "packages", "app"); - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn(), - }; - mockProviders.push(provider); - mkdirSync(nestedDir, { recursive: true }); - mkdirSync(join(workspaceDir, ".git")); - process.env.OPENCODE_CONFIG_DIR = ".opencode"; - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), - "utf8", - ); - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: [], - cwd: nestedDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, + expect(result).toEqual({ + code: 1, + stdout: "", + stderr: "Quota disabled in config (enabled: false).\n", }); - - expect(code).toBe(1); - expect(stdout.output).toBe(""); - expect(stderr.output).toContain("Quota disabled in config"); - expect(provider.fetch).not.toHaveBeenCalled(); }); - it("renders Copilot and Gemini CLI success rows in standalone show", async () => { - const copilotProvider = { - id: "copilot", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "Copilot", - group: "Copilot (personal)", - label: "Quota:", - right: "0/300", - percentRemaining: 100, - }, - ], - errors: [], - }), - }; - const geminiCliProvider = { - id: "google-gemini-cli", - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "Gemini Pro", - group: "Gemini CLI", - label: "Gemini Pro:", - right: "840 left", - percentRemaining: 84, - }, - ], - errors: [], - }), - }; - mockProviders.push(copilotProvider, geminiCliProvider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { - quotaToast: { - enabledProviders: ["copilot", "google-gemini-cli"], - formatStyle: "allWindows", - }, - }, - }), - "utf8", - ); - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - - expect(code).toBe(0); - expect(stdout.output).toContain("Copilot"); - expect(stdout.output).toContain("Gemini CLI"); - expect(copilotProvider.fetch).toHaveBeenCalledOnce(); - expect(geminiCliProvider.fetch).toHaveBeenCalledOnce(); - expect(stderr.output).toBe(""); - }); + it("normalizes --provider aliases before calling OpenCode", async () => { + const result = await runShow(["--provider=github-copilot"]); - it("uses root-level OpenCode provider ids for standalone provider availability", async () => { - const provider = { - id: "copilot", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn(async (ctx: any) => { - const response = await ctx.client.config.providers(); - return response.data.providers.some((item: { id: string }) => item.id === "github-copilot"); - }), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 88 }], - errors: [], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ provider: { "github-copilot": {} } }), - "utf8", + expect(result.code).toBe(0); + expect(callQuotaRpc).toHaveBeenCalledWith( + ENDPOINT, + "cli", + { command: "show", providerId: "copilot", threshold: undefined }, + expect.anything(), ); - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - const code = await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - - expect(code).toBe(0); - expect(stdout.output).toContain("Copilot"); - expect(provider.fetch).toHaveBeenCalledOnce(); - expect(stderr.output).toBe(""); }); - // ────────────────────────────────────────────── - // --json / --threshold tests - // ────────────────────────────────────────────── - - it("--json outputs valid JSON to stdout with cached provider data", async () => { - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 75 }], - errors: [], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, - }), - "utf8", - ); - - // Run non-JSON show first to populate the cache. - const textOut = createCaptureStream(); - const textErr = createCaptureStream(); - const textCode = await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: textOut.stream as any, - stderr: textErr.stream as any, - }); - expect(textCode).toBe(0); - expect(provider.fetch).toHaveBeenCalledOnce(); - - // Now run --json (reads from cache). - const jsonOut = createCaptureStream(); - const jsonErr = createCaptureStream(); - const jsonCode = await runCliShowCommand({ - argv: ["--json"], - cwd: workspaceDir, - stdout: jsonOut.stream as any, - stderr: jsonErr.stream as any, - }); - - expect(jsonCode).toBe(0); - expect(jsonErr.output).toBe(""); - - const parsed = JSON.parse(jsonOut.output); - expect(parsed).toHaveProperty("version", 2); - expect(parsed).toHaveProperty("exportedAt"); - expect(parsed).toHaveProperty("fromCache", true); - expect(parsed).toHaveProperty("cacheAgeSeconds"); - expect(parsed.providers).toHaveProperty("synthetic"); - expect(parsed.providers.synthetic.status).toBe("ok"); - expect(parsed.providers.synthetic.entries[0].name).toBe("Synthetic"); - expect(parsed.providers.synthetic.entries[0].percentRemaining).toBe(75); - expect(parsed.providers.synthetic.entries[0].renderType).toBe("percent"); - expect(parsed.providers.synthetic.entries[0]).not.toHaveProperty("unlimited"); - expect(provider.fetch).toHaveBeenCalledTimes(1); // still only called from text path - }); + it("exits 3 with an empty stdout when OpenCode is not running", async () => { + vi.mocked(discoverOpenCodeService).mockResolvedValue(undefined); - it("--json reads from cache only and returns unavailable when no cache exists", async () => { - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 100 }], - errors: [], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, - }), - "utf8", - ); + const result = await runShow(["--json"]); - // Run --json WITHOUT populating cache first → all unavailable, no fetch. - const jsonOut = createCaptureStream(); - const jsonErr = createCaptureStream(); - const jsonCode = await runCliShowCommand({ - argv: ["--json"], - cwd: workspaceDir, - stdout: jsonOut.stream as any, - stderr: jsonErr.stream as any, + expect(result).toEqual({ + code: 3, + stdout: "", + stderr: "OpenCode is not running. Start OpenCode and try again.\n", }); - - expect(jsonCode).toBe(0); - expect(jsonErr.output).toBe(""); - const parsed = JSON.parse(jsonOut.output); - expect(parsed.providers.synthetic.status).toBe("unavailable"); - expect(provider.fetch).not.toHaveBeenCalled(); + expect(callQuotaRpc).not.toHaveBeenCalled(); }); - it.each([ - [80, "50", 0], - [30, "50", 1], - ])("--threshold uses percentage rows even when the first row is a quantity: %s vs %s", async (percentRemaining, threshold, expectedCode) => { - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - kind: "quantity", - accounting: { ...TEST_ACCOUNTING, resultType: "balance" }, - semantic: { - metric: { kind: "component", component: "current_balance" }, - prominence: "primary", - }, - name: "Synthetic balance", - quantity: { decimal: "5", unit: { kind: "currency", code: "USD" } }, - }, - { accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining }, - ], - errors: [], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, - }), - "utf8", + it("exits 3 with an empty stdout when the call to OpenCode fails", async () => { + vi.mocked(callQuotaRpc).mockRejectedValue( + new QuotaRpcTransportError( + "OpenCode Quota's server plugin is not loaded. Check the plugin entry in opencode.json and restart OpenCode.", + ), ); - // Populate cache. - await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: { write: () => true } as any, - stderr: { write: () => true } as any, - }); + const result = await runShow(["--json"]); - const jsonCode = await runCliShowCommand({ - argv: ["--json", "--threshold", threshold], - cwd: workspaceDir, - stdout: { write: () => true } as any, - stderr: { write: () => true } as any, + expect(result).toEqual({ + code: 3, + stdout: "", + stderr: + "OpenCode Quota's server plugin is not loaded. Check the plugin entry in opencode.json and restart OpenCode.\n", }); - - expect(jsonCode).toBe(expectedCode); }); - it("--threshold exits 2 when no provider is ok", async () => { - // Provider that is unavailable (no cache populated). - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 100 }], - errors: [], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, - }), - "utf8", - ); + it("exits 3 when OpenCode returns an output that is not a report", async () => { + vi.mocked(callQuotaRpc).mockResolvedValue({ exitCode: 0, stdout: 5 }); - // Run --threshold WITHOUT populating cache → all unavailable. - const jsonOut = createCaptureStream(); - const jsonCode = await runCliShowCommand({ - argv: ["--json", "--threshold", "10"], - cwd: workspaceDir, - stdout: jsonOut.stream as any, - stderr: { write: () => true } as any, - }); + const result = await runShow([]); - expect(jsonCode).toBe(2); + expect(result.code).toBe(3); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("returned an unexpected result"); }); - it("--threshold exits 2 when cached ok providers have no percentRemaining values", async () => { - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [ - { - accounting: TEST_ACCOUNTING, - name: "Synthetic", - kind: "value", - value: "$42", - label: "Usage:", - }, - ], - errors: [], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, - }), - "utf8", - ); - - await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: { write: () => true } as any, - stderr: { write: () => true } as any, - }); - - const jsonCode = await runCliShowCommand({ - argv: ["--json", "--threshold", "10"], - cwd: workspaceDir, - stdout: { write: () => true } as any, - stderr: { write: () => true } as any, - }); + it("rejects an unknown provider before contacting OpenCode", async () => { + const result = await runShow(["--provider", "not-a-provider"]); - expect(jsonCode).toBe(2); - }); - - it("--threshold exits 2 for partial cached results instead of passing incomplete data", async () => { - const provider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 80 }], - errors: [{ label: "Synthetic secondary", message: "quota endpoint unavailable" }], - }), - }; - mockProviders.push(provider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, - }), - "utf8", - ); - - await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: { write: () => true } as any, - stderr: { write: () => true } as any, - }); - - const stdout = createCaptureStream(); - const code = await runCliShowCommand({ - argv: ["--json", "--threshold", "50"], - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: { write: () => true } as any, - }); - - expect(code).toBe(2); - expect(JSON.parse(stdout.output).providers.synthetic).toMatchObject({ - status: "partial", - entries: [expect.objectContaining({ percentRemaining: 80 })], - errors: [{ label: "Synthetic secondary", message: "quota endpoint unavailable" }], - }); + expect(result.code).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("Unknown provider: not-a-provider"); + expect(discoverOpenCodeService).not.toHaveBeenCalled(); + expect(callQuotaRpc).not.toHaveBeenCalled(); }); - it("--json --provider copilot only includes the copilot key", async () => { - const copilotProvider = { - id: "copilot", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 90 }], - errors: [], - }), - }; - const syntheticProvider = { - id: "synthetic", - cachePolicy: { kind: "account-neutral" as const }, - isAvailable: vi.fn().mockResolvedValue(true), - fetch: vi.fn().mockResolvedValue({ - attempted: true, - entries: [{ accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 50 }], - errors: [], - }), - }; - mockProviders.push(copilotProvider, syntheticProvider); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { quotaToast: { enabledProviders: ["copilot", "synthetic"] } }, - }), - "utf8", - ); - - // Populate cache for both providers. - await runCliShowCommand({ - argv: [], - cwd: workspaceDir, - stdout: { write: () => true } as any, - stderr: { write: () => true } as any, - }); - - const jsonOut = createCaptureStream(); - const jsonCode = await runCliShowCommand({ - argv: ["--json", "--provider", "copilot"], - cwd: workspaceDir, - stdout: jsonOut.stream as any, - stderr: { write: () => true } as any, - }); + it("rejects missing provider values", async () => { + const result = await runShow(["--provider"]); - expect(jsonCode).toBe(0); - const parsed = JSON.parse(jsonOut.output); - expect(Object.keys(parsed.providers)).toEqual(["copilot"]); - expect(parsed.providers.copilot.status).toBe("ok"); + expect(result.code).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("Missing value for --provider"); + expect(result.stderr).toContain("opencode-quota show"); + expect(discoverOpenCodeService).not.toHaveBeenCalled(); }); it("reports unknown flag with --json as error on stderr with exit code 1", async () => { - const jsonOut = createCaptureStream(); - const jsonErr = createCaptureStream(); - - const jsonCode = await runCliShowCommand({ - argv: ["--json", "--bogus-flag"], - cwd: workspaceDir, - stdout: jsonOut.stream as any, - stderr: jsonErr.stream as any, - }); + const result = await runShow(["--json", "--bogus-flag"]); - expect(jsonCode).toBe(1); - expect(jsonErr.output).toContain("Unknown option: --bogus-flag"); - expect(jsonErr.output).toContain("opencode-quota show"); + expect(result.code).toBe(1); + expect(result.stderr).toContain("Unknown option: --bogus-flag"); + expect(result.stderr).toContain("opencode-quota show"); }); it("--threshold validates input and requires --json", async () => { - const run = async (argv: string[]) => { - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const code = await runCliShowCommand({ - argv, - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - return { code, stderr: stderr.output }; - }; - - // Invalid value - let result = await run(["--json", "--threshold", "abc"]); + let result = await runShow(["--json", "--threshold", "abc"]); expect(result.code).toBe(1); expect(result.stderr).toContain("--threshold must be a positive finite number"); - // Zero - result = await run(["--json", "--threshold", "0"]); + result = await runShow(["--json", "--threshold", "0"]); expect(result.code).toBe(1); expect(result.stderr).toContain("--threshold must be a positive finite number"); - // Missing value - result = await run(["--json", "--threshold"]); + result = await runShow(["--json", "--threshold"]); expect(result.code).toBe(1); expect(result.stderr).toContain("Missing value for --threshold"); - // Without --json - result = await run(["--threshold", "5"]); + result = await runShow(["--threshold", "5"]); expect(result.code).toBe(1); expect(result.stderr).toContain("--threshold requires --json"); + + expect(discoverOpenCodeService).not.toHaveBeenCalled(); + }); + + it("prints help that names the OpenCode requirement and exit code 3", async () => { + const result = await runShow(["--help"]); + + expect(result.code).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain("opencode-quota show"); + expect(result.stdout).toContain("Needs OpenCode running"); + expect(result.stdout).toContain("Exit code 3"); + expect(discoverOpenCodeService).not.toHaveBeenCalled(); }); }); diff --git a/tests/lib.cli-status.test.ts b/tests/lib.cli-status.test.ts index cd5e7f91..d7629bf8 100644 --- a/tests/lib.cli-status.test.ts +++ b/tests/lib.cli-status.test.ts @@ -1,50 +1,20 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const { mockProviders, runtimeDirs, statusData } = vi.hoisted(() => ({ - mockProviders: [] as any[], - runtimeDirs: { - value: { - dataDir: "/tmp/opencode-quota-cli-status-data", - configDir: "/tmp/opencode-quota-cli-status-config", - cacheDir: "/tmp/opencode-quota-cli-status-cache", - stateDir: "/tmp/opencode-quota-cli-status-state", - }, - }, - statusData: { - value: null as null | { - output: string; - payload: Record; - hasComparableProviderData?: boolean; - }, - }, -})); - -vi.mock("../src/providers/registry.js", () => ({ - getProviders: () => mockProviders, -})); +import { homedir } from "node:os"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirs: () => runtimeDirs.value, +vi.mock("../src/lib/opencode-service.js", () => ({ + discoverOpenCodeService: vi.fn(), })); -vi.mock("../src/lib/quota-dialog-commands.js", () => ({ - buildStatusReportData: vi.fn(async () => { - if (!statusData.value) { - return { output: null, payload: null }; - } - return { - output: statusData.value.output, - payload: statusData.value.payload, - hasComparableProviderData: statusData.value.hasComparableProviderData ?? true, - }; - }), +vi.mock("../src/lib/quota-rpc-client.js", async (importOriginal) => ({ + ...(await importOriginal()), + callQuotaRpc: vi.fn(), })); import { runCliStatusCommand } from "../src/lib/cli-status.js"; -import { buildStatusReportData } from "../src/lib/quota-dialog-commands.js"; +import { discoverOpenCodeService } from "../src/lib/opencode-service.js"; +import { callQuotaRpc, QuotaRpcTransportError } from "../src/lib/quota-rpc-client.js"; + +const ENDPOINT = { url: "http://127.0.0.1:4096", headers: {} }; function createCaptureStream() { let output = ""; @@ -61,188 +31,113 @@ function createCaptureStream() { }; } -function basePayload(overrides: Record = {}) { - return { - version: "3.11.2", - generatedAt: "2026-07-16T00:00:00.000Z", - config: { - configSource: "workspace", - configPaths: ["/tmp/opencode.json"], - enabledProviders: ["synthetic"], - onlyCurrentModel: false, - pricingSnapshotSource: "auto", - }, - providers: [ - { id: "synthetic", enabled: true, available: true, matchesCurrentModel: undefined }, - ], - pricing: { - selection: "auto", - activeSource: "bundled", - snapshot: { - source: "bundled", - generatedAt: "2026-01-01T00:00:00.000Z", - units: "USD per 1M tokens", - }, - snapshotPath: "/tmp/pricing.json", - refreshStatePath: "/tmp/pricing-state.json", - }, - liveProbes: [{ id: "synthetic", ok: true }], - ...overrides, - }; -} - -function useStatusData( - payload = basePayload(), - hasComparableProviderData?: boolean, - output = "report text", -): void { - statusData.value = { output, payload, hasComparableProviderData }; +async function runStatus(argv: string[]) { + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + const code = await runCliStatusCommand({ + argv, + stdout: stdout.stream as any, + stderr: stderr.stream as any, + }); + return { code, stdout: stdout.output, stderr: stderr.output }; } describe("runCliStatusCommand", () => { - let tempDir: string; - let globalConfigDir: string; - let workspaceDir: string; - let savedConfigDir: string | undefined; - - async function runStatus(argv: string[]) { - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const code = await runCliStatusCommand({ - argv, - cwd: workspaceDir, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - return { code, stdout: stdout.output, stderr: stderr.output }; - } - beforeEach(() => { - savedConfigDir = process.env.OPENCODE_CONFIG_DIR; - delete process.env.OPENCODE_CONFIG_DIR; - tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-cli-status-")); - globalConfigDir = join(tempDir, "global-config", "opencode"); - workspaceDir = join(tempDir, "workspace"); - mkdirSync(globalConfigDir, { recursive: true }); - mkdirSync(workspaceDir, { recursive: true }); - runtimeDirs.value = { - dataDir: "/tmp/opencode-quota-cli-status-data", - configDir: globalConfigDir, - cacheDir: join(tempDir, "cache"), - stateDir: "/tmp/opencode-quota-cli-status-state", - }; - mockProviders.length = 0; - statusData.value = null; - vi.mocked(buildStatusReportData).mockClear(); - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ - experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, - }), - "utf8", - ); - }); - - afterEach(() => { - if (savedConfigDir !== undefined) process.env.OPENCODE_CONFIG_DIR = savedConfigDir; - else delete process.env.OPENCODE_CONFIG_DIR; - mockProviders.length = 0; - statusData.value = null; - rmSync(tempDir, { recursive: true, force: true }); + vi.mocked(discoverOpenCodeService).mockReset().mockResolvedValue(ENDPOINT); + vi.mocked(callQuotaRpc).mockReset().mockResolvedValue({ + exitCode: 0, + stdout: "Quota Status (opencode-quota v5.0.0)\n", + stderr: "", + }); }); - it("prints a plain-text Quota Status report and returns zero", async () => { - useStatusData( - basePayload(), - undefined, - "Quota Status (opencode-quota v3.11.2)\ntoast:\n- enabledProviders: synthetic", - ); + it("asks the running OpenCode service for the report at the home folder and prints it", async () => { const result = await runStatus([]); - expect(result.code).toBe(0); - expect(result.stdout).toContain("Quota Status"); - expect(result.stdout).toContain("enabledProviders: synthetic"); - expect(result.stderr).toBe(""); - expect(buildStatusReportData).toHaveBeenCalledWith( - expect.objectContaining({ providerFilterId: undefined }), + expect(result).toEqual({ + code: 0, + stdout: "Quota Status (opencode-quota v5.0.0)\n", + stderr: "", + }); + expect(callQuotaRpc).toHaveBeenCalledWith( + ENDPOINT, + "cli", + { command: "status", providerId: undefined }, + { directory: homedir(), timeoutMs: 60_000 }, ); }); - it("--json emits a structured payload and returns zero when live probes exist", async () => { - useStatusData(); - const result = await runStatus(["--json"]); - - expect(result.code).toBe(0); - expect(result.stderr).toBe(""); - const parsed = JSON.parse(result.stdout); - expect(parsed).toHaveProperty("version", "3.11.2"); - expect(parsed).toHaveProperty("generatedAt"); - expect(parsed).toHaveProperty("config"); - expect(parsed).toHaveProperty("providers"); - expect(parsed).toHaveProperty("pricing"); - expect(parsed).toHaveProperty("liveProbes"); - expect(parsed.liveProbes).toHaveLength(1); - expect(parsed.liveProbes[0]).toEqual({ id: "synthetic", ok: true }); - }); - - it("--json exits 2 when there is no comparable provider data", async () => { - useStatusData(basePayload({ liveProbes: [] }), false); - const result = await runStatus(["--json"]); - - expect(result.code).toBe(2); - expect(result.stderr).toBe(""); - // JSON still prints on exit 2. - const parsed = JSON.parse(result.stdout); - expect(parsed.liveProbes).toEqual([]); - }); + it("forwards --json and returns the report's exit code", async () => { + vi.mocked(callQuotaRpc).mockResolvedValue({ + exitCode: 2, + stdout: '{\n "liveProbes": []\n}\n', + stderr: "", + }); - it("--json exits 2 when probes fail without producing quota entries", async () => { - useStatusData(basePayload({ liveProbes: [{ id: "synthetic", ok: false }] }), false); const result = await runStatus(["--json"]); - expect(result.code).toBe(2); - expect(JSON.parse(result.stdout).liveProbes).toEqual([{ id: "synthetic", ok: false }]); - }); - - it("--json succeeds when a partial probe produced quota entries", async () => { - useStatusData(basePayload({ liveProbes: [{ id: "synthetic", ok: false }] }), true); - - expect((await runStatus(["--json"])).code).toBe(0); + expect(result).toEqual({ code: 2, stdout: '{\n "liveProbes": []\n}\n', stderr: "" }); + expect(callQuotaRpc).toHaveBeenCalledWith( + ENDPOINT, + "cli", + { command: "status-json", providerId: undefined }, + expect.anything(), + ); }); - it("--provider filters the report to one provider", async () => { - useStatusData(); - const result = await runStatus(["--provider", "synthetic"]); + it("--provider --json forwards the provider filter", async () => { + const result = await runStatus(["--json", "--provider", "synthetic"]); expect(result.code).toBe(0); - expect(result.stderr).toBe(""); - expect(buildStatusReportData).toHaveBeenCalledWith( - expect.objectContaining({ providerFilterId: "synthetic" }), + expect(callQuotaRpc).toHaveBeenCalledWith( + ENDPOINT, + "cli", + { command: "status-json", providerId: "synthetic" }, + expect.anything(), ); }); it("resolves a case-insensitive provider synonym before filtering", async () => { - useStatusData(); const result = await runStatus(["--provider", " CLAUDE "]); expect(result.code).toBe(0); expect(result.stderr).toBe(""); - expect(buildStatusReportData).toHaveBeenCalledWith( - expect.objectContaining({ providerFilterId: "anthropic" }), + expect(callQuotaRpc).toHaveBeenCalledWith( + ENDPOINT, + "cli", + { command: "status", providerId: "anthropic" }, + expect.anything(), ); }); - it("--provider --json forwards the provider filter and still emits JSON", async () => { - useStatusData(); - const result = await runStatus(["--json", "--provider", "synthetic"]); + it("exits 3 with an empty stdout when OpenCode is not running", async () => { + vi.mocked(discoverOpenCodeService).mockResolvedValue(undefined); - expect(result.code).toBe(0); - expect(buildStatusReportData).toHaveBeenCalledWith( - expect.objectContaining({ providerFilterId: "synthetic" }), + const result = await runStatus(["--json"]); + + expect(result).toEqual({ + code: 3, + stdout: "", + stderr: "OpenCode is not running. Start OpenCode and try again.\n", + }); + expect(callQuotaRpc).not.toHaveBeenCalled(); + }); + + it("exits 3 with an empty stdout when the call to OpenCode fails", async () => { + vi.mocked(callQuotaRpc).mockRejectedValue( + new QuotaRpcTransportError( + "OpenCode's service password changed. Restart OpenCode and try again.", + ), ); - const parsed = JSON.parse(result.stdout); - expect(parsed.providers).toHaveLength(1); - expect(parsed).not.toHaveProperty("providerFilterId"); + + const result = await runStatus([]); + + expect(result).toEqual({ + code: 3, + stdout: "", + stderr: "OpenCode's service password changed. Restart OpenCode and try again.\n", + }); }); it("rejects --threshold with a redirect to show --json --threshold", async () => { @@ -252,7 +147,7 @@ describe("runCliStatusCommand", () => { expect(result.stdout).toBe(""); expect(result.stderr).toContain("--threshold is not supported by status"); expect(result.stderr).toContain("opencode-quota show --json --threshold"); - expect(buildStatusReportData).not.toHaveBeenCalled(); + expect(discoverOpenCodeService).not.toHaveBeenCalled(); }); it("rejects --threshold even when combined with --json", async () => { @@ -260,16 +155,17 @@ describe("runCliStatusCommand", () => { expect(result.code).toBe(1); expect(result.stderr).toContain("--threshold is not supported by status"); - expect(buildStatusReportData).not.toHaveBeenCalled(); + expect(discoverOpenCodeService).not.toHaveBeenCalled(); }); - it("rejects an unknown provider before building the report", async () => { + it("rejects an unknown provider before contacting OpenCode", async () => { const result = await runStatus(["--provider", "not-a-provider"]); expect(result.code).toBe(1); expect(result.stdout).toBe(""); expect(result.stderr).toContain("Unknown provider: not-a-provider"); - expect(buildStatusReportData).not.toHaveBeenCalled(); + expect(discoverOpenCodeService).not.toHaveBeenCalled(); + expect(callQuotaRpc).not.toHaveBeenCalled(); }); it("rejects a missing --provider value", async () => { @@ -288,35 +184,17 @@ describe("runCliStatusCommand", () => { expect(result.stderr).toContain("opencode-quota status"); }); - it("returns non-zero when quota is disabled in config", async () => { - writeFileSync( - join(workspaceDir, "opencode.json"), - JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), - "utf8", - ); - const result = await runStatus([]); - - expect(result.code).toBe(1); - expect(result.stdout).toBe(""); - expect(result.stderr).toContain("Quota disabled in config"); - expect(buildStatusReportData).not.toHaveBeenCalled(); - }); - it("prints help and returns zero for --help", async () => { const result = await runStatus(["--help"]); expect(result.code).toBe(0); expect(result.stderr).toBe(""); expect(result.stdout).toContain("opencode-quota status"); + expect(result.stdout).toContain("Needs OpenCode running"); expect(result.stdout).toContain("Exit codes:"); - expect(buildStatusReportData).not.toHaveBeenCalled(); - }); - - it("returns non-zero when report building throws", async () => { - vi.mocked(buildStatusReportData).mockRejectedValueOnce(new Error("boom")); - const result = await runStatus([]); - - expect(result.code).toBe(1); - expect(result.stderr).toContain("Failed to generate quota status: boom"); + expect(result.stdout).toContain( + "3 OpenCode is not running or the plugin could not be reached", + ); + expect(discoverOpenCodeService).not.toHaveBeenCalled(); }); }); diff --git a/tests/lib.opencode-service.test.ts b/tests/lib.opencode-service.test.ts new file mode 100644 index 00000000..ebcb7397 --- /dev/null +++ b/tests/lib.opencode-service.test.ts @@ -0,0 +1,147 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type IncomingHttpHeaders, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { discoverOpenCodeService } from "../src/lib/opencode-service.js"; + +type InfoAnswer = { status: number; body: unknown } | "never"; + +describe("discoverOpenCodeService", () => { + let stateHome: string; + let savedStateHome: string | undefined; + let server: Server | undefined; + let requests: Array<{ url?: string; headers: IncomingHttpHeaders }>; + + // Every test points XDG_STATE_HOME at a temp folder, so the real service.json is never read. + beforeEach(() => { + savedStateHome = process.env.XDG_STATE_HOME; + stateHome = mkdtempSync(join(tmpdir(), "opencode-quota-service-")); + process.env.XDG_STATE_HOME = stateHome; + requests = []; + }); + + afterEach(async () => { + if (savedStateHome === undefined) delete process.env.XDG_STATE_HOME; + else process.env.XDG_STATE_HOME = savedStateHome; + if (server) { + server.closeAllConnections(); + await new Promise((resolve) => server?.close(resolve)); + server = undefined; + } + rmSync(stateHome, { recursive: true, force: true }); + }); + + async function startInfoServer(answer: InfoAnswer): Promise { + server = createServer((request, response) => { + requests.push({ url: request.url, headers: request.headers }); + if (answer === "never") return; + response.writeHead(answer.status, { "content-type": "application/json" }); + response.end(JSON.stringify(answer.body)); + }); + await new Promise((resolve) => server?.listen(0, "127.0.0.1", resolve)); + return `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + } + + function writeRegistration(value: unknown): void { + mkdirSync(join(stateHome, "opencode"), { recursive: true }); + writeFileSync( + join(stateHome, "opencode", "service.json"), + typeof value === "string" ? value : JSON.stringify(value), + "utf8", + ); + } + + it("returns undefined when no service is registered", async () => { + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + }); + + it("returns undefined for a registration file that is not valid JSON", async () => { + writeRegistration("{not json"); + + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + }); + + it("returns undefined for a registration without a url or an integer pid", async () => { + const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 42 } }); + + writeRegistration({ pid: 42 }); + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + writeRegistration({ url, pid: "42" }); + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + writeRegistration({ url, pid: 4.2 }); + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + expect(requests).toEqual([]); + }); + + it("finds a service without a password and sends no authorization header", async () => { + const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 42 } }); + writeRegistration({ id: "service", version: "2.0.16", url, pid: 42 }); + + await expect(discoverOpenCodeService()).resolves.toEqual({ url, headers: {} }); + expect(requests).toHaveLength(1); + expect(requests[0].url).toBe("/api/info"); + expect(requests[0].headers.authorization).toBeUndefined(); + }); + + it("finds a service with a password and returns its basic auth header", async () => { + const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 42 } }); + writeRegistration({ id: "service", version: "2.0.16", url, pid: 42, password: "secret" }); + const authorization = `Basic ${Buffer.from("opencode:secret").toString("base64")}`; + + await expect(discoverOpenCodeService()).resolves.toEqual({ + url, + headers: { authorization }, + }); + expect(requests[0].headers.authorization).toBe(authorization); + }); + + it("returns undefined when another process answers", async () => { + const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 7 } }); + writeRegistration({ version: "2.0.16", url, pid: 42 }); + + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + }); + + it("returns undefined when the answering version differs from the registration", async () => { + const url = await startInfoServer({ status: 200, body: { version: "2.0.17", pid: 42 } }); + writeRegistration({ version: "2.0.16", url, pid: 42 }); + + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + }); + + it("returns undefined for a non-2xx answer or a malformed info body", async () => { + const url = await startInfoServer({ status: 401, body: { message: "Unauthorized" } }); + writeRegistration({ url, pid: 42, password: "old" }); + + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + server?.closeAllConnections(); + await new Promise((resolve) => server?.close(resolve)); + + const malformed = await startInfoServer({ status: 200, body: { version: 2, pid: 42 } }); + writeRegistration({ url: malformed, pid: 42 }); + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + }); + + it("returns undefined when nothing listens at the registered url", async () => { + const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 42 } }); + server?.closeAllConnections(); + await new Promise((resolve) => server?.close(resolve)); + server = undefined; + writeRegistration({ url, pid: 42 }); + + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + }); + + it("returns undefined when the service does not answer within 2 seconds", async () => { + const url = await startInfoServer("never"); + writeRegistration({ url, pid: 42 }); + + const startedAt = Date.now(); + await expect(discoverOpenCodeService()).resolves.toBeUndefined(); + expect(Date.now() - startedAt).toBeGreaterThanOrEqual(1900); + expect(requests).toHaveLength(1); + }); +}); diff --git a/tests/lib.quota-rpc-client.test.ts b/tests/lib.quota-rpc-client.test.ts new file mode 100644 index 00000000..40a8f0eb --- /dev/null +++ b/tests/lib.quota-rpc-client.test.ts @@ -0,0 +1,173 @@ +import { createServer, type IncomingHttpHeaders, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterEach, describe, expect, it } from "vitest"; + +import { callQuotaRpc, QuotaRpcTransportError } from "../src/lib/quota-rpc-client.js"; + +type Answer = { status: number; body: string } | "never"; + +describe("callQuotaRpc", () => { + let server: Server | undefined; + let requests: Array<{ + method?: string; + url?: string; + headers: IncomingHttpHeaders; + body: string; + }>; + + afterEach(async () => { + if (server) { + server.closeAllConnections(); + await new Promise((resolve) => server?.close(resolve)); + server = undefined; + } + }); + + async function startServer(answer: Answer): Promise { + requests = []; + server = createServer((request, response) => { + let body = ""; + request.on("data", (chunk) => { + body += chunk; + }); + request.on("end", () => { + requests.push({ method: request.method, url: request.url, headers: request.headers, body }); + if (answer === "never") return; + response.writeHead(answer.status, { "content-type": "application/json" }); + response.end(answer.body); + }); + }); + await new Promise((resolve) => server?.listen(0, "127.0.0.1", resolve)); + return `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + } + + async function callFailure(url: string, timeoutMs = 5000): Promise { + return callQuotaRpc( + { url, headers: {} }, + "cli", + { command: "show" }, + { directory: "/home/user", timeoutMs }, + ).then( + () => { + throw new Error("expected the call to fail"); + }, + (error: unknown) => error, + ); + } + + it("posts the input to the plugin's method at the location and returns the output", async () => { + const url = await startServer({ + status: 200, + body: JSON.stringify({ output: { exitCode: 0, stdout: "ok\n", stderr: "" } }), + }); + + const output = await callQuotaRpc( + { url, headers: { authorization: "Basic abc" } }, + "cli", + { command: "show-json", threshold: 5 }, + { directory: "/Users/Jöhn Doe", timeoutMs: 5000 }, + ); + + expect(output).toEqual({ exitCode: 0, stdout: "ok\n", stderr: "" }); + expect(requests).toHaveLength(1); + expect(requests[0].method).toBe("POST"); + expect(requests[0].url).toBe("/api/rpc/slkiser.opencode-quota/cli"); + expect(requests[0].headers.authorization).toBe("Basic abc"); + expect(requests[0].headers["content-type"]).toBe("application/json"); + expect(requests[0].headers["x-opencode-directory"]).toBe("%2FUsers%2FJ%C3%B6hn%20Doe"); + expect(decodeURIComponent(requests[0].headers["x-opencode-directory"] as string)).toBe( + "/Users/Jöhn Doe", + ); + expect(JSON.parse(requests[0].body)).toEqual({ + input: { command: "show-json", threshold: 5 }, + }); + }); + + it("explains an unloaded server plugin", async () => { + const url = await startServer({ + status: 400, + body: JSON.stringify({ + _tag: "RpcError", + type: "rpc.unavailable", + message: "RPC is unavailable: slkiser.opencode-quota", + }), + }); + + const error = await callFailure(url); + + expect(error).toBeInstanceOf(QuotaRpcTransportError); + expect((error as Error).message).toBe( + "OpenCode Quota's server plugin is not loaded. Check the plugin entry in opencode.json and restart OpenCode.", + ); + }); + + it("explains a rejected service password", async () => { + const url = await startServer({ + status: 401, + body: JSON.stringify({ _tag: "UnauthorizedError", message: "Unauthorized" }), + }); + + const error = await callFailure(url); + + expect(error).toBeInstanceOf(QuotaRpcTransportError); + expect((error as Error).message).toBe( + "OpenCode's service password changed. Restart OpenCode and try again.", + ); + }); + + it("passes other RPC errors on as a sanitized single line", async () => { + const url = await startServer({ + status: 400, + body: JSON.stringify({ + _tag: "RpcError", + type: "rpc.method_not_found", + message: "Unknown RPC method:\u001b[31m slkiser.opencode-quota.cli\n", + }), + }); + + const error = await callFailure(url); + + expect(error).toBeInstanceOf(QuotaRpcTransportError); + expect((error as Error).message).toBe( + "OpenCode could not run OpenCode Quota: Unknown RPC method: slkiser.opencode-quota.cli", + ); + }); + + it("names the HTTP status when the error body has no message", async () => { + const url = await startServer({ status: 502, body: "Bad gateway" }); + + const error = await callFailure(url); + + expect((error as Error).message).toBe("OpenCode could not run OpenCode Quota: HTTP 502"); + }); + + it("rejects a successful answer without an output", async () => { + const url = await startServer({ status: 200, body: JSON.stringify({ result: {} }) }); + + const error = await callFailure(url); + + expect(error).toBeInstanceOf(QuotaRpcTransportError); + expect((error as Error).message).toBe("OpenCode returned an answer without an output."); + }); + + it("reports a network error with its reason", async () => { + const url = await startServer({ status: 200, body: "{}" }); + server?.closeAllConnections(); + await new Promise((resolve) => server?.close(resolve)); + server = undefined; + + const error = await callFailure(url); + + expect(error).toBeInstanceOf(QuotaRpcTransportError); + expect((error as Error).message).toMatch(/^Could not reach OpenCode: .*ECONNREFUSED/); + }); + + it("reports a call that does not answer in time", async () => { + const url = await startServer("never"); + + const error = await callFailure(url, 200); + + expect(error).toBeInstanceOf(QuotaRpcTransportError); + expect((error as Error).message).toBe("OpenCode did not answer within 0.2 s. Try again."); + }); +}); diff --git a/tests/plugin-rpc.test.ts b/tests/plugin-rpc.test.ts index f6af1c3c..75266328 100644 --- a/tests/plugin-rpc.test.ts +++ b/tests/plugin-rpc.test.ts @@ -138,7 +138,7 @@ describe("server quota RPC", () => { await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); - it("registers the quota RPC with its four methods during setup", async () => { + it("registers the quota RPC with its five methods during setup", async () => { const { QuotaRpc } = await import("../src/rpc.js"); const { definition, handlers } = await setupServer(); @@ -149,6 +149,7 @@ describe("server quota RPC", () => { "footer", "writeExport", "command", + "cli", ]); expect(Object.keys(handlers)).toEqual(Object.keys(definition.methods)); }); @@ -276,6 +277,91 @@ describe("server quota RPC", () => { }); }); + it("builds the terminal show report and its cached JSON with threshold exit codes", async () => { + useConfig({}); + const { call } = await setupServer(); + + const text = (await call("cli", { command: "show" })) as { + exitCode: number; + stdout: string; + stderr: string; + }; + expect(text.exitCode).toBe(0); + expect(text.stdout).toContain("Copilot"); + expect(text.stdout).toContain("81%"); + expect(text.stderr).toBe(""); + + const json = (await call("cli", { command: "show-json", providerId: "copilot" })) as { + exitCode: number; + stdout: string; + stderr: string; + }; + expect(json).toEqual({ exitCode: 0, stdout: expect.any(String), stderr: "" }); + expect(JSON.parse(json.stdout).providers.copilot).toEqual( + expect.objectContaining({ status: "ok" }), + ); + await expect(call("cli", { command: "show-json", threshold: 50 })).resolves.toEqual( + expect.objectContaining({ exitCode: 0 }), + ); + await expect(call("cli", { command: "show-json", threshold: 90 })).resolves.toEqual( + expect.objectContaining({ exitCode: 1 }), + ); + }); + + it("builds the terminal status report as text and JSON", async () => { + useConfig({}); + const dialogModule = await import("../src/lib/quota-dialog-commands.js"); + const buildStatusReportData = vi + .spyOn(dialogModule, "buildStatusReportData") + .mockResolvedValue({ + output: "Quota Status (opencode-quota v5.0.0)", + payload: { version: "5.0.0", liveProbes: [] } as never, + hasComparableProviderData: false, + }); + const { call } = await setupServer(); + + await expect(call("cli", { command: "status" })).resolves.toEqual({ + exitCode: 0, + stdout: "Quota Status (opencode-quota v5.0.0)\n", + stderr: "", + }); + expect(buildStatusReportData).toHaveBeenLastCalledWith( + expect.objectContaining({ providerFilterId: undefined }), + ); + + await expect(call("cli", { command: "status-json", providerId: "copilot" })).resolves.toEqual({ + exitCode: 2, + stdout: `${JSON.stringify({ version: "5.0.0", liveProbes: [] }, null, 2)}\n`, + stderr: "", + }); + expect(buildStatusReportData).toHaveBeenLastCalledWith( + expect.objectContaining({ providerFilterId: "copilot" }), + ); + }); + + it("returns disabled quota and failures as the terminal command's stderr", async () => { + useConfig({ enabled: false }); + const disabled = await setupServer(); + await expect(disabled.call("cli", { command: "show-json" })).resolves.toEqual({ + exitCode: 1, + stdout: "", + stderr: "Quota disabled in config (enabled: false).\n", + }); + + mocks.loadConfig.mockRejectedValue(new Error("boom")); + const failing = await setupServer(); + await expect(failing.call("cli", { command: "show" })).resolves.toEqual({ + exitCode: 1, + stdout: "", + stderr: "Failed to show quota: boom\n", + }); + await expect(failing.call("cli", { command: "status-json" })).resolves.toEqual({ + exitCode: 1, + stdout: "", + stderr: "Failed to generate quota status: boom\n", + }); + }); + it("treats a session the server cannot find as a session without a model", async () => { useConfig({ onlyCurrentModel: true }); const input = { surface: "sidebar", sessionID: "missing" }; diff --git a/tests/rpc-definition.test.ts b/tests/rpc-definition.test.ts index 88115224..0d03020d 100644 --- a/tests/rpc-definition.test.ts +++ b/tests/rpc-definition.test.ts @@ -33,11 +33,11 @@ async function expectRejected(schema: StandardSchema, value: unknown, message: s const methods = QuotaRpc.methods; describe("quota RPC definition", () => { - it("defines the plain RPC id, no events and four methods", () => { + it("defines the plain RPC id, no events and five methods", () => { expect(QUOTA_RPC_ID).toBe("slkiser.opencode-quota"); expect(QuotaRpc.id).toBe(QUOTA_RPC_ID); expect(QuotaRpc.events).toEqual({}); - expect(Object.keys(methods)).toEqual(["surface", "footer", "writeExport", "command"]); + expect(Object.keys(methods)).toEqual(["surface", "footer", "writeExport", "command", "cli"]); }); it("uses Standard Schema v1 validators that carry no runtime types", () => { @@ -127,6 +127,41 @@ describe("quota RPC definition", () => { ); }); + it("validates cli input", async () => { + for (const command of ["show", "show-json", "status", "status-json"]) { + await expectAccepted(methods.cli.input, { command }); + } + await expectAccepted(methods.cli.input, { + command: "show-json", + providerId: "copilot", + threshold: 5, + }); + await expectAccepted(methods.cli.input, { command: "status", providerId: "anthropic" }); + await expectRejected(methods.cli.input, "show", "input must be an object"); + await expectRejected( + methods.cli.input, + {}, + "command must be one of show, show-json, status, status-json", + ); + await expectRejected( + methods.cli.input, + { command: "update" }, + "command must be one of show, show-json, status, status-json", + ); + await expectRejected( + methods.cli.input, + { command: "show", providerId: 1 }, + "providerId must be a string", + ); + for (const threshold of ["5", 0, -1]) { + await expectRejected( + methods.cli.input, + { command: "show-json", threshold }, + "threshold must be a positive finite number", + ); + } + }); + it("requires every output to be an object", async () => { for (const method of Object.values(methods)) { await expectAccepted(method.output, { any: "object" }); From 23aa94fac61bfd26811330a47bc4991ffd66e561 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 13:57:30 +0200 Subject: [PATCH 68/97] refactor(auth): name integration ids on every login read and model failed logins Every credential read now names the OpenCode integration ids it needs, so the upcoming plugin-API reader can ask OpenCode for exactly those logins. Fan-out reads also name the connection methods they use, and the Console reader takes only the active opencode login. A failed login (a CredentialRow with resolveError) now shows as its own error row for OpenAI, xAI, Gemini CLI, Z.ai/Zhipu, Kimi, MiniMax and Go, and still counts as present. The SQLite reader never produces one, so nothing changes yet; the plugin-API reader will. Co-authored-by: Ran Sagy --- contributing/provider-template/config.test.ts | 3 + contributing/provider-template/config.ts | 2 +- src/lib/alibaba-auth.ts | 1 + src/lib/anthropic-auth.ts | 1 + src/lib/api-key-resolver.ts | 6 + src/lib/chutes-config.ts | 2 +- src/lib/copilot.ts | 20 +- src/lib/cursor-detection.ts | 7 +- src/lib/deepseek-auth.ts | 2 +- src/lib/google-agy.ts | 16 +- src/lib/google-gemini-cli.ts | 44 +++-- src/lib/kilo-config.ts | 6 +- src/lib/kimi-auth.ts | 2 +- src/lib/minimax-auth.ts | 1 + src/lib/nanogpt-config.ts | 2 +- src/lib/ollama-cloud-config.ts | 6 +- src/lib/openai.ts | 21 +- src/lib/opencode-auth.ts | 111 ++++++++--- src/lib/opencode-console-auth.ts | 14 +- src/lib/opencode-go-auth.ts | 2 +- src/lib/quota-providers-remote.ts | 2 +- src/lib/synthetic-config.ts | 2 +- src/lib/types.ts | 8 + src/lib/xai.ts | 16 +- src/lib/zai-auth.ts | 2 +- src/lib/zhipu-auth.ts | 2 +- src/providers/anthropic.ts | 4 +- src/providers/copilot.ts | 14 +- src/providers/glm-coding-plan-provider.ts | 28 ++- src/providers/google-agy.ts | 5 +- src/providers/google-gemini-cli.ts | 25 ++- src/providers/kimi-code.ts | 16 +- src/providers/minimax-coding-plan.ts | 14 +- src/providers/openai.ts | 27 ++- src/providers/opencode-go.ts | 5 +- src/providers/xai.ts | 19 +- tests/lib.alibaba-auth.test.ts | 6 +- tests/lib.anthropic-auth.test.ts | 6 +- tests/lib.api-key-resolver.test.ts | 10 + tests/lib.cursor-detection.test.ts | 14 ++ tests/lib.google-gemini-cli.test.ts | 56 ++++++ tests/lib.invalid-aware-provider-auth.test.ts | 32 +++ tests/lib.minimax-auth.test.ts | 6 +- tests/lib.ollama-cloud-config.test.ts | 7 +- tests/lib.openai.test.ts | 33 ++++ tests/lib.opencode-auth.test.ts | 182 +++++++++++++++++- tests/lib.opencode-console-auth.test.ts | 52 ++++- tests/lib.opencode-db-path.test.ts | 12 +- tests/lib.opencode-go-auth.test.ts | 6 +- tests/lib.xai.test.ts | 24 +++ tests/provider-surfaces.google-agy.test.ts | 1 + tests/providers.copilot.test.ts | 1 + tests/providers.glm-coding-plan.test.ts | 66 +++++++ tests/providers.google-agy.test.ts | 1 + tests/providers.google-db-credentials.test.ts | 8 + tests/providers.google-gemini-cli.test.ts | 69 +++++++ tests/providers.kimi-code.test.ts | 46 +++++ tests/providers.minimax-coding-plan.test.ts | 3 + tests/providers.openai.test.ts | 57 ++++++ tests/providers.xai.test.ts | 37 ++++ 60 files changed, 1055 insertions(+), 138 deletions(-) diff --git a/contributing/provider-template/config.test.ts b/contributing/provider-template/config.test.ts index ae519d0c..f48d1df4 100644 --- a/contributing/provider-template/config.test.ts +++ b/contributing/provider-template/config.test.ts @@ -152,5 +152,8 @@ describe("example provider config", () => { key: "auth-key", source: "opencode.db", }); + expect(readAuthFile).toHaveBeenCalledWith({ + integrationIds: ["example-provider", "exampleProvider"], + }); }); }); diff --git a/contributing/provider-template/config.ts b/contributing/provider-template/config.ts index d58da3a7..b4ed5d33 100644 --- a/contributing/provider-template/config.ts +++ b/contributing/provider-template/config.ts @@ -38,7 +38,7 @@ export async function resolveExampleProviderApiKey(): Promise readAuthFile({ integrationIds: PROVIDER_KEYS }), authSource: "opencode.db", }, }); diff --git a/src/lib/alibaba-auth.ts b/src/lib/alibaba-auth.ts index f8f0889e..9616bc9a 100644 --- a/src/lib/alibaba-auth.ts +++ b/src/lib/alibaba-auth.ts @@ -197,6 +197,7 @@ async function resolveAlibabaCodingPlanAuthWithSource(params?: { const maxAgeMs = Math.max(0, params?.maxAgeMs ?? DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS); const authData = await readAuthFileCached({ maxAgeMs, + integrationIds: ALIBABA_AUTH_KEYS, }); const auth = resolveAlibabaCodingPlanAuth(authData, fallbackTier); diff --git a/src/lib/anthropic-auth.ts b/src/lib/anthropic-auth.ts index 3ea6042b..82e4ba8d 100644 --- a/src/lib/anthropic-auth.ts +++ b/src/lib/anthropic-auth.ts @@ -45,6 +45,7 @@ export async function resolveAnthropicOAuthCached(params?: { }): Promise { const auth = await readAuthFileCached({ maxAgeMs: Math.max(0, params?.maxAgeMs ?? DEFAULT_ANTHROPIC_AUTH_CACHE_MAX_AGE_MS), + integrationIds: ["anthropic"], }); return resolveAnthropicOAuth(auth); } diff --git a/src/lib/api-key-resolver.ts b/src/lib/api-key-resolver.ts index dccfa1b6..c9bc32c3 100644 --- a/src/lib/api-key-resolver.ts +++ b/src/lib/api-key-resolver.ts @@ -337,6 +337,12 @@ function parseInvalidAwareAuth( } const record = entry as Record; + if (typeof record.resolveError === "string") { + return { + state: "invalid", + error: `OpenCode could not read this login: ${record.resolveError}`, + }; + } if (typeof record.type !== "string") { return { state: "invalid", diff --git a/src/lib/chutes-config.ts b/src/lib/chutes-config.ts index a39b16eb..9c1b3475 100644 --- a/src/lib/chutes-config.ts +++ b/src/lib/chutes-config.ts @@ -28,7 +28,7 @@ const chutesApiKeyResolver = createProviderApiKeyResolver({ configJsoncSource: "opencode.jsonc", getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: readAuthFile, + readAuth: () => readAuthFile({ integrationIds: CHUTES_PROVIDER_KEYS }), getCredentialDatabasePaths, authSource: "opencode.db", }, diff --git a/src/lib/copilot.ts b/src/lib/copilot.ts index b77db249..eb44cf67 100644 --- a/src/lib/copilot.ts +++ b/src/lib/copilot.ts @@ -34,7 +34,15 @@ const COPILOT_QUOTA_CONFIG_FILENAME = "copilot-quota-token.json"; const USER_AGENT = "opencode-quota/copilot-billing"; const COPILOT_INTERNAL_USER_PATH = "/copilot_internal/user"; -type CopilotAuthKeyName = "github-copilot" | "copilot" | "copilot-chat" | "github-copilot-chat"; +/** Integration ids that can hold the Copilot OAuth login, in precedence order. */ +export const COPILOT_AUTH_KEYS = [ + "github-copilot", + "copilot", + "copilot-chat", + "github-copilot-chat", +] as const; + +type CopilotAuthKeyName = (typeof COPILOT_AUTH_KEYS)[number]; type CopilotPatTokenKind = "github_pat" | "ghp" | "ghu" | "ghs" | "other"; type EffectiveCopilotAuthSource = "pat" | "oauth" | "none"; type CopilotQuotaApi = @@ -1337,7 +1345,9 @@ export async function queryCopilotQuota( ); } if (pat.state === "absent" || !pat.config) { - const { auth } = selectCopilotAuth(options.authData ?? (await readAuthFile())); + const { auth } = selectCopilotAuth( + options.authData ?? (await readAuthFile({ integrationIds: COPILOT_AUTH_KEYS })), + ); const token = getCopilotOAuthToken(auth); if (!auth || !token) return null; @@ -1415,7 +1425,7 @@ export async function resolveCopilotAuthIdentity(): Promise { - const diagnostics = getCopilotQuotaAuthDiagnostics(await readAuthFile()); + const diagnostics = getCopilotQuotaAuthDiagnostics( + await readAuthFile({ integrationIds: COPILOT_AUTH_KEYS }), + ); return diagnostics.billingApiAccessLikely; } diff --git a/src/lib/cursor-detection.ts b/src/lib/cursor-detection.ts index 2291bfd7..813d43bd 100644 --- a/src/lib/cursor-detection.ts +++ b/src/lib/cursor-detection.ts @@ -91,7 +91,8 @@ export async function inspectCursorAuthPresence(): Promise { let invalidPath: string | undefined; let invalidError: string | undefined; - const cursorAuth = (await readAuthFile())?.cursor; + const cursorAuth = (await readAuthFile({ integrationIds: ["cursor"] }))?.cursor; + const cursorResolveError = cursorAuth?.resolveError; if (cursorAuth) { if (isValidCursorCredential(cursorAuth)) { return { @@ -104,7 +105,9 @@ export async function inspectCursorAuthPresence(): Promise { invalidPath = credentialDatabasePath; invalidError = - "Cursor credential in the OpenCode database is missing a valid OAuth token or API key"; + cursorResolveError !== undefined + ? `OpenCode could not read this login: ${cursorResolveError}` + : "Cursor credential in the OpenCode database is missing a valid OAuth token or API key"; } if (hasNonEmptyString(process.env[CURSOR_API_KEY_ENV])) { diff --git a/src/lib/deepseek-auth.ts b/src/lib/deepseek-auth.ts index d2324193..3955ab23 100644 --- a/src/lib/deepseek-auth.ts +++ b/src/lib/deepseek-auth.ts @@ -28,7 +28,7 @@ const deepseekApiKeyResolver = createProviderApiKeyResolver({ configJsoncSource: "opencode.jsonc", getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: readAuthFile, + readAuth: () => readAuthFile({ integrationIds: DEEPSEEK_PROVIDER_KEYS }), getCredentialDatabasePaths, authSource: "opencode.db", }, diff --git a/src/lib/google-agy.ts b/src/lib/google-agy.ts index df1d8bd4..92cbc926 100644 --- a/src/lib/google-agy.ts +++ b/src/lib/google-agy.ts @@ -181,7 +181,10 @@ export async function resolveAgyConfiguredProjectId(): Promise { const [auth, configuredProjectId, credentials] = await Promise.all([ - readAuthFileCached({ maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS }), + readAuthFileCached({ + maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS, + integrationIds: AGY_AUTH_KEYS, + }), resolveAgyConfiguredProjectId(), resolveAgyClientCredentials(), ]); @@ -214,7 +217,10 @@ export async function resolveGoogleAgyAuthIdentity(): Promise { const [auth, configuredProjectId] = await Promise.all([ - readAuthFileCached({ maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS }), + readAuthFileCached({ + maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS, + integrationIds: AGY_AUTH_KEYS, + }), resolveAgyConfiguredProjectId(), ]); @@ -697,7 +703,11 @@ export async function queryGoogleAgyQuota( options: { requestTimeoutMs?: number; authData?: AuthData } = {}, ): Promise { const [auth, configuredProjectId] = await Promise.all([ - options.authData ?? readAuthFileCached({ maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS }), + options.authData ?? + readAuthFileCached({ + maxAgeMs: DEFAULT_AGY_AUTH_CACHE_MAX_AGE_MS, + integrationIds: AGY_AUTH_KEYS, + }), resolveAgyConfiguredProjectId(), ]); const accounts = resolveAgyAccounts(auth, configuredProjectId); diff --git a/src/lib/google-gemini-cli.ts b/src/lib/google-gemini-cli.ts index 3edd3245..99d23a9e 100644 --- a/src/lib/google-gemini-cli.ts +++ b/src/lib/google-gemini-cli.ts @@ -29,7 +29,7 @@ import type { export const DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS = 5_000; -const GEMINI_CLI_AUTH_KEYS = [ +export const GEMINI_CLI_AUTH_KEYS = [ "google-gemini-cli", "gemini-cli", "opencode-gemini-auth", @@ -153,7 +153,7 @@ export function resolveGeminiCliAccounts( for (const sourceKey of GEMINI_CLI_AUTH_KEYS) { const entry = getAuthEntry(auth, sourceKey); - if (!entry || entry.type !== "oauth") { + if (!entry || entry.type !== "oauth" || entry.resolveError !== undefined) { continue; } @@ -207,6 +207,18 @@ function firstGeminiCliAuthKey( return GEMINI_CLI_AUTH_KEYS.find((sourceKey) => getAuthEntry(auth, sourceKey)?.type === "oauth"); } +function firstFailedGeminiCliAuthKey( + auth: AuthData | null | undefined, +): GeminiCliAuthSourceKey | undefined { + if (!auth) { + return undefined; + } + return GEMINI_CLI_AUTH_KEYS.find((sourceKey) => { + const entry = getAuthEntry(auth, sourceKey); + return entry?.type === "oauth" && entry.resolveError !== undefined; + }); +} + function getCompanionQuotaError(state: "missing" | "invalid"): string { return state === "missing" ? "Gemini CLI requires the opencode-gemini-auth plugin" @@ -232,7 +244,10 @@ export async function resolveGeminiCliConfiguredProjectId(): Promise { const [auth, configuredProjectId] = await Promise.all([ - readAuthFileCached({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS }), + readAuthFileCached({ + maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS, + integrationIds: GEMINI_CLI_AUTH_KEYS, + }), resolveGeminiCliConfiguredProjectId(), ]); const accountCount = countGeminiCliAuthEntries(auth); @@ -242,7 +257,9 @@ export async function inspectGeminiCliAuthPresence(): Promise { inspectGeminiCliCompanionPresence(), ]); - return ( - authPresence.state === "present" && - authPresence.validAccountCount > 0 && - companionPresence.state === "present" - ); + return authPresence.state === "present" && companionPresence.state === "present"; } export async function resolveGeminiCliAuthIdentity(): Promise { const [auth, configuredProjectId, credentials] = await Promise.all([ - readAuthFileCached({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS }), + readAuthFileCached({ + maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS, + integrationIds: GEMINI_CLI_AUTH_KEYS, + }), resolveGeminiCliConfiguredProjectId(), resolveGeminiCliClientCredentials(), ]); @@ -615,7 +631,11 @@ export async function queryGeminiCliQuota( options: { requestTimeoutMs?: number; authData?: AuthData } = {}, ): Promise { const [auth, configuredProjectId] = await Promise.all([ - options.authData ?? readAuthFileCached({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS }), + options.authData ?? + readAuthFileCached({ + maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS, + integrationIds: GEMINI_CLI_AUTH_KEYS, + }), resolveGeminiCliConfiguredProjectId(), ]); const accounts = resolveGeminiCliAccounts(auth, configuredProjectId); diff --git a/src/lib/kilo-config.ts b/src/lib/kilo-config.ts index 58da7ed9..38c2d842 100644 --- a/src/lib/kilo-config.ts +++ b/src/lib/kilo-config.ts @@ -9,19 +9,21 @@ export interface KiloApiKeyResult { source: KiloKeySource; } +const KILO_PROVIDER_KEYS = ["kilo"] as const; + export type KiloKeySource = "env:KILO_API_KEY" | "opencode.json" | "opencode.jsonc" | "opencode.db"; export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePaths } from "./api-key-resolver.js"; const kiloApiKeyResolver = createProviderApiKeyResolver({ envVars: [{ name: "KILO_API_KEY", source: "env:KILO_API_KEY" }], - providerKeys: ["kilo"], + providerKeys: KILO_PROVIDER_KEYS, allowedEnvVars: ["KILO_API_KEY"], configJsonSource: "opencode.json", configJsoncSource: "opencode.jsonc", getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: readAuthFile, + readAuth: () => readAuthFile({ integrationIds: KILO_PROVIDER_KEYS }), getCredentialDatabasePaths, authSource: "opencode.db", }, diff --git a/src/lib/kimi-auth.ts b/src/lib/kimi-auth.ts index dacf9463..d30fa9aa 100644 --- a/src/lib/kimi-auth.ts +++ b/src/lib/kimi-auth.ts @@ -80,7 +80,7 @@ function createKimiAuthResolver(spec: KimiAuthSpec) { authSource: "opencode.db", displayName: "Kimi", defaultMaxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, - readAuth: (maxAgeMs) => readAuthFileCached({ maxAgeMs }), + readAuth: (maxAgeMs) => readAuthFileCached({ maxAgeMs, integrationIds: spec.authKeys }), getCredentialDatabasePaths, }, }); diff --git a/src/lib/minimax-auth.ts b/src/lib/minimax-auth.ts index 42ef92a1..d395a346 100644 --- a/src/lib/minimax-auth.ts +++ b/src/lib/minimax-auth.ts @@ -239,6 +239,7 @@ async function resolveMiniMaxAuthWithSource( const maxAgeMs = Math.max(0, params?.maxAgeMs ?? DEFAULT_MINIMAX_AUTH_CACHE_MAX_AGE_MS); const authData = await readAuthFileCached({ maxAgeMs, + integrationIds: spec.authKeys, }); const auth = resolveMiniMaxAuthForSpec(authData, spec); diff --git a/src/lib/nanogpt-config.ts b/src/lib/nanogpt-config.ts index 32968bc0..63b5046c 100644 --- a/src/lib/nanogpt-config.ts +++ b/src/lib/nanogpt-config.ts @@ -32,7 +32,7 @@ const nanoGptApiKeyResolver = createProviderApiKeyResolver({ configJsoncSource: "opencode.jsonc", getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: readAuthFile, + readAuth: () => readAuthFile({ integrationIds: NANOGPT_PROVIDER_KEYS }), getCredentialDatabasePaths, authSource: "opencode.db", }, diff --git a/src/lib/ollama-cloud-config.ts b/src/lib/ollama-cloud-config.ts index e5e0e8bc..b430eae8 100644 --- a/src/lib/ollama-cloud-config.ts +++ b/src/lib/ollama-cloud-config.ts @@ -9,6 +9,8 @@ export interface OllamaCloudApiKeyResult { source: OllamaCloudKeySource; } +const OLLAMA_CLOUD_PROVIDER_KEYS = ["ollama-cloud"] as const; + export type OllamaCloudKeySource = | "env:OLLAMA_API_KEY" | "opencode.json" @@ -19,13 +21,13 @@ export { getGlobalOpencodeConfigCandidatePaths as getOpencodeConfigCandidatePath const ollamaCloudApiKeyResolver = createProviderApiKeyResolver({ envVars: [{ name: "OLLAMA_API_KEY", source: "env:OLLAMA_API_KEY" }], - providerKeys: ["ollama-cloud"], + providerKeys: OLLAMA_CLOUD_PROVIDER_KEYS, allowedEnvVars: ["OLLAMA_API_KEY"], configJsonSource: "opencode.json", configJsoncSource: "opencode.jsonc", getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: readAuthFile, + readAuth: () => readAuthFile({ integrationIds: OLLAMA_CLOUD_PROVIDER_KEYS }), getCredentialDatabasePaths, authSource: "opencode.db", }, diff --git a/src/lib/openai.ts b/src/lib/openai.ts index 0d9dacfa..e94e04ab 100644 --- a/src/lib/openai.ts +++ b/src/lib/openai.ts @@ -207,6 +207,7 @@ export type OpenAIResult = export type ResolvedOpenAIOAuth = | { state: "none" } + | { state: "failed"; sourceKey: OpenAIAuthSourceKey; error: string } | { state: "configured"; sourceKey: OpenAIAuthSourceKey; @@ -227,7 +228,7 @@ function getOpenAIOAuthEntry( } const accessToken = typeof entry.access === "string" ? entry.access.trim() : ""; - if (accessToken) { + if (accessToken || entry.resolveError !== undefined) { return { sourceKey, entry, accessToken }; } } @@ -240,6 +241,9 @@ export function resolveOpenAIOAuth(auth: AuthData | null | undefined): ResolvedO if (!resolved) { return { state: "none" }; } + if (resolved.entry.resolveError !== undefined) { + return { state: "failed", sourceKey: resolved.sourceKey, error: resolved.entry.resolveError }; + } const email = getEmailFromJwt(resolved.accessToken) ?? undefined; const accountId = @@ -260,7 +264,7 @@ export function resolveOpenAIOAuth(auth: AuthData | null | undefined): ResolvedO } export function hasOpenAIOAuth(auth: AuthData | null | undefined): boolean { - return resolveOpenAIOAuth(auth).state === "configured"; + return resolveOpenAIOAuth(auth).state !== "none"; } export async function resolveOpenAIAuthIdentity(params?: { @@ -268,6 +272,7 @@ export async function resolveOpenAIAuthIdentity(params?: { }): Promise { const auth = await readAuthFileCached({ maxAgeMs: Math.max(0, params?.maxAgeMs ?? DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS), + integrationIds: OPENAI_AUTH_SOURCE_KEYS, }); const resolved = resolveOpenAIOAuth(auth); if (resolved.state !== "configured") return null; @@ -288,6 +293,7 @@ export async function resolveOpenAIAuthIdentity(params?: { export async function hasOpenAIOAuthCached(params?: { maxAgeMs?: number }): Promise { const auth = await readAuthFileCached({ maxAgeMs: Math.max(0, params?.maxAgeMs ?? DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS), + integrationIds: OPENAI_AUTH_SOURCE_KEYS, }); return hasOpenAIOAuth(auth); } @@ -298,8 +304,17 @@ export async function queryOpenAIQuota( const resolvedAuth = options.auth ?? resolveOpenAIOAuth( - await readAuthFileCached({ maxAgeMs: DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS }), + await readAuthFileCached({ + maxAgeMs: DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS, + integrationIds: OPENAI_AUTH_SOURCE_KEYS, + }), ); + if (resolvedAuth.state === "failed") { + return { + success: false, + error: `OpenAI sign-in could not be refreshed: ${resolvedAuth.error}. Run \`opencode auth login openai\`.`, + }; + } if (resolvedAuth.state !== "configured") return null; if (resolvedAuth.expiresAt && resolvedAuth.expiresAt < Date.now()) { diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index ad05a0a4..dfad89bf 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -12,8 +12,6 @@ import { openOpenCodeSqliteReadOnly, type SqliteConn } from "./opencode-sqlite.j import type { AuthData } from "./types.js"; -const DEFAULT_AUTH_CACHE_MAX_AGE_MS = 5_000; - type AuthCacheEntry = { timestamp: number; value: AuthData | null; @@ -26,6 +24,23 @@ export type CredentialRow = { label: string; active: boolean; value: Record; + /** + * Why OpenCode could not return this login (for example a failed token + * refresh). A failed row keeps its id, label and position, but its `value` + * is only `{ type: "api" | "oauth" }`; consumers show `resolveError` as a + * per-account error. The SQLite reader never sets it. + */ + resolveError?: string; +}; + +/** OpenCode 2 connection methods: a stored API key or an OAuth sign-in. */ +export type CredentialMethod = "key" | "oauth"; + +export type ReadCredentialRowsOptions = { + /** Keep only rows whose stored credential uses one of these methods. */ + methods?: readonly CredentialMethod[]; + /** Keep only the first (active) row of each integration id. */ + firstOnly?: boolean; }; /** @@ -64,7 +79,8 @@ export function formatCredentialDisplayNames( }); } -let authCache: AuthCacheEntry | null = null; +/** Cached auth maps keyed by the sorted, joined integration id list. */ +const authCache = new Map(); /** * OpenCode credential database paths: the one resolved database, or none when @@ -75,18 +91,61 @@ export function getCredentialDatabasePaths(): string[] { return path === ":memory:" ? [] : [path]; } -export async function readAuthFile(): Promise { - const rows = await readCredentialRows(); +/** + * The auth entry resolvers read for a row: its value, plus `resolveError` + * when OpenCode could not return the login. + */ +export function credentialRowAuthEntry(row: CredentialRow): Record { + return row.resolveError === undefined + ? row.value + : { ...row.value, resolveError: row.resolveError }; +} + +/** Map of the first (active) login per requested integration id. */ +export async function readAuthFile(params: { + integrationIds: readonly string[]; +}): Promise { + const rows = await readCredentialRows(params.integrationIds, { firstOnly: true }); const auth: Record = {}; for (const row of rows) { - if (!(row.integrationId in auth)) auth[row.integrationId] = row.value; + if (!(row.integrationId in auth)) auth[row.integrationId] = credentialRowAuthEntry(row); } return Object.keys(auth).length > 0 ? (auth as AuthData) : null; } -export async function readCredentialRows(): Promise { +/** The OpenCode 2 method of a parsed credential value (the reader maps a stored `key` to `api`). */ +function credentialMethod(value: Record): CredentialMethod | undefined { + if (value.type === "oauth") return "oauth"; + if (value.type === "api" || value.type === "key") return "key"; + return undefined; +} + +/** + * Credential rows of the requested integration ids, in database order + * (active first, then most recently updated). + */ +export async function readCredentialRows( + integrationIds: readonly string[], + options: ReadCredentialRowsOptions = {}, +): Promise { const [path] = getCredentialDatabasePaths(); - return path ? readCredentialDatabase(path) : []; + const rows = (path ? await readCredentialDatabase(path) : []).filter((row) => + integrationIds.includes(row.integrationId), + ); + const seenIntegrationIds = new Set(); + const firstRows = options.firstOnly + ? rows.filter((row) => { + if (seenIntegrationIds.has(row.integrationId)) return false; + seenIntegrationIds.add(row.integrationId); + return true; + }) + : rows; + const methods = options.methods; + if (!methods) return firstRows; + return firstRows.filter((row) => { + const method = credentialMethod(row.value); + return method !== undefined && methods.includes(method); + }); } function canonicalCredentialValueKey(value: unknown): string { @@ -193,40 +252,46 @@ function parseCredentialValue(value: string): Record | null { * Cached auth reader for frequently triggered code paths (e.g. per-question hooks). * This avoids repeated filesystem reads while keeping auth updates visible quickly. */ -export async function readAuthFileCached(params?: { maxAgeMs?: number }): Promise { - const maxAgeMs = Math.max(0, params?.maxAgeMs ?? DEFAULT_AUTH_CACHE_MAX_AGE_MS); +export async function readAuthFileCached(params: { + maxAgeMs: number; + integrationIds: readonly string[]; +}): Promise { + const maxAgeMs = Math.max(0, params.maxAgeMs); + const cacheKey = [...params.integrationIds].sort().join(","); + const cached = authCache.get(cacheKey); const now = Date.now(); - if (authCache && now - authCache.timestamp <= maxAgeMs) { - return authCache.value; + if (cached && now - cached.timestamp <= maxAgeMs) { + return cached.value; } - if (authCache?.inFlight) { - return authCache.inFlight; + if (cached?.inFlight) { + return cached.inFlight; } const inFlight = (async () => { - const value = await readAuthFile(); - authCache = { timestamp: Date.now(), value }; + const value = await readAuthFile({ integrationIds: params.integrationIds }); + authCache.set(cacheKey, { timestamp: Date.now(), value }); return value; })(); - authCache = { - timestamp: authCache?.timestamp ?? 0, - value: authCache?.value ?? null, + authCache.set(cacheKey, { + timestamp: cached?.timestamp ?? 0, + value: cached?.value ?? null, inFlight, - }; + }); try { return await inFlight; } finally { - if (authCache?.inFlight === inFlight) { - authCache.inFlight = undefined; + const entry = authCache.get(cacheKey); + if (entry?.inFlight === inFlight) { + entry.inFlight = undefined; } } } /** Test helper to clear cached auth state between test cases. */ export function clearReadAuthFileCacheForTests(): void { - authCache = null; + authCache.clear(); } diff --git a/src/lib/opencode-console-auth.ts b/src/lib/opencode-console-auth.ts index 2ca8c424..87aa5c81 100644 --- a/src/lib/opencode-console-auth.ts +++ b/src/lib/opencode-console-auth.ts @@ -36,15 +36,15 @@ export type OpenCodeConsoleAuthState = export async function resolveOpenCodeConsoleAuth(params?: { nowMs?: number; }): Promise { - const rows = await readCredentialRows(); - const candidates = rows - .filter((row) => row.integrationId === OPENCODE_CONSOLE_INTEGRATION_ID) - .filter((row) => (row.value as Record | null)?.type === "oauth"); + // Only the active Console login counts, the way OpenCode itself picks it. + const rows = await readCredentialRows([OPENCODE_CONSOLE_INTEGRATION_ID], { firstOnly: true }); + const row = rows.find((row) => row.integrationId === OPENCODE_CONSOLE_INTEGRATION_ID); + if (!row) return { state: "none" }; + if (row.resolveError !== undefined) return { state: "invalid", error: row.resolveError }; - if (candidates.length === 0) return { state: "none" }; + const value = row.value; + if (value.type !== "oauth") return { state: "none" }; - const row = candidates[0]!; - const value = row.value as Record; const accessToken = typeof value.access === "string" ? value.access.trim() : ""; if (!accessToken) { return { state: "invalid", error: "OpenCode Console credential has no access token" }; diff --git a/src/lib/opencode-go-auth.ts b/src/lib/opencode-go-auth.ts index 6000aa49..f1a2762c 100644 --- a/src/lib/opencode-go-auth.ts +++ b/src/lib/opencode-go-auth.ts @@ -43,7 +43,7 @@ const openCodeGoAuthResolver = createProviderApiKeyResolver readAuthFileCached({ maxAgeMs }), + readAuth: (maxAgeMs) => readAuthFileCached({ maxAgeMs, integrationIds: OPENCODE_GO_AUTH_KEYS }), getCredentialDatabasePaths, }, }); diff --git a/src/lib/quota-providers-remote.ts b/src/lib/quota-providers-remote.ts index 97e02bb4..dbd17d78 100644 --- a/src/lib/quota-providers-remote.ts +++ b/src/lib/quota-providers-remote.ts @@ -103,7 +103,7 @@ export async function resolveQuotaProviderApiKey( configJsoncSource: "opencode.jsonc", getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: readAuthFile, + readAuth: () => readAuthFile({ integrationIds: [source.providerId] }), authKeys: [source.providerId], authSource: "opencode.db", }, diff --git a/src/lib/synthetic-config.ts b/src/lib/synthetic-config.ts index 29051410..cfbaf754 100644 --- a/src/lib/synthetic-config.ts +++ b/src/lib/synthetic-config.ts @@ -28,7 +28,7 @@ const syntheticApiKeyResolver = createProviderApiKeyResolver configJsoncSource: "opencode.jsonc", getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: readAuthFile, + readAuth: () => readAuthFile({ integrationIds: SYNTHETIC_PROVIDER_KEYS }), getCredentialDatabasePaths, authSource: "opencode.db", }, diff --git a/src/lib/types.ts b/src/lib/types.ts index 508de307..ea9ff2fc 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -314,6 +314,8 @@ export interface CursorAuthData { refresh?: string; expires?: number; key?: string; + /** Set when OpenCode could not return this login; see `CredentialRow.resolveError`. */ + resolveError?: string; [key: string]: unknown; } @@ -331,6 +333,8 @@ export interface OpenAIOAuthData { refresh?: string; expires?: number; accountId?: string; + /** Set when OpenCode could not return this login; see `CredentialRow.resolveError`. */ + resolveError?: string; [key: string]: unknown; } @@ -339,6 +343,8 @@ export interface XaiOAuthData { access?: string; refresh?: string; expires?: number; + /** Set when OpenCode could not return this login; see `CredentialRow.resolveError`. */ + resolveError?: string; [key: string]: unknown; } @@ -355,6 +361,8 @@ export interface GeminiCliOAuthAuthData { email?: string; accountEmail?: string; login?: string; + /** Set when OpenCode could not return this login; see `CredentialRow.resolveError`. */ + resolveError?: string; [key: string]: unknown; } diff --git a/src/lib/xai.ts b/src/lib/xai.ts index a0ace1c5..f61f97f6 100644 --- a/src/lib/xai.ts +++ b/src/lib/xai.ts @@ -49,6 +49,7 @@ export type XaiResult = export type ResolvedXaiOAuth = | { state: "none" } + | { state: "failed"; error: string } | { state: "configured"; accessToken: string; @@ -95,6 +96,7 @@ export function periodKindLabel(kind: XaiPeriodKind): string { export function resolveXaiOAuth(auth: AuthData | null | undefined): ResolvedXaiOAuth { const entry = auth?.xai; if (!entry || entry.type !== "oauth") return { state: "none" }; + if (entry.resolveError !== undefined) return { state: "failed", error: entry.resolveError }; const accessToken = typeof entry.access === "string" ? entry.access.trim() : ""; if (!accessToken) return { state: "none" }; @@ -110,18 +112,19 @@ export function resolveXaiOAuth(auth: AuthData | null | undefined): ResolvedXaiO } export function hasXaiOAuth(auth: AuthData | null | undefined): boolean { - return resolveXaiOAuth(auth).state === "configured"; + return resolveXaiOAuth(auth).state !== "none"; } export async function hasXaiOAuthCached(params?: { maxAgeMs?: number }): Promise { const auth = await readAuthFileCached({ maxAgeMs: Math.max(0, params?.maxAgeMs ?? DEFAULT_XAI_AUTH_CACHE_MAX_AGE_MS), + integrationIds: ["xai"], }); return hasXaiOAuth(auth); } export async function resolveXaiAuthIdentity(): Promise { - const resolved = resolveXaiOAuth(await readAuthFile()); + const resolved = resolveXaiOAuth(await readAuthFile({ integrationIds: ["xai"] })); if (resolved.state !== "configured") return null; return deriveResolvedAuthIdentity({ providerId: "xai", @@ -260,7 +263,14 @@ export async function queryXaiQuota( // OpenCode can replace this OAuth entry while servicing a model request. // Read the file directly so a post-request quota fetch cannot reuse the // token snapshot from before that refresh. - const resolvedAuth = options.auth ?? resolveXaiOAuth(await readAuthFile()); + const resolvedAuth = + options.auth ?? resolveXaiOAuth(await readAuthFile({ integrationIds: ["xai"] })); + if (resolvedAuth.state === "failed") { + return { + success: false, + error: `xAI sign-in could not be refreshed: ${resolvedAuth.error}. Run \`opencode auth login xai\`.`, + }; + } if (resolvedAuth.state !== "configured") return null; if (resolvedAuth.expiresAt !== undefined && resolvedAuth.expiresAt <= Date.now()) { diff --git a/src/lib/zai-auth.ts b/src/lib/zai-auth.ts index 1d9f1184..c47d1e72 100644 --- a/src/lib/zai-auth.ts +++ b/src/lib/zai-auth.ts @@ -39,7 +39,7 @@ const zaiAuthResolver = createProviderApiKeyResolver readAuthFileCached({ maxAgeMs }), + readAuth: (maxAgeMs) => readAuthFileCached({ maxAgeMs, integrationIds: ZAI_AUTH_KEYS }), getCredentialDatabasePaths, }, }); diff --git a/src/lib/zhipu-auth.ts b/src/lib/zhipu-auth.ts index 435860db..cfcb991a 100644 --- a/src/lib/zhipu-auth.ts +++ b/src/lib/zhipu-auth.ts @@ -44,7 +44,7 @@ const zhipuAuthResolver = createProviderApiKeyResolver readAuthFileCached({ maxAgeMs }), + readAuth: (maxAgeMs) => readAuthFileCached({ maxAgeMs, integrationIds: ZHIPU_AUTH_KEYS }), getCredentialDatabasePaths, }, }); diff --git a/src/providers/anthropic.ts b/src/providers/anthropic.ts index 2278a5db..a41a0362 100644 --- a/src/providers/anthropic.ts +++ b/src/providers/anthropic.ts @@ -62,7 +62,9 @@ export const anthropicProvider: QuotaProvider = { }; let statusDetails; let acquisitionMethod: QuotaToastEntry["accounting"]["acquisitionMethod"] = "local_cli"; - const databaseCredentials = (await readCredentialRows()).flatMap((row) => { + const databaseCredentials = ( + await readCredentialRows(["anthropic"], { methods: ["oauth"] }) + ).flatMap((row) => { if (row.integrationId !== "anthropic") return []; const auth = resolveAnthropicOAuth({ anthropic: row.value } as AuthData); return auth.state === "configured" ? [{ row, auth }] : []; diff --git a/src/providers/copilot.ts b/src/providers/copilot.ts index 004cc1c7..134216de 100644 --- a/src/providers/copilot.ts +++ b/src/providers/copilot.ts @@ -6,6 +6,7 @@ */ import { + COPILOT_AUTH_KEYS, getCopilotQuotaAuthDiagnostics, hasCopilotQuotaRuntimeAvailable, queryCopilotQuota, @@ -209,7 +210,9 @@ function personalEntries(result: CopilotQuotaResult): QuotaToastEntry[] { } async function getCopilotStatusDetails() { - const diagnostics = getCopilotQuotaAuthDiagnostics(await readAuthFileCached({ maxAgeMs: 5_000 })); + const diagnostics = getCopilotQuotaAuthDiagnostics( + await readAuthFileCached({ maxAgeMs: 5_000, integrationIds: COPILOT_AUTH_KEYS }), + ); const managedBilling = diagnostics.billingMode === "organization_usage" || diagnostics.billingMode === "enterprise_usage"; @@ -298,9 +301,14 @@ export const copilotProvider: QuotaProvider = { async fetch(ctx: QuotaProviderContext): Promise { const statusDetails = await getCopilotStatusDetails(); - const authData = await readAuthFileCached({ maxAgeMs: 5_000 }); + const authData = await readAuthFileCached({ + maxAgeMs: 5_000, + integrationIds: COPILOT_AUTH_KEYS, + }); if (getCopilotQuotaAuthDiagnostics(authData).effectiveSource === "oauth") { - const credentials = (await readCredentialRows()).filter((row) => + const credentials = ( + await readCredentialRows(COPILOT_AUTH_KEYS, { methods: ["oauth"] }) + ).filter((row) => ["github-copilot", "copilot", "copilot-chat", "github-copilot-chat"].includes( row.integrationId, ), diff --git a/src/providers/glm-coding-plan-provider.ts b/src/providers/glm-coding-plan-provider.ts index 0eb8b73f..e6593297 100644 --- a/src/providers/glm-coding-plan-provider.ts +++ b/src/providers/glm-coding-plan-provider.ts @@ -1,5 +1,9 @@ import type { QuotaProvider, QuotaProviderContext } from "../lib/entries.js"; -import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; +import { + credentialRowAuthEntry, + formatCredentialDisplayNames, + readCredentialRows, +} from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import type { CanonicalQuotaProviderId } from "../lib/provider-metadata.js"; import type { AuthData } from "../lib/types.js"; @@ -74,19 +78,27 @@ export function createGlmCodingPlanProvider(params: { const diagnostics = await params.getAuthDiagnostics({ maxAgeMs: params.authCacheMaxAgeMs }); const authDetails = apiKeyStatusDetails(diagnostics); if (diagnostics.source === "opencode.db") { - const credentials = (await readCredentialRows()).flatMap((row) => { + const credentials = ( + await readCredentialRows(params.credentialIntegrationIds, { methods: ["key"] }) + ).flatMap((row) => { if (!params.credentialIntegrationIds.includes(row.integrationId)) return []; - const auth = params.resolveCredentialAuth({ [row.integrationId]: row.value } as AuthData); - return auth.state === "configured" ? [{ row, auth }] : []; + const auth = params.resolveCredentialAuth({ + [row.integrationId]: credentialRowAuthEntry(row), + } as AuthData); + // An invalid login stays in the list so it shows as its own error row. + return auth.state === "none" ? [] : [{ row, auth }]; }); if (credentials.length > 0) { const results = await Promise.all( credentials.map(async ({ row, auth }) => ({ row, - result: await params.queryQuota({ - requestTimeoutMs: ctx.config?.requestTimeoutMs, - apiKey: auth.apiKey, - }), + result: + auth.state === "invalid" + ? { success: false as const, error: auth.error } + : await params.queryQuota({ + requestTimeoutMs: ctx.config?.requestTimeoutMs, + apiKey: auth.apiKey, + }), })), ); const names = formatCredentialDisplayNames( diff --git a/src/providers/google-agy.ts b/src/providers/google-agy.ts index edab3d64..b22f3c07 100644 --- a/src/providers/google-agy.ts +++ b/src/providers/google-agy.ts @@ -6,6 +6,7 @@ import type { QuotaToastEntry, } from "../lib/entries.js"; import { + AGY_AUTH_KEYS, hasAgyQuotaRuntimeAvailable, inspectAgyAuthPresence, queryGoogleAgyQuota, @@ -138,8 +139,8 @@ export const googleAgyProvider: QuotaProvider = { companion_error: companion.state !== "present" ? sanitizeDisplayText(companion.error) : undefined, }); - const credentialRows = (await readCredentialRows()).filter((row) => - ["google-agy", "opencode-agy-auth", "google-agy-auth"].includes(row.integrationId), + const credentialRows = (await readCredentialRows(AGY_AUTH_KEYS, { methods: ["oauth"] })).filter( + (row) => ["google-agy", "opencode-agy-auth", "google-agy-auth"].includes(row.integrationId), ); if (credentialRows.length > 0) { const results = await Promise.all( diff --git a/src/providers/google-gemini-cli.ts b/src/providers/google-gemini-cli.ts index 6c62a40b..947f0abb 100644 --- a/src/providers/google-gemini-cli.ts +++ b/src/providers/google-gemini-cli.ts @@ -6,6 +6,7 @@ import type { QuotaToastEntry, } from "../lib/entries.js"; import { + GEMINI_CLI_AUTH_KEYS, hasGeminiCliQuotaRuntimeAvailable, inspectGeminiCliAuthPresence, queryGeminiCliQuota, @@ -13,7 +14,7 @@ import { import { inspectGeminiCliCompanionPresence } from "../lib/google-gemini-cli-companion.js"; import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; import { parseProviderModelRef } from "../lib/provider-model-matching.js"; -import type { AuthData } from "../lib/types.js"; +import type { AuthData, GeminiCliResult } from "../lib/types.js"; import { createGoogleAccountLabelMap, formatGoogleAccountErrors, @@ -75,7 +76,9 @@ export const googleGeminiCliProvider: QuotaProvider = { companion_error: companion.state !== "present" ? sanitizeDisplayText(companion.error) : undefined, }); - const credentialRows = (await readCredentialRows()).filter((row) => + const credentialRows = ( + await readCredentialRows(GEMINI_CLI_AUTH_KEYS, { methods: ["oauth"] }) + ).filter((row) => ["google-gemini-cli", "gemini-cli", "opencode-gemini-auth", "gemini", "google"].includes( row.integrationId, ), @@ -84,12 +87,18 @@ export const googleGeminiCliProvider: QuotaProvider = { const results = await Promise.all( credentialRows.map(async (row) => ({ row, - result: await queryGeminiCliQuota({ - requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured - ? ctx.config.requestTimeoutMs - : undefined, - authData: { [row.integrationId]: row.value } as AuthData, - }), + result: + row.resolveError !== undefined + ? ({ + success: false, + error: `Gemini CLI sign-in could not be refreshed: ${row.resolveError}. Run \`opencode auth login google\`.`, + } satisfies GeminiCliResult) + : await queryGeminiCliQuota({ + requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured + ? ctx.config.requestTimeoutMs + : undefined, + authData: { [row.integrationId]: row.value } as AuthData, + }), })), ); const names = formatCredentialDisplayNames( diff --git a/src/providers/kimi-code.ts b/src/providers/kimi-code.ts index 8f8d2c9f..2dc4e3f4 100644 --- a/src/providers/kimi-code.ts +++ b/src/providers/kimi-code.ts @@ -17,7 +17,11 @@ import { resolveKimiGlobalAuthWithDiagnosticsCached, } from "../lib/kimi-auth.js"; import { getKimiQuotaEndpoint, type KimiQuotaEndpointId } from "../lib/kimi-endpoints.js"; -import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; +import { + credentialRowAuthEntry, + formatCredentialDisplayNames, + readCredentialRows, +} from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import { normalizeQuotaProviderId } from "../lib/provider-metadata.js"; import type { AuthData } from "../lib/types.js"; @@ -94,9 +98,9 @@ function createKimiProvider(spec: KimiProviderSpec): QuotaProvider { } if (diagnostics.source === "opencode.db") { - const credentialRows = (await readCredentialRows()).filter((row) => - spec.integrationIds.includes(row.integrationId), - ); + const credentialRows = ( + await readCredentialRows(spec.integrationIds, { methods: ["key"] }) + ).filter((row) => spec.integrationIds.includes(row.integrationId)); const rowNames = formatCredentialDisplayNames( spec.label, credentialRows.map((row) => ({ row, fallbackName: spec.label })), @@ -106,7 +110,9 @@ function createKimiProvider(spec: KimiProviderSpec): QuotaProvider { ); const invalidErrors: QuotaProviderResult["errors"] = []; const credentials = credentialRows.flatMap((row) => { - const rowAuth = spec.parseAuth({ [row.integrationId]: row.value } as AuthData); + const rowAuth = spec.parseAuth({ + [row.integrationId]: credentialRowAuthEntry(row), + } as AuthData); if (rowAuth.state === "invalid") { invalidErrors.push({ label: displayNamesByRowId.get(row.id) ?? spec.label, diff --git a/src/providers/minimax-coding-plan.ts b/src/providers/minimax-coding-plan.ts index 202f5986..33f15c0a 100644 --- a/src/providers/minimax-coding-plan.ts +++ b/src/providers/minimax-coding-plan.ts @@ -32,7 +32,11 @@ import { resolveMiniMaxChinaAuthCached, } from "../lib/minimax-auth.js"; import { getMiniMaxQuotaEndpoint, type MiniMaxQuotaEndpointId } from "../lib/minimax-endpoints.js"; -import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; +import { + credentialRowAuthEntry, + formatCredentialDisplayNames, + readCredentialRows, +} from "../lib/opencode-auth.js"; import { isAnyProviderIdAvailable, isCanonicalProviderAvailable, @@ -460,9 +464,9 @@ function createMiniMaxProvider(spec: MiniMaxProviderSpec): QuotaProvider { } if (diagnostics.source === "opencode.db") { - const credentialRows = (await readCredentialRows()).filter((row) => - spec.credentialIntegrationIds.includes(row.integrationId), - ); + const credentialRows = ( + await readCredentialRows(spec.credentialIntegrationIds, { methods: ["key"] }) + ).filter((row) => spec.credentialIntegrationIds.includes(row.integrationId)); const rowNames = formatCredentialDisplayNames( spec.label, credentialRows.map((row) => ({ row, fallbackName: spec.label })), @@ -473,7 +477,7 @@ function createMiniMaxProvider(spec: MiniMaxProviderSpec): QuotaProvider { const invalidErrors: QuotaProviderResult["errors"] = []; const credentials = credentialRows.flatMap((row) => { const rowAuth = spec.resolveCredentialAuth({ - [row.integrationId]: row.value, + [row.integrationId]: credentialRowAuthEntry(row), } as AuthData); if (rowAuth.state === "invalid") { invalidErrors.push({ diff --git a/src/providers/openai.ts b/src/providers/openai.ts index e589455d..16884ceb 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -10,7 +10,11 @@ import { queryOpenAIQuota, resolveOpenAIOAuth, } from "../lib/openai.js"; -import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; +import { + credentialRowAuthEntry, + formatCredentialDisplayNames, + readCredentialRows, +} from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import { modelProviderIncludesAny } from "../lib/provider-model-matching.js"; import type { AuthData } from "../lib/types.js"; @@ -45,12 +49,15 @@ export const openaiProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { - const rows = (await readCredentialRows()).filter((row) => - (OPENAI_AUTH_SOURCE_KEYS as readonly string[]).includes(row.integrationId), + const rows = (await readCredentialRows(OPENAI_AUTH_SOURCE_KEYS, { methods: ["oauth"] })).filter( + (row) => (OPENAI_AUTH_SOURCE_KEYS as readonly string[]).includes(row.integrationId), ); + // A failed login stays in the list so it shows as its own error row. const credentials = rows.flatMap((row) => { - const auth = resolveOpenAIOAuth({ [row.integrationId]: row.value } as AuthData); - return auth.state === "configured" ? [{ row, auth }] : []; + const auth = resolveOpenAIOAuth({ + [row.integrationId]: credentialRowAuthEntry(row), + } as AuthData); + return auth.state === "none" ? [] : [{ row, auth }]; }); const entries: QuotaProviderResult["entries"] = []; const errors: QuotaProviderResult["errors"] = []; @@ -110,7 +117,7 @@ export const openaiProvider: QuotaProvider = { : mapResult(await queryOpenAIQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs })); const configuredAuth = credentials[0]?.auth; const configured = configuredAuth !== undefined; - const expiresAt = configuredAuth?.expiresAt; + const expiresAt = configuredAuth?.state === "configured" ? configuredAuth.expiresAt : undefined; return withStatusDetails( providerResult, statusDetailsFromRecord({ @@ -118,9 +125,11 @@ export const openaiProvider: QuotaProvider = { auth_source: configuredAuth?.sourceKey ?? "(none)", token_status: !configured ? "(none)" - : expiresAt && expiresAt < Date.now() - ? "expired" - : "valid", + : configuredAuth?.state === "failed" + ? "failed" + : expiresAt && expiresAt < Date.now() + ? "expired" + : "valid", token_expires_at: expiresAt ? new Date(expiresAt).toISOString() : "(none)", }), ); diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index ab2954ee..1aaa883a 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -8,6 +8,7 @@ import type { QuotaToastEntry, } from "../lib/entries.js"; import { + credentialRowAuthEntry, formatCredentialDisplayNames, readCredentialRows, selectConnectionCredentialRows, @@ -221,7 +222,7 @@ async function fetchOpenCodeGoLegacy( // holding the same credential (e.g. Zen + Go sharing a workspace key). // Console OAuth credentials cannot authorize this API and are skipped. const credentialRows = selectConnectionCredentialRows( - (await readCredentialRows()) + (await readCredentialRows(OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS, { methods: ["key"] })) .filter((row) => OPENCODE_GO_CREDENTIAL_INTEGRATION_IDS.includes(row.integrationId)) .filter((row) => row.value.type !== "oauth"), "opencode-go", @@ -235,7 +236,7 @@ async function fetchOpenCodeGoLegacy( ); const invalidErrors: QuotaProviderResult["errors"] = []; const credentials = credentialRows.flatMap((row) => { - const rowAuth = resolveOpenCodeGoAuth({ [row.integrationId]: row.value }); + const rowAuth = resolveOpenCodeGoAuth({ [row.integrationId]: credentialRowAuthEntry(row) }); if (rowAuth.state === "invalid") { invalidErrors.push({ label: displayNamesByRowId.get(row.id) ?? OPENCODE_GO_PROVIDER_LABEL, diff --git a/src/providers/xai.ts b/src/providers/xai.ts index ee045c75..d11370cd 100644 --- a/src/providers/xai.ts +++ b/src/providers/xai.ts @@ -8,7 +8,11 @@ import type { QuotaProviderMatchContext, QuotaProviderResult, } from "../lib/entries.js"; -import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; +import { + credentialRowAuthEntry, + formatCredentialDisplayNames, + readCredentialRows, +} from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import { modelProviderMatchesRuntimeId } from "../lib/provider-model-matching.js"; import type { AuthData } from "../lib/types.js"; @@ -43,11 +47,14 @@ export const xaiProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { - const credentials = (await readCredentialRows()).flatMap((row) => { - if (row.integrationId !== "xai") return []; - const auth = resolveXaiOAuth({ xai: row.value } as AuthData); - return auth.state === "configured" ? [{ row, auth }] : []; - }); + // A failed login stays in the list so it shows as its own error row. + const credentials = (await readCredentialRows(["xai"], { methods: ["oauth"] })).flatMap( + (row) => { + if (row.integrationId !== "xai") return []; + const auth = resolveXaiOAuth({ xai: credentialRowAuthEntry(row) } as AuthData); + return auth.state === "none" ? [] : [{ row, auth }]; + }, + ); if (credentials.length > 0) { const results = await Promise.all( credentials.map(async ({ row, auth }) => ({ diff --git a/tests/lib.alibaba-auth.test.ts b/tests/lib.alibaba-auth.test.ts index 905f158c..eeb7e6b4 100644 --- a/tests/lib.alibaba-auth.test.ts +++ b/tests/lib.alibaba-auth.test.ts @@ -273,6 +273,7 @@ describe("alibaba auth resolution", () => { }); expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS, + integrationIds: ["alibaba-coding-plan", "alibaba"], }); }); @@ -303,7 +304,10 @@ describe("alibaba auth resolution", () => { mocks.readAuthFileCached.mockResolvedValueOnce({}); await resolveAlibabaCodingPlanAuthCached({ maxAgeMs: -100 }); - expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: 0 }); + expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ + maxAgeMs: 0, + integrationIds: ["alibaba-coding-plan", "alibaba"], + }); }); }); diff --git a/tests/lib.anthropic-auth.test.ts b/tests/lib.anthropic-auth.test.ts index b97f71f5..0a62c8b9 100644 --- a/tests/lib.anthropic-auth.test.ts +++ b/tests/lib.anthropic-auth.test.ts @@ -83,6 +83,7 @@ describe("anthropic auth resolution", () => { }); expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: DEFAULT_ANTHROPIC_AUTH_CACHE_MAX_AGE_MS, + integrationIds: ["anthropic"], }); }); @@ -90,6 +91,9 @@ describe("anthropic auth resolution", () => { mocks.readAuthFileCached.mockResolvedValueOnce(null); await expect(resolveAnthropicOAuthCached({ maxAgeMs: -1 })).resolves.toEqual({ state: "none" }); - expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: 0 }); + expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ + maxAgeMs: 0, + integrationIds: ["anthropic"], + }); }); }); diff --git a/tests/lib.api-key-resolver.test.ts b/tests/lib.api-key-resolver.test.ts index 4a86c802..05405399 100644 --- a/tests/lib.api-key-resolver.test.ts +++ b/tests/lib.api-key-resolver.test.ts @@ -270,5 +270,15 @@ describe("api-key-resolver", () => { ["provider"], ), ).toBeNull(); + + // A login OpenCode could not return carries no key, so strict resolvers see nothing. + expect( + extractAuthApiKeyEntry( + { + provider: { type: "api", resolveError: "refresh_failed: HTTP 400" }, + }, + ["provider"], + ), + ).toBeNull(); }); }); diff --git a/tests/lib.cursor-detection.test.ts b/tests/lib.cursor-detection.test.ts index 4a448ff4..09dd49d7 100644 --- a/tests/lib.cursor-detection.test.ts +++ b/tests/lib.cursor-detection.test.ts @@ -112,6 +112,20 @@ describe("cursor detection", () => { ); }); + it("reports a Cursor login OpenCode could not return as invalid with its reason", async () => { + mockFiles.set(testPaths.credentialDatabase, ""); + mockAuth.value = { cursor: { type: "oauth", resolveError: "refresh_failed: HTTP 401" } }; + + const { inspectCursorAuthPresence } = await import("../src/lib/cursor-detection.js"); + const { readAuthFile } = await import("../src/lib/opencode-auth.js"); + const result = await inspectCursorAuthPresence(); + + expect(result.state).toBe("invalid"); + expect(result.selectedPath).toBe(testPaths.credentialDatabase); + expect(result.error).toBe("OpenCode could not read this login: refresh_failed: HTTP 401"); + expect(readAuthFile).toHaveBeenCalledWith({ integrationIds: ["cursor"] }); + }); + it("accepts a Cursor API key credential in the OpenCode database", async () => { mockFiles.set(testPaths.credentialDatabase, ""); // The credential reader maps OpenCode 2's stored `{ type: "key", key }` to `type: "api"`. diff --git a/tests/lib.google-gemini-cli.test.ts b/tests/lib.google-gemini-cli.test.ts index b85ebc9d..43a774c6 100644 --- a/tests/lib.google-gemini-cli.test.ts +++ b/tests/lib.google-gemini-cli.test.ts @@ -64,6 +64,7 @@ vi.mock("../src/lib/resolved-auth-identity.js", () => ({ import { DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS, + hasGeminiCliQuotaRuntimeAvailable, inspectGeminiCliAuthPresence, parseGeminiCliRefreshParts, queryGeminiCliQuota, @@ -322,6 +323,61 @@ describe("gemini cli auth resolution", () => { }); expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: DEFAULT_GEMINI_CLI_AUTH_CACHE_MAX_AGE_MS, + integrationIds: [ + "google-gemini-cli", + "gemini-cli", + "opencode-gemini-auth", + "gemini", + "google", + ], + }); + }); + + it("keeps a login OpenCode could not return present without making it an account", async () => { + mocks.readAuthFileCached.mockResolvedValue({ + "google-gemini-cli": { type: "oauth", resolveError: "refresh_failed: HTTP 400" }, + }); + mocks.inspectGeminiCliCompanionPresence.mockResolvedValue({ + state: "present", + resolvedPath: "/plugin", + }); + + await expect(inspectGeminiCliAuthPresence()).resolves.toEqual({ + state: "present", + sourceKey: "google-gemini-cli", + accountCount: 1, + validAccountCount: 0, + }); + await expect(hasGeminiCliQuotaRuntimeAvailable()).resolves.toBe(true); + await expect(queryGeminiCliQuota()).resolves.toBeNull(); + expect(mocks.fetchResponse).not.toHaveBeenCalled(); + }); + + it("skips a failed login when resolving accounts and counts it next to valid ones", async () => { + const auth = { + "google-gemini-cli": { type: "oauth", resolveError: "refresh_failed: HTTP 400" }, + google: { type: "oauth", refresh: "refresh-token|project-id" }, + }; + mocks.readAuthFileCached.mockResolvedValue(auth); + + expect(resolveGeminiCliAccounts(auth).map((account) => account.sourceKey)).toEqual(["google"]); + await expect(inspectGeminiCliAuthPresence()).resolves.toEqual({ + state: "present", + sourceKey: "google", + accountCount: 2, + validAccountCount: 1, + }); + }); + + it("does not treat a failed Google API key login as a Gemini CLI login", async () => { + mocks.readAuthFileCached.mockResolvedValue({ + google: { type: "api", resolveError: "resolve_empty: no value" }, + }); + + await expect(inspectGeminiCliAuthPresence()).resolves.toEqual({ + state: "missing", + accountCount: 0, + validAccountCount: 0, }); }); diff --git a/tests/lib.invalid-aware-provider-auth.test.ts b/tests/lib.invalid-aware-provider-auth.test.ts index 377e5f8e..5f8f05d2 100644 --- a/tests/lib.invalid-aware-provider-auth.test.ts +++ b/tests/lib.invalid-aware-provider-auth.test.ts @@ -133,6 +133,36 @@ describe("invalid-aware provider auth", () => { module.parseAuth(withEntry({ type: "api", key: " contract-key " })), `${provider.name} configured`, ).toEqual({ state: "configured", apiKey: "contract-key" }); + expect( + module.parseAuth(withEntry({ type: "api", resolveError: "resolve_empty: no value" })), + `${provider.name} failed`, + ).toEqual({ + state: "invalid", + error: "OpenCode could not read this login: resolve_empty: no value", + }); + } + }); + + it("keeps a login OpenCode could not return present as an invalid auth", async () => { + for (const provider of providers) { + const module = await provider.load(); + resetFixture(); + authMocks.readAuthFileCached.mockResolvedValue( + authWithEntry(provider.authKeys[0], { + type: "api", + resolveError: "active_failed: database is locked", + }), + ); + + await expect(module.resolve(), provider.name).resolves.toEqual({ + state: "invalid", + error: "OpenCode could not read this login: active_failed: database is locked", + }); + await expect(module.diagnostics(), `${provider.name} diagnostics`).resolves.toMatchObject({ + state: "invalid", + source: "opencode.db", + error: "OpenCode could not read this login: active_failed: database is locked", + }); } }); @@ -256,6 +286,7 @@ describe("invalid-aware provider auth", () => { }); expect(authMocks.readAuthFileCached, provider.name).toHaveBeenLastCalledWith({ maxAgeMs: provider.defaultCacheMaxAgeMs, + integrationIds: provider.authKeys, }); authMocks.readAuthFileCached.mockResolvedValue( @@ -269,6 +300,7 @@ describe("invalid-aware provider auth", () => { await module.resolve({ maxAgeMs: -1 }); expect(authMocks.readAuthFileCached, `${provider.name} clamp`).toHaveBeenLastCalledWith({ maxAgeMs: 0, + integrationIds: provider.authKeys, }); } }); diff --git a/tests/lib.minimax-auth.test.ts b/tests/lib.minimax-auth.test.ts index 56820e1a..111aedb3 100644 --- a/tests/lib.minimax-auth.test.ts +++ b/tests/lib.minimax-auth.test.ts @@ -253,6 +253,7 @@ describe("minimax auth resolution", () => { }); expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: DEFAULT_MINIMAX_AUTH_CACHE_MAX_AGE_MS, + integrationIds: ["minimax-coding-plan"], }); }); @@ -297,7 +298,10 @@ describe("minimax auth resolution", () => { mocks.readAuthFileCached.mockResolvedValueOnce({}); await resolveMiniMaxAuthCached({ maxAgeMs: -500 }); - expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: 0 }); + expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ + maxAgeMs: 0, + integrationIds: ["minimax-coding-plan"], + }); }); }); diff --git a/tests/lib.ollama-cloud-config.test.ts b/tests/lib.ollama-cloud-config.test.ts index 3bb5060a..1a217cc6 100644 --- a/tests/lib.ollama-cloud-config.test.ts +++ b/tests/lib.ollama-cloud-config.test.ts @@ -42,7 +42,7 @@ describe("ollama-cloud API key config", () => { vi.clearAllMocks(); }); - it("uses only the documented API key and trusted Ollama Cloud sources", () => { + it("uses only the documented API key and trusted Ollama Cloud sources", async () => { expect(mocks.config).toMatchObject({ envVars: [{ name: "OLLAMA_API_KEY", source: "env:OLLAMA_API_KEY" }], providerKeys: ["ollama-cloud"], @@ -51,11 +51,14 @@ describe("ollama-cloud API key config", () => { configJsoncSource: "opencode.jsonc", getConfigCandidates: mocks.getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: mocks.readAuthFile, + readAuth: expect.any(Function), getCredentialDatabasePaths: mocks.getCredentialDatabasePaths, authSource: "opencode.db", }, }); + const auth = mocks.config?.auth as { readAuth: () => Promise }; + await auth.readAuth(); + expect(mocks.readAuthFile).toHaveBeenCalledWith({ integrationIds: ["ollama-cloud"] }); expect(JSON.stringify(mocks.config)).not.toContain("OLLAMA_USAGE_COOKIE"); expect(JSON.stringify(mocks.config)).not.toContain("ollama-usage"); expect(getOpencodeConfigCandidatePaths()).toEqual([ diff --git a/tests/lib.openai.test.ts b/tests/lib.openai.test.ts index 53846ccf..adc2e8e6 100644 --- a/tests/lib.openai.test.ts +++ b/tests/lib.openai.test.ts @@ -113,12 +113,44 @@ describe("openai auth resolution", () => { }); }); + it("keeps a login OpenCode could not return present, with no identity and a clear error", async () => { + const failed = { openai: { type: "oauth", resolveError: "refresh_failed: HTTP 400" } }; + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + + expect(resolveOpenAIOAuth(failed)).toEqual({ + state: "failed", + sourceKey: "openai", + error: "refresh_failed: HTTP 400", + }); + mocks.readAuthFileCached.mockResolvedValue(failed); + await expect(hasOpenAIOAuthCached()).resolves.toBe(true); + await expect(resolveOpenAIAuthIdentity()).resolves.toBeNull(); + await expect(queryOpenAIQuota()).resolves.toEqual({ + success: false, + error: + "OpenAI sign-in could not be refreshed: refresh_failed: HTTP 400. Run `opencode auth login openai`.", + }); + expect(fetchMock).not.toHaveBeenCalled(); + expect(mocks.deriveResolvedAuthIdentity).not.toHaveBeenCalled(); + }); + + it("lets a failed openai login win over a later compatibility key, as a configured one would", () => { + expect( + resolveOpenAIOAuth({ + openai: { type: "oauth", resolveError: "resolve_empty: no value" }, + codex: { type: "oauth", access: "codex-token" }, + }), + ).toEqual({ state: "failed", sourceKey: "openai", error: "resolve_empty: no value" }); + }); + it("returns null when quota is not configured", async () => { mocks.readAuthFileCached.mockResolvedValueOnce({}); await expect(queryOpenAIQuota()).resolves.toBeNull(); expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS, + integrationIds: ["openai", "codex", "chatgpt"], }); }); @@ -212,6 +244,7 @@ describe("openai auth resolution", () => { await expect(hasOpenAIOAuthCached()).resolves.toBe(true); expect(mocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: DEFAULT_OPENAI_AUTH_CACHE_MAX_AGE_MS, + integrationIds: ["openai", "codex", "chatgpt"], }); }); diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 62f9ea50..a8616328 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -8,9 +8,11 @@ import { resolveAgyAccounts } from "../src/lib/google-agy.js"; import { resolveGeminiCliAccounts } from "../src/lib/google-gemini-cli.js"; import { clearReadAuthFileCacheForTests, + credentialRowAuthEntry, formatCredentialDisplayNames, getCredentialDatabasePaths, readAuthFile, + readAuthFileCached, readCredentialRows, selectConnectionCredentialRows, } from "../src/lib/opencode-auth.js"; @@ -19,6 +21,7 @@ const temporaryDirectories: string[] = []; afterEach(async () => { clearReadAuthFileCacheForTests(); + vi.useRealTimers(); vi.unstubAllEnvs(); await Promise.all( temporaryDirectories @@ -152,7 +155,9 @@ describe("OpenCode auth reader", () => { const { dataDir } = await createCredentialDatabase(); vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); - await expect(readAuthFile()).resolves.toMatchObject({ + await expect( + readAuthFile({ integrationIds: ["github-copilot", "deepseek", "openai"] }), + ).resolves.toMatchObject({ "github-copilot": { access: "copilot-access", enterpriseUrl: "example.ghe.com" }, deepseek: { type: "api", key: "deepseek-key" }, openai: { access: "openai-access" }, @@ -177,7 +182,7 @@ describe("OpenCode auth reader", () => { database.close(); vi.stubEnv("OPENCODE_DB", databasePath); - await expect(readCredentialRows()).resolves.toEqual( + await expect(readCredentialRows(["openai"])).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ id: "openai-active", @@ -189,9 +194,10 @@ describe("OpenCode auth reader", () => { expect.objectContaining({ id: "openai", active: false }), ]), ); - expect( - (await readCredentialRows()).findIndex((row) => row.id === "openai-active"), - ).toBeLessThan((await readCredentialRows()).findIndex((row) => row.id === "openai")); + const rows = await readCredentialRows(["openai"]); + expect(rows.findIndex((row) => row.id === "openai-active")).toBeLessThan( + rows.findIndex((row) => row.id === "openai"), + ); }); it("ignores legacy auth.json entries", async () => { @@ -202,13 +208,175 @@ describe("OpenCode auth reader", () => { JSON.stringify({ openai: { type: "oauth", access: "file-access" } }), ); - await expect(readAuthFile()).resolves.toMatchObject({ + await expect( + readAuthFile({ integrationIds: ["github-copilot", "openai"] }), + ).resolves.toMatchObject({ "github-copilot": { access: "copilot-access" }, openai: { access: "openai-access" }, }); }); }); +describe("reader requests name their integration ids", () => { + async function addRow( + databasePath: string, + row: { id: string; integrationId: string; active: number; updated: number; value: unknown }, + ): Promise { + const database = new DatabaseSync(databasePath); + database + .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, ?, 1, ?)") + .run(row.id, row.integrationId, JSON.stringify(row.value), row.active, row.updated); + database.close(); + } + + it("returns only rows of the requested ids, in database order", async () => { + const { databasePath } = await createCredentialDatabase(); + vi.stubEnv("OPENCODE_DB", databasePath); + + expect((await readCredentialRows(["openai"])).map((row) => row.id)).toEqual(["openai"]); + // Database order (most recently updated first), not request order. + expect((await readCredentialRows(["github-copilot", "openai"])).map((row) => row.id)).toEqual([ + "openai", + "copilot", + ]); + await expect(readCredentialRows([])).resolves.toEqual([]); + await expect(readAuthFile({ integrationIds: ["xai"] })).resolves.toBeNull(); + await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toEqual({ + openai: { type: "oauth", access: "openai-access", refresh: "openai-refresh", expires: 20 }, + }); + }); + + it("keeps only the requested connection methods", async () => { + const { databasePath } = await createCredentialDatabase(); + await addRow(databasePath, { + id: "console", + integrationId: "opencode", + active: 1, + updated: 5, + value: { type: "oauth", access: "console-access", refresh: "console-refresh", expires: 1 }, + }); + await addRow(databasePath, { + id: "workspace-key", + integrationId: "opencode", + active: 0, + updated: 4, + value: { type: "key", key: "workspace-key" }, + }); + vi.stubEnv("OPENCODE_DB", databasePath); + + const ids = ["opencode", "openai", "deepseek"]; + expect((await readCredentialRows(ids, { methods: ["key"] })).map((row) => row.id)).toEqual([ + "workspace-key", + "deepseek", + ]); + expect((await readCredentialRows(ids, { methods: ["oauth"] })).map((row) => row.id)).toEqual([ + "console", + "openai", + ]); + expect( + (await readCredentialRows(ids, { methods: ["key", "oauth"] })).map((row) => row.id), + ).toEqual(["console", "workspace-key", "deepseek", "openai"]); + }); + + it("keeps the first (active) row per id when firstOnly is set", async () => { + const { databasePath } = await createCredentialDatabase(); + const database = new DatabaseSync(databasePath); + database.prepare("UPDATE credential SET active = 1 WHERE id = 'openai'").run(); + database.close(); + await addRow(databasePath, { + id: "openai-newer-inactive", + integrationId: "openai", + active: 0, + updated: 9, + value: { type: "oauth", access: "inactive-access" }, + }); + vi.stubEnv("OPENCODE_DB", databasePath); + + expect( + (await readCredentialRows(["openai", "deepseek"], { firstOnly: true })).map((row) => row.id), + ).toEqual(["openai", "deepseek"]); + await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toMatchObject({ + openai: { access: "openai-access" }, + }); + }); + + it("applies firstOnly before the method filter, so a mismatched active row yields nothing", async () => { + const { databasePath } = await createCredentialDatabase(); + await addRow(databasePath, { + id: "openai-key", + integrationId: "openai", + active: 1, + updated: 9, + value: { type: "key", key: "openai-key" }, + }); + vi.stubEnv("OPENCODE_DB", databasePath); + + await expect( + readCredentialRows(["openai"], { firstOnly: true, methods: ["oauth"] }), + ).resolves.toEqual([]); + }); + + it("caches auth maps per sorted id list", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(1_000_000); + const { databasePath } = await createCredentialDatabase(); + vi.stubEnv("OPENCODE_DB", databasePath); + + await expect( + readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai", "deepseek"] }), + ).resolves.toMatchObject({ openai: { access: "openai-access" } }); + const database = new DatabaseSync(databasePath); + database + .prepare("UPDATE credential SET value = ? WHERE id = 'openai'") + .run(JSON.stringify({ type: "oauth", access: "rotated-access" })); + database.close(); + + // Same ids in another order hit the same cache entry. + await expect( + readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["deepseek", "openai"] }), + ).resolves.toMatchObject({ openai: { access: "openai-access" } }); + // A different id list has its own entry and reads fresh. + await expect( + readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai"] }), + ).resolves.toEqual({ openai: { type: "oauth", access: "rotated-access" } }); + // Once time moves on, maxAgeMs 0 re-reads the first entry too. + vi.setSystemTime(1_000_001); + await expect( + readAuthFileCached({ maxAgeMs: 0, integrationIds: ["openai", "deepseek"] }), + ).resolves.toMatchObject({ openai: { access: "rotated-access" } }); + }); + + it("shares one in-flight read per id list", async () => { + const { databasePath } = await createCredentialDatabase(); + vi.stubEnv("OPENCODE_DB", databasePath); + + const [first, second] = await Promise.all([ + readAuthFileCached({ maxAgeMs: 0, integrationIds: ["deepseek"] }), + readAuthFileCached({ maxAgeMs: 0, integrationIds: ["deepseek"] }), + ]); + expect(first).toEqual({ deepseek: { type: "api", key: "deepseek-key" } }); + expect(second).toBe(first); + }); + + it("adds resolveError to the auth entry of a failed row only", () => { + const row = { + id: "openai", + integrationId: "openai", + label: "default", + active: true, + value: { type: "oauth", access: "token" }, + }; + expect(credentialRowAuthEntry(row)).toBe(row.value); + expect( + credentialRowAuthEntry({ + ...row, + value: { type: "oauth" }, + resolveError: "refresh_failed: HTTP 400", + }), + ).toEqual({ type: "oauth", resolveError: "refresh_failed: HTTP 400" }); + }); +}); + describe("selectConnectionCredentialRows", () => { const row = (id: string, integrationId: string, key: string) => ({ id, @@ -324,7 +492,7 @@ describe("Google companion credentials written by OpenCode 2", () => { ); database.close(); vi.stubEnv("OPENCODE_DB", databasePath); - const auth = await readAuthFile(); + const auth = await readAuthFile({ integrationIds: ["google", "google-agy"] }); expect(resolveGeminiCliAccounts(auth)).toEqual([ { diff --git a/tests/lib.opencode-console-auth.test.ts b/tests/lib.opencode-console-auth.test.ts index e304c233..c9b06522 100644 --- a/tests/lib.opencode-console-auth.test.ts +++ b/tests/lib.opencode-console-auth.test.ts @@ -6,6 +6,16 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { resolveOpenCodeConsoleAuth } from "../src/lib/opencode-console-auth.js"; +const authMocks = vi.hoisted(() => ({ readCredentialRows: vi.fn() })); + +// Real SQLite reads by default; single tests swap in fixture rows the SQLite +// reader cannot produce (failed logins). +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => { + const actual = await importOriginal(); + authMocks.readCredentialRows.mockImplementation(actual.readCredentialRows); + return { ...actual, readCredentialRows: authMocks.readCredentialRows }; +}); + const temporaryDirectories: string[] = []; afterEach(async () => { @@ -20,7 +30,7 @@ afterEach(async () => { async function createCredentialDatabaseWith( integrationId: string, value: Record, -): Promise { +): Promise { const root = await mkdtemp(join(tmpdir(), "opencode-console-auth-")); temporaryDirectories.push(root); const dataDir = join(root, "opencode"); @@ -42,6 +52,7 @@ async function createCredentialDatabaseWith( .run("cred-console", integrationId, JSON.stringify(value)); database.close(); vi.stubEnv("XDG_DATA_HOME", root); + return join(dataDir, "opencode.db"); } describe("OpenCode Console credential reader", () => { @@ -104,4 +115,43 @@ describe("OpenCode Console credential reader", () => { await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); }); + + it("reads only the active opencode login", async () => { + const databasePath = await createCredentialDatabaseWith("opencode", { + type: "key", + key: "workspace-key", + }); + const database = new DatabaseSync(databasePath); + database.prepare("UPDATE credential SET active = 1 WHERE id = 'cred-console'").run(); + database + .prepare("INSERT INTO credential VALUES (?, 'opencode', 'default', ?, NULL, NULL, 0, 1, 2)") + .run( + "cred-console-inactive", + JSON.stringify({ type: "oauth", access: "console-access", expires: Date.now() + 60_000 }), + ); + database.close(); + + await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); + expect(authMocks.readCredentialRows).toHaveBeenLastCalledWith(["opencode"], { + firstOnly: true, + }); + }); + + it("reports a login OpenCode could not return as invalid", async () => { + authMocks.readCredentialRows.mockResolvedValueOnce([ + { + id: "cred-console", + integrationId: "opencode", + label: "default", + active: true, + value: { type: "oauth" }, + resolveError: "refresh_failed: HTTP 401", + }, + ]); + + await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ + state: "invalid", + error: "refresh_failed: HTTP 401", + }); + }); }); diff --git a/tests/lib.opencode-db-path.test.ts b/tests/lib.opencode-db-path.test.ts index 70c45c7c..fcfffe13 100644 --- a/tests/lib.opencode-db-path.test.ts +++ b/tests/lib.opencode-db-path.test.ts @@ -75,7 +75,7 @@ describe("OpenCode database path", () => { const expected = join(sandboxData, "opencode", "opencode.db"); expect(getOpenCodeDbPath()).toBe(expected); expect(getCredentialDatabasePaths()).toEqual([expected]); - await expect(readAuthFile()).resolves.toBeNull(); + await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toBeNull(); await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: expected, sessionCount: 0, @@ -91,7 +91,9 @@ describe("OpenCode database path", () => { vi.stubEnv("OPENCODE_DB", custom); expect(getOpenCodeDbPath()).toBe(custom); - await expect(readAuthFile()).resolves.toMatchObject({ deepseek: { key: "custom-key" } }); + await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toMatchObject({ + deepseek: { key: "custom-key" }, + }); await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: custom, sessionCount: 2 }); }); @@ -104,7 +106,9 @@ describe("OpenCode database path", () => { vi.stubEnv("OPENCODE_DB", join("profiles", "work.db")); expect(getOpenCodeDbPath()).toBe(custom); - await expect(readAuthFile()).resolves.toMatchObject({ deepseek: { key: "work-key" } }); + await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toMatchObject({ + deepseek: { key: "work-key" }, + }); await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: custom, sessionCount: 4 }); }); @@ -116,7 +120,7 @@ describe("OpenCode database path", () => { expect(getOpenCodeDbPath()).toBe(":memory:"); expect(getCredentialDatabasePaths()).toEqual([]); - await expect(readAuthFile()).resolves.toBeNull(); + await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toBeNull(); await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: ":memory:", sessionCount: 0, diff --git a/tests/lib.opencode-go-auth.test.ts b/tests/lib.opencode-go-auth.test.ts index 77613eca..c457c9f5 100644 --- a/tests/lib.opencode-go-auth.test.ts +++ b/tests/lib.opencode-go-auth.test.ts @@ -287,6 +287,7 @@ describe("OpenCode Go auth resolution", () => { }); expect(authMocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, + integrationIds: ["opencode-go", "opencode"], }); }); @@ -312,7 +313,10 @@ describe("OpenCode Go auth resolution", () => { error: "OpenCode Go auth entry has unsupported type", }); expect(JSON.stringify(diagnostics)).not.toContain(secret); - expect(authMocks.readAuthFileCached).toHaveBeenLastCalledWith({ maxAgeMs: 0 }); + expect(authMocks.readAuthFileCached).toHaveBeenLastCalledWith({ + maxAgeMs: 0, + integrationIds: ["opencode-go", "opencode"], + }); const { opencodeGoProvider } = await import("../src/providers/opencode-go.js"); const result = await opencodeGoProvider.fetch(createProviderAvailabilityContext()); diff --git a/tests/lib.xai.test.ts b/tests/lib.xai.test.ts index e4db7533..f0d3e60f 100644 --- a/tests/lib.xai.test.ts +++ b/tests/lib.xai.test.ts @@ -113,6 +113,30 @@ describe("queryXaiQuota", () => { await expect(queryXaiQuota()).resolves.toBeNull(); }); + it("reports a login OpenCode could not return without fetching, and keeps it present", async () => { + const { readAuthFile, readAuthFileCached } = await import("../src/lib/opencode-auth.js"); + const failed = { xai: { type: "oauth", resolveError: "refresh_failed: HTTP 401" } }; + (readAuthFile as any).mockResolvedValue(failed); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + + expect(resolveXaiOAuth(failed)).toEqual({ + state: "failed", + error: "refresh_failed: HTTP 401", + }); + expect(hasXaiOAuth(failed)).toBe(true); + await expect(queryXaiQuota()).resolves.toEqual({ + success: false, + error: + "xAI sign-in could not be refreshed: refresh_failed: HTTP 401. Run `opencode auth login xai`.", + }); + await expect(resolveXaiAuthIdentity()).resolves.toBeNull(); + expect(readAuthFile).toHaveBeenCalledWith({ integrationIds: ["xai"] }); + expect(readAuthFileCached).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + expect(deriveResolvedAuthIdentity).not.toHaveBeenCalled(); + }); + it("does not refresh, fetch, or write an expired token", async () => { const { readAuthFile, readAuthFileCached } = await import("../src/lib/opencode-auth.js"); (readAuthFile as any).mockResolvedValueOnce({ diff --git a/tests/provider-surfaces.google-agy.test.ts b/tests/provider-surfaces.google-agy.test.ts index d835b27f..0f193985 100644 --- a/tests/provider-surfaces.google-agy.test.ts +++ b/tests/provider-surfaces.google-agy.test.ts @@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({ })); vi.mock("../src/lib/google-agy.js", () => ({ + AGY_AUTH_KEYS: ["google-agy", "opencode-agy-auth", "google-agy-auth"], hasAgyQuotaRuntimeAvailable: vi.fn(async () => true), queryGoogleAgyQuota: mocks.queryGoogleAgyQuota, inspectAgyAuthPresence: vi.fn(async () => ({ diff --git a/tests/providers.copilot.test.ts b/tests/providers.copilot.test.ts index 22d2a54c..16a4161a 100644 --- a/tests/providers.copilot.test.ts +++ b/tests/providers.copilot.test.ts @@ -14,6 +14,7 @@ const authMocks = vi.hoisted(() => ({ })); vi.mock("../src/lib/copilot.js", () => ({ + COPILOT_AUTH_KEYS: ["github-copilot", "copilot", "copilot-chat", "github-copilot-chat"], hasCopilotQuotaRuntimeAvailable: vi.fn(async () => false), queryCopilotQuota: vi.fn(), getCopilotQuotaAuthDiagnostics: vi.fn(() => ({ diff --git a/tests/providers.glm-coding-plan.test.ts b/tests/providers.glm-coding-plan.test.ts index 9f253a35..0eb7a024 100644 --- a/tests/providers.glm-coding-plan.test.ts +++ b/tests/providers.glm-coding-plan.test.ts @@ -46,6 +46,13 @@ vi.mock("../src/lib/zhipu-auth.js", () => ({ })), })); +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: vi.fn().mockResolvedValue([]), +})); + +import { readCredentialRows } from "../src/lib/opencode-auth.js"; +import { getZaiAuthDiagnostics, resolveZaiAuth } from "../src/lib/zai-auth.js"; import { zaiProvider } from "../src/providers/zai.js"; import { zhipuProvider } from "../src/providers/zhipu.js"; @@ -187,6 +194,65 @@ describe.each(PROVIDERS)("$label GLM provider", (descriptor) => { }); }); +describe("GLM database credential rows", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("shows a login OpenCode could not return as its own error row next to working rows", async () => { + const actual = + await vi.importActual("../src/lib/zai-auth.js"); + vi.mocked(resolveZaiAuth).mockImplementation(actual.resolveZaiAuth); + vi.mocked(getZaiAuthDiagnostics).mockResolvedValueOnce({ + state: "invalid", + source: "opencode.db", + checkedPaths: [], + credentialDatabasePaths: ["/tmp/opencode.db"], + error: "OpenCode could not read this login: refresh_failed: HTTP 401", + }); + vi.mocked(readCredentialRows).mockResolvedValueOnce([ + { + id: "failed-id", + integrationId: "zai-coding-plan", + label: "Work", + active: true, + value: { type: "api" }, + resolveError: "refresh_failed: HTTP 401", + }, + { + id: "working-id", + integrationId: "zai-coding-plan", + label: "Home", + active: false, + value: { type: "api", key: "home-key" }, + }, + ]); + mocks.queryZaiQuota.mockResolvedValueOnce({ + success: true, + label: "Z.ai", + windows: { fiveHour: { percentRemaining: 80 } }, + }); + + const out = await zaiProvider.fetch({ config: {} } as any); + + expect(readCredentialRows).toHaveBeenCalledWith(["zai-coding-plan"], { methods: ["key"] }); + expect(mocks.queryZaiQuota).toHaveBeenCalledTimes(1); + expect(mocks.queryZaiQuota).toHaveBeenCalledWith({ + requestTimeoutMs: undefined, + apiKey: "home-key", + }); + expect(out.errors).toEqual([ + { + label: "[Z.ai Work]*", + message: "OpenCode could not read this login: refresh_failed: HTTP 401", + }, + ]); + expect(out.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ + ["[Z.ai Home]", "working-id"], + ]); + }); +}); + describe("Z.ai credit quota surfaces", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/tests/providers.google-agy.test.ts b/tests/providers.google-agy.test.ts index f50d8c43..96af7999 100644 --- a/tests/providers.google-agy.test.ts +++ b/tests/providers.google-agy.test.ts @@ -8,6 +8,7 @@ import { } from "./helpers/provider-assertions.js"; vi.mock("../src/lib/google-agy.js", () => ({ + AGY_AUTH_KEYS: ["google-agy", "opencode-agy-auth", "google-agy-auth"], hasAgyQuotaRuntimeAvailable: vi.fn(), queryGoogleAgyQuota: vi.fn(), inspectAgyAuthPresence: vi.fn(async () => ({ diff --git a/tests/providers.google-db-credentials.test.ts b/tests/providers.google-db-credentials.test.ts index d66dfbef..06cd8ebf 100644 --- a/tests/providers.google-db-credentials.test.ts +++ b/tests/providers.google-db-credentials.test.ts @@ -11,6 +11,7 @@ vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ readCredentialRows: mocks.rows, })); vi.mock("../src/lib/google-agy.js", () => ({ + AGY_AUTH_KEYS: ["google-agy", "opencode-agy-auth", "google-agy-auth"], hasAgyQuotaRuntimeAvailable: vi.fn(), inspectAgyAuthPresence: vi.fn(async () => ({ state: "configured", @@ -21,6 +22,13 @@ vi.mock("../src/lib/google-agy.js", () => ({ queryGoogleAgyQuota: mocks.agyQuery, })); vi.mock("../src/lib/google-gemini-cli.js", () => ({ + GEMINI_CLI_AUTH_KEYS: [ + "google-gemini-cli", + "gemini-cli", + "opencode-gemini-auth", + "gemini", + "google", + ], hasGeminiCliQuotaRuntimeAvailable: vi.fn(), inspectGeminiCliAuthPresence: vi.fn(async () => ({ state: "configured", diff --git a/tests/providers.google-gemini-cli.test.ts b/tests/providers.google-gemini-cli.test.ts index 0420bdd6..8be92d23 100644 --- a/tests/providers.google-gemini-cli.test.ts +++ b/tests/providers.google-gemini-cli.test.ts @@ -8,6 +8,13 @@ import { } from "./helpers/provider-assertions.js"; vi.mock("../src/lib/google-gemini-cli.js", () => ({ + GEMINI_CLI_AUTH_KEYS: [ + "google-gemini-cli", + "gemini-cli", + "opencode-gemini-auth", + "gemini", + "google", + ], hasGeminiCliQuotaRuntimeAvailable: vi.fn(), queryGeminiCliQuota: vi.fn(), inspectGeminiCliAuthPresence: vi.fn(async () => ({ @@ -18,6 +25,11 @@ vi.mock("../src/lib/google-gemini-cli.js", () => ({ })), })); +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: vi.fn().mockResolvedValue([]), +})); + vi.mock("../src/lib/google-gemini-cli-companion.js", () => ({ inspectGeminiCliCompanionPresence: vi.fn(async () => ({ state: "missing", @@ -166,6 +178,63 @@ describe("google gemini cli provider", () => { expectAttemptedWithErrorLabel(out, "Gemini CLI"); }); + it("adds an error row per login OpenCode could not return, next to working accounts", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + const { queryGeminiCliQuota } = await import("../src/lib/google-gemini-cli.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "failed-id", + integrationId: "google", + label: "work@example.com", + active: true, + value: { type: "oauth" }, + resolveError: "refresh_failed: HTTP 400", + }, + { + id: "working-id", + integrationId: "google-gemini-cli", + label: "home@example.com", + active: false, + value: { type: "oauth", refresh: "refresh-token|project-id" }, + }, + ]); + (queryGeminiCliQuota as any).mockResolvedValueOnce({ + success: true, + buckets: [ + { + modelId: "gemini-2.5-pro", + displayName: "Gemini Pro", + accountEmail: "home@example.com", + percentRemaining: 64, + }, + ], + }); + + const out = await googleGeminiCliProvider.fetch({ client: {} } as any); + + expect(readCredentialRows).toHaveBeenCalledWith( + ["google-gemini-cli", "gemini-cli", "opencode-gemini-auth", "gemini", "google"], + { methods: ["oauth"] }, + ); + // Only the working row is queried. + expect(queryGeminiCliQuota).toHaveBeenCalledTimes(1); + expect(queryGeminiCliQuota).toHaveBeenLastCalledWith({ + requestTimeoutMs: undefined, + authData: { "google-gemini-cli": { type: "oauth", refresh: "refresh-token|project-id" } }, + }); + expect(out.attempted).toBe(true); + expect(out.errors).toEqual([ + { + label: "[Gemini CLI work@example.com]*", + message: + "Gemini CLI sign-in could not be refreshed: refresh_failed: HTTP 400. Run `opencode auth login google`.", + }, + ]); + expect(out.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ + ["[Gemini CLI home@example.com]", "working-id"], + ]); + }); + it("is available only when the Gemini CLI runtime is configured", async () => { const { hasGeminiCliQuotaRuntimeAvailable } = await import("../src/lib/google-gemini-cli.js"); (hasGeminiCliQuotaRuntimeAvailable as any).mockResolvedValueOnce(true); diff --git a/tests/providers.kimi-code.test.ts b/tests/providers.kimi-code.test.ts index e6a56bcb..7d68a6cb 100644 --- a/tests/providers.kimi-code.test.ts +++ b/tests/providers.kimi-code.test.ts @@ -335,4 +335,50 @@ describe("Kimi regional providers", () => { expect.objectContaining({ apiKey: "cn-key", endpoint: "cn" }), ); }); + + it("shows a CN login OpenCode could not return as its own error row", async () => { + const actual = + await vi.importActual("../src/lib/kimi-auth.js"); + authMocks.parseCn.mockImplementation(actual.resolveKimiCnAuth); + authMocks.resolveCnWithDiagnostics.mockResolvedValueOnce({ + auth: { + state: "invalid", + error: "OpenCode could not read this login: resolve_empty: no value", + }, + diagnostics: { + state: "invalid", + source: "opencode.db", + error: "OpenCode could not read this login: resolve_empty: no value", + checkedPaths: [], + credentialDatabasePaths: [], + }, + }); + vi.mocked(readCredentialRows).mockResolvedValueOnce([ + { + id: "failed", + integrationId: "kimi-code-plan-cn", + label: "Work", + active: true, + value: { type: "api" }, + resolveError: "resolve_empty: no value", + }, + ]); + vi.mocked(queryKimiQuota).mockClear(); + + const out = await kimiCodePlanCnProvider.fetch({ config: {} } as any); + + expect(readCredentialRows).toHaveBeenLastCalledWith( + ["kimi-code-plan-cn", "kimi-for-coding", "kimi-code", "kimi"], + { methods: ["key"] }, + ); + expect(out.attempted).toBe(true); + expect(out.entries).toEqual([]); + expect(out.errors).toEqual([ + { + label: "[Kimi Code (CN) Work]*", + message: "OpenCode could not read this login: resolve_empty: no value", + }, + ]); + expect(queryKimiQuota).not.toHaveBeenCalled(); + }); }); diff --git a/tests/providers.minimax-coding-plan.test.ts b/tests/providers.minimax-coding-plan.test.ts index f13592af..6a4bbbc6 100644 --- a/tests/providers.minimax-coding-plan.test.ts +++ b/tests/providers.minimax-coding-plan.test.ts @@ -202,6 +202,9 @@ describe("minimax-coding-plan provider", () => { mockMiniMaxHttpSuccess([createCodingPlanModel()]); const out = await runProviderFetch(); + expect(readCredentialRows).toHaveBeenCalledWith(["minimax-coding-plan"], { + methods: ["key"], + }); expect(out.errors).toContainEqual({ label: "[MiniMax Token Plan shared]*", message: "empty key", diff --git a/tests/providers.openai.test.ts b/tests/providers.openai.test.ts index 9fc2bee7..76164972 100644 --- a/tests/providers.openai.test.ts +++ b/tests/providers.openai.test.ts @@ -208,6 +208,63 @@ describe("openai provider", () => { } }); + it("shows a login OpenCode could not return as its own error row next to working ones", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + const { queryOpenAIQuota, resolveOpenAIOAuth } = await import("../src/lib/openai.js"); + const actual = + await vi.importActual("../src/lib/openai.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "work-id", + integrationId: "openai", + label: "Work", + active: true, + value: { type: "oauth" }, + resolveError: "refresh_failed: HTTP 400", + }, + { + id: "home-id", + integrationId: "openai", + label: "Home", + active: false, + value: { type: "oauth", access: "home-token" }, + }, + ]); + (resolveOpenAIOAuth as any) + .mockImplementationOnce(actual.resolveOpenAIOAuth) + .mockImplementationOnce(actual.resolveOpenAIOAuth); + (queryOpenAIQuota as any) + .mockImplementationOnce(actual.queryOpenAIQuota) + .mockResolvedValueOnce({ + success: true, + label: "OpenAI (Pro)", + windows: { hourly: { percentRemaining: 42 } }, + }); + + const out = await openaiProvider.fetch({} as any); + + expect(readCredentialRows).toHaveBeenLastCalledWith(["openai", "codex", "chatgpt"], { + methods: ["oauth"], + }); + expect(out.errors).toEqual([ + { + label: "[OpenAI Work]*", + message: + "OpenAI sign-in could not be refreshed: refresh_failed: HTTP 400. Run `opencode auth login openai`.", + }, + ]); + expect(out.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ + ["[OpenAI Home] (Pro)", "home-id"], + ]); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "auth_configured", value: "true" }, + { key: "auth_source", value: "openai" }, + { key: "token_status", value: "failed" }, + ]), + ); + }); + it("is available when provider ids include openai/chatgpt/codex", async () => { const { hasOpenAIOAuthCached } = await import("../src/lib/openai.js"); (hasOpenAIOAuthCached as any).mockResolvedValue(false); diff --git a/tests/providers.xai.test.ts b/tests/providers.xai.test.ts index afdb2326..57aa3c71 100644 --- a/tests/providers.xai.test.ts +++ b/tests/providers.xai.test.ts @@ -19,6 +19,12 @@ vi.mock("../src/lib/xai.js", () => ({ return "Period"; }), queryXaiQuota: vi.fn(), + resolveXaiOAuth: vi.fn(), +})); + +vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + readCredentialRows: vi.fn().mockResolvedValue([]), })); describe("xai provider", () => { @@ -116,6 +122,37 @@ describe("xai provider", () => { expect(output.errors).toEqual([{ label: "xAI", message: "Token expired" }]); }); + it("shows a login OpenCode could not return as its own error row", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + const { queryXaiQuota, resolveXaiOAuth } = await import("../src/lib/xai.js"); + const actual = await vi.importActual("../src/lib/xai.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "xai-id", + integrationId: "xai", + label: "default", + active: true, + value: { type: "oauth" }, + resolveError: "refresh_failed: HTTP 401", + }, + ]); + (resolveXaiOAuth as any).mockImplementationOnce(actual.resolveXaiOAuth); + (queryXaiQuota as any).mockImplementationOnce(actual.queryXaiQuota); + + const output = await xaiProvider.fetch({ config: {} } as any); + + expect(readCredentialRows).toHaveBeenCalledWith(["xai"], { methods: ["oauth"] }); + expect(output.attempted).toBe(true); + expect(output.entries).toEqual([]); + expect(output.errors).toEqual([ + { + label: "[xAI]*", + message: + "xAI sign-in could not be refreshed: refresh_failed: HTTP 401. Run `opencode auth login xai`.", + }, + ]); + }); + it("uses currentProviderID to distinguish direct xAI from Copilot Grok models", () => { expect( xaiProvider.matchesCurrentModel?.("grok-4", { From 94f402f8a254211792521ffb7340f18c0dbc7389 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:21:06 +0200 Subject: [PATCH 69/97] refactor(auth): read OpenCode logins through the plugin integration API The server plugin now binds OpenCode's ctx.integration as the only login source in setup and unbinds it in its cleanup. Every read names its integration ids: single-login reads ask active() per id and never list(); fan-outs take every connection of registered ids from list() and the active one for any other id (or for every id when list() fails). The method filter runs before resolve(), so a key read never refreshes a sign-in. The SQLite credential reader is gone; without a bound source (TUI, terminal command) a read finds no login and warns once. A login OpenCode cannot return (failed refresh, empty resolve, failed active()) keeps its place as a failed row with a scrubbed reason, is resolved again at most once a minute, and is forgotten on success and on credential.updated / credential.switched events. Failed logins now also show OpenCode's reason for MiniMax and Alibaba, show as per-account error rows for Copilot and Anthropic (still present, no identity), and two failed Go logins no longer merge into one row. Co-authored-by: Ran Sagy --- src/lib/alibaba-auth.ts | 7 + src/lib/anthropic-auth.ts | 4 + src/lib/anthropic.ts | 3 +- src/lib/copilot.ts | 8 +- src/lib/minimax-auth.ts | 7 + src/lib/opencode-auth.ts | 434 +++++++--- src/lib/types.ts | 8 + src/plugin.ts | 30 +- src/providers/anthropic.ts | 19 +- src/providers/copilot.ts | 4 +- tests/helpers/fake-integration.ts | 84 ++ tests/lib.alibaba-auth.test.ts | 12 + tests/lib.anthropic-auth.test.ts | 8 + tests/lib.anthropic.test.ts | 11 + tests/lib.copilot.test.ts | 21 + tests/lib.minimax-auth.test.ts | 19 + tests/lib.opencode-auth.test.ts | 772 ++++++++++++------ tests/lib.opencode-console-auth.test.ts | 183 +++-- tests/lib.opencode-db-path.test.ts | 52 +- tests/plugin-rpc.test.ts | 3 + tests/plugin-v2.test.ts | 81 ++ tests/plugin.command-handled-boundary.test.ts | 3 + tests/plugin.maintainer-announcements.test.ts | 3 + tests/plugin.quota-command.test.ts | 5 + tests/provider-surfaces.opencode-go.test.ts | 3 + tests/providers.anthropic.test.ts | 52 ++ tests/providers.copilot.test.ts | 54 ++ tests/providers.opencode-go.test.ts | 29 + tests/tui-dist-import-graph.test.ts | 3 +- tests/v4-phase5-cross-surface.test.ts | 3 + 30 files changed, 1443 insertions(+), 482 deletions(-) create mode 100644 tests/helpers/fake-integration.ts diff --git a/src/lib/alibaba-auth.ts b/src/lib/alibaba-auth.ts index 9616bc9a..a4394250 100644 --- a/src/lib/alibaba-auth.ts +++ b/src/lib/alibaba-auth.ts @@ -108,6 +108,13 @@ export function resolveAlibabaCodingPlanAuth( return { state: "invalid", error: "Alibaba Coding Plan auth entry has invalid shape" }; } + if (typeof alibaba.resolveError === "string") { + return { + state: "invalid", + error: `OpenCode could not read this login: ${alibaba.resolveError}`, + }; + } + if (typeof alibaba.type !== "string") { return { state: "invalid", diff --git a/src/lib/anthropic-auth.ts b/src/lib/anthropic-auth.ts index 82e4ba8d..69df6125 100644 --- a/src/lib/anthropic-auth.ts +++ b/src/lib/anthropic-auth.ts @@ -13,6 +13,7 @@ export const DEFAULT_ANTHROPIC_AUTH_CACHE_MAX_AGE_MS = 5_000; export type ResolvedAnthropicOAuth = | { state: "none" } + | { state: "failed"; error: string } | { state: "expired"; expiresAt: number } | { state: "configured"; accessToken: string; expiresAt?: number }; @@ -24,6 +25,9 @@ export function resolveAnthropicOAuth( if (!entry || entry.type !== "oauth") { return { state: "none" }; } + if (entry.resolveError !== undefined) { + return { state: "failed", error: entry.resolveError }; + } const accessToken = typeof entry.access === "string" ? entry.access.trim() : ""; if (!accessToken) { diff --git a/src/lib/anthropic.ts b/src/lib/anthropic.ts index f06336f7..1b67198c 100644 --- a/src/lib/anthropic.ts +++ b/src/lib/anthropic.ts @@ -1416,7 +1416,8 @@ export async function hasAnthropicCredentialsConfigured( ): Promise { try { const opencodeCredentials = await resolveAnthropicOAuthCached(); - if (opencodeCredentials.state === "configured") { + // A login OpenCode could not return still counts, so it shows as an error. + if (opencodeCredentials.state === "configured" || opencodeCredentials.state === "failed") { return true; } } catch { diff --git a/src/lib/copilot.ts b/src/lib/copilot.ts index eb44cf67..e5c7b321 100644 --- a/src/lib/copilot.ts +++ b/src/lib/copilot.ts @@ -557,7 +557,8 @@ function selectCopilotAuth(authData: AuthData | null): { for (const [keyName, auth] of candidates) { if (!auth || auth.type !== "oauth") continue; - if (!getCopilotOAuthToken(auth)) continue; + // A login OpenCode could not return still counts, so it shows as an error. + if (!getCopilotOAuthToken(auth) && auth.resolveError === undefined) continue; return { auth, keyName }; } @@ -1348,6 +1349,11 @@ export async function queryCopilotQuota( const { auth } = selectCopilotAuth( options.authData ?? (await readAuthFile({ integrationIds: COPILOT_AUTH_KEYS })), ); + if (auth?.resolveError !== undefined) { + return toQuotaError( + `Copilot sign-in could not be read: ${auth.resolveError}. Run \`opencode auth login github-copilot\`.`, + ); + } const token = getCopilotOAuthToken(auth); if (!auth || !token) return null; diff --git a/src/lib/minimax-auth.ts b/src/lib/minimax-auth.ts index d395a346..d702c751 100644 --- a/src/lib/minimax-auth.ts +++ b/src/lib/minimax-auth.ts @@ -179,6 +179,13 @@ function resolveMiniMaxAuthForSpec( return { state: "invalid", error: "MiniMax auth entry has invalid shape" }; } + if (typeof minimax.resolveError === "string") { + return { + state: "invalid", + error: `OpenCode could not read this login: ${minimax.resolveError}`, + }; + } + if (typeof minimax.type !== "string") { return { state: "invalid", error: "MiniMax auth entry present but type is missing or invalid" }; } diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index dfad89bf..0a98e5a0 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -1,14 +1,17 @@ /** - * OpenCode credential database reader + * OpenCode login reader * - * Shared helper to read credentials from OpenCode's SQLite database. - * Providers should prefer this to duplicating database/path parsing. + * Reads OpenCode 2 logins through the server plugin's integration API + * (`ctx.integration`). The server plugin binds that source in `setup`; the TUI + * and the terminal command never bind one, so a read there finds no login. + * Every read names the integration ids it needs, so OpenCode resolves (and may + * refresh) only those logins. */ -import { existsSync } from "fs"; +import type { Plugin } from "@opencode/plugin"; +import { sanitizeSingleLineDisplayText } from "./display-sanitize.js"; import { getOpenCodeDbPath } from "./opencode-db-path.js"; -import { openOpenCodeSqliteReadOnly, type SqliteConn } from "./opencode-sqlite.js"; import type { AuthData } from "./types.js"; @@ -26,9 +29,9 @@ export type CredentialRow = { value: Record; /** * Why OpenCode could not return this login (for example a failed token - * refresh). A failed row keeps its id, label and position, but its `value` - * is only `{ type: "api" | "oauth" }`; consumers show `resolveError` as a - * per-account error. The SQLite reader never sets it. + * refresh), as `: `. A failed row keeps its id, + * label and position, but its `value` is only `{ type: "api" | "oauth" }`; + * consumers show `resolveError` as a per-account error. */ resolveError?: string; }; @@ -37,12 +40,70 @@ export type CredentialRow = { export type CredentialMethod = "key" | "oauth"; export type ReadCredentialRowsOptions = { - /** Keep only rows whose stored credential uses one of these methods. */ + /** Keep only connections using one of these methods; checked before OpenCode resolves them. */ methods?: readonly CredentialMethod[]; - /** Keep only the first (active) row of each integration id. */ + /** Read only the active connection of each integration id. */ firstOnly?: boolean; }; +export type CredentialReadRequest = ReadCredentialRowsOptions & { + integrationIds: readonly string[]; +}; + +/** Where logins come from. The only production source is `ctx.integration`. */ +export type CredentialSource = { + readRows(request: CredentialReadRequest): Promise; +}; + +/** The parts of the server plugin's `ctx.integration` the reader uses. */ +export type CredentialIntegration = Pick; + +type Connection = NonNullable>>; +type CredentialConnection = Extract; +type CredentialValue = NonNullable< + Awaited> +>; + +type CredentialFailureCategory = "refresh_failed" | "resolve_empty" | "active_failed"; + +type CredentialFailure = { + at: number; + category: CredentialFailureCategory; + detail: string; + integrationId: string; + label: string; +}; + +export type CredentialSourceDiagnostics = { + state: "bound" | "unbound"; + kind: "opencode-integration-api"; + lastListError?: { at: number; detail: string }; + failures: Array<{ + integrationId: string; + connectionId: string; + label: string; + category: CredentialFailureCategory; + detail: string; + at: number; + }>; +}; + +/** A failed login is resolved again at most this often. */ +const FAILED_LOGIN_RETRY_MS = 60_000; + +/** Bound sources; reads use the last one. */ +const credentialSources: Array<{ source: CredentialSource }> = []; +let unboundWarningShown = false; +/** + * Failed logins, keyed by connection id (by integration id for a failed + * `active()`, which has no connection). An entry lives until that login reads + * again, a credential event arrives, or `notifyCredentialsChanged` runs. + */ +const credentialFailures = new Map(); +/** One `resolve()` at a time per connection id. */ +const pendingResolves = new Map>(); +let lastListError: { at: number; detail: string } | undefined; + /** * Connection labels OpenCode assigns when the user never named the connection: * `default` for a new connection, and `OAuth` / `API key` for credentials @@ -83,14 +144,254 @@ export function formatCredentialDisplayNames( const authCache = new Map(); /** - * OpenCode credential database paths: the one resolved database, or none when - * `OPENCODE_DB` is `:memory:`. + * The database OpenCode keeps its logins in, for diagnostics only (logins are + * read through OpenCode, never from this file): the one resolved path, or none + * when `OPENCODE_DB` is `:memory:`. */ export function getCredentialDatabasePaths(): string[] { const path = getOpenCodeDbPath(); return path === ":memory:" ? [] : [path]; } +/** + * Makes `source` the one reads use until the returned function unbinds it. + * Unbinding removes only this binding; the last binding still bound wins. + */ +export function bindCredentialSource(source: CredentialSource): () => void { + const binding = { source }; + credentialSources.push(binding); + return () => { + const index = credentialSources.indexOf(binding); + if (index !== -1) credentialSources.splice(index, 1); + }; +} + +/** Drops cached logins, failed-login entries and the last `list()` error. */ +export function notifyCredentialsChanged(): void { + credentialFailures.clear(); + authCache.clear(); + lastListError = undefined; +} + +export function getCredentialSourceDiagnostics(): CredentialSourceDiagnostics { + return { + state: credentialSources.length > 0 ? "bound" : "unbound", + kind: "opencode-integration-api", + ...(lastListError ? { lastListError: { ...lastListError } } : {}), + failures: Array.from(credentialFailures, ([connectionId, failure]) => ({ + integrationId: failure.integrationId, + connectionId, + label: failure.label, + category: failure.category, + detail: failure.detail, + at: failure.at, + })), + }; +} + +/** + * Error text that is safe to show and to store: one sanitized line with + * JWT-shaped and long opaque strings (tokens) replaced, at most 120 characters. + */ +export function scrubCredentialErrorText(message: string): string { + // Redact before cutting to length, so a token cut at the limit cannot leave a readable part. + return sanitizeSingleLineDisplayText(message) + .replace(/eyJ[A-Za-z0-9_-]{10,}(\.[A-Za-z0-9_-]+)*/gu, "[redacted]") + .replace(/[A-Za-z0-9_-]{32,}/gu, "[redacted]") + .slice(0, 120); +} + +function credentialErrorDetail(error: unknown): string { + return scrubCredentialErrorText(error instanceof Error ? error.message : String(error)); +} + +type ConnectionSlot = + | { integrationId: string; connection: CredentialConnection } + | { integrationId: string; activeFailure: CredentialFailure }; + +type ResolveOutcome = { value: CredentialValue } | { failure: CredentialFailure }; + +/** The active connection of one integration id; works for ids OpenCode has not registered. */ +async function activeConnectionSlots( + integration: CredentialIntegration, + integrationId: string, +): Promise { + try { + const connection = await integration.connection.active(integrationId); + credentialFailures.delete(integrationId); + // An env connection means no stored login; providers apply their own env rules. + return connection?.type === "credential" ? [{ integrationId, connection }] : []; + } catch (error) { + const activeFailure: CredentialFailure = { + at: Date.now(), + category: "active_failed", + detail: credentialErrorDetail(error), + integrationId, + label: "", + }; + credentialFailures.set(integrationId, activeFailure); + return [{ integrationId, activeFailure }]; + } +} + +/** + * Every connection of the requested ids: all of them for ids `list()` shows + * (registered integrations), the active one for any other id. + */ +async function listedConnectionSlots( + integration: CredentialIntegration, + integrationIds: readonly string[], +): Promise { + let listed: Map | undefined; + try { + const { data } = await integration.list(); + listed = new Map(data.map((info) => [info.id, info.connections])); + lastListError = undefined; + } catch (error) { + // One malformed login anywhere makes list() fail; each id's active login still reads. + lastListError = { at: Date.now(), detail: credentialErrorDetail(error) }; + } + + const slots: ConnectionSlot[] = []; + for (const integrationId of integrationIds) { + const connections = listed?.get(integrationId); + if (connections === undefined) { + slots.push(...(await activeConnectionSlots(integration, integrationId))); + continue; + } + for (const connection of connections) { + if (connection.type === "credential") slots.push({ integrationId, connection }); + } + } + return slots; +} + +function recordResolveFailure( + connection: CredentialConnection, + failure: Omit, +): ResolveOutcome { + const recorded: CredentialFailure = { ...failure, at: Date.now(), label: connection.label }; + credentialFailures.set(connection.id, recorded); + return { failure: recorded }; +} + +/** + * Resolves one connection (OpenCode may refresh its token). A login that failed + * less than a minute ago is not resolved again; concurrent reads share one call. + */ +function resolveCredentialConnection( + integration: CredentialIntegration, + integrationId: string, + connection: CredentialConnection, +): Promise { + const failure = credentialFailures.get(connection.id); + if (failure && Date.now() - failure.at < FAILED_LOGIN_RETRY_MS) { + return Promise.resolve({ failure }); + } + const pending = pendingResolves.get(connection.id); + if (pending) return pending; + + const outcome = (async (): Promise => { + try { + const value = await integration.connection.resolve(connection); + if (value === undefined) { + return recordResolveFailure(connection, { + category: "resolve_empty", + detail: "OpenCode returned no login", + integrationId, + }); + } + credentialFailures.delete(connection.id); + return { value }; + } catch (error) { + return recordResolveFailure(connection, { + category: "refresh_failed", + detail: credentialErrorDetail(error), + integrationId, + }); + } + })().finally(() => { + pendingResolves.delete(connection.id); + }); + pendingResolves.set(connection.id, outcome); + return outcome; +} + +/** The value resolvers read: metadata fields on top (kept under `metadata` too), `key` as `api`. */ +function credentialRowValue(value: CredentialValue): Record { + const flattened: Record = { ...value.metadata, ...value }; + if (flattened.type === "key") flattened.type = "api"; + return flattened; +} + +async function credentialRowForSlot( + integration: CredentialIntegration, + slot: ConnectionSlot, + methods: readonly CredentialMethod[] | undefined, +): Promise { + if ("activeFailure" in slot) { + const failure = slot.activeFailure; + return { + id: slot.integrationId, + integrationId: slot.integrationId, + label: "", + active: false, + // A failed active() has no connection, so its method is unknown: a key-only read + // gets a key-shaped failure, every other read an OAuth-shaped one. + value: { type: methods && !methods.includes("oauth") ? "api" : "oauth" }, + resolveError: `${failure.category}: ${failure.detail}`, + }; + } + + const { connection } = slot; + const outcome = await resolveCredentialConnection(integration, slot.integrationId, connection); + if ("value" in outcome) { + return { + id: connection.id, + integrationId: slot.integrationId, + label: connection.label, + active: false, + value: credentialRowValue(outcome.value), + }; + } + return { + id: connection.id, + integrationId: slot.integrationId, + label: connection.label, + active: false, + value: { type: connection.method === "key" ? "api" : "oauth" }, + resolveError: `${outcome.failure.category}: ${outcome.failure.detail}`, + }; +} + +/** The credential source backed by the server plugin's `ctx.integration`. */ +export function createIntegrationCredentialSource( + integration: CredentialIntegration, +): CredentialSource { + return { + async readRows(request) { + const slots: ConnectionSlot[] = []; + if (request.firstOnly) { + for (const integrationId of request.integrationIds) { + slots.push(...(await activeConnectionSlots(integration, integrationId))); + } + } else { + slots.push(...(await listedConnectionSlots(integration, request.integrationIds))); + } + + const methods = request.methods; + const wanted = slots.filter( + (slot) => !methods || !("connection" in slot) || methods.includes(slot.connection.method), + ); + const rows = await Promise.all( + wanted.map((slot) => credentialRowForSlot(integration, slot, methods)), + ); + // The `*` goes to the first row of the first requested id that has rows. + return rows.map((row, index) => ({ ...row, active: index === 0 })); + }, + }; +} + /** * The auth entry resolvers read for a row: its value, plus `resolveError` * when OpenCode could not return the login. @@ -113,39 +414,24 @@ export async function readAuthFile(params: { return Object.keys(auth).length > 0 ? (auth as AuthData) : null; } -/** The OpenCode 2 method of a parsed credential value (the reader maps a stored `key` to `api`). */ -function credentialMethod(value: Record): CredentialMethod | undefined { - if (value.type === "oauth") return "oauth"; - if (value.type === "api" || value.type === "key") return "key"; - return undefined; -} - /** - * Credential rows of the requested integration ids, in database order - * (active first, then most recently updated). + * Credential rows of the requested integration ids, in request-id order, then + * OpenCode's connection order (active first, then newest). Without a bound + * source (the TUI and the terminal command) there are none. */ export async function readCredentialRows( integrationIds: readonly string[], options: ReadCredentialRowsOptions = {}, ): Promise { - const [path] = getCredentialDatabasePaths(); - const rows = (path ? await readCredentialDatabase(path) : []).filter((row) => - integrationIds.includes(row.integrationId), - ); - const seenIntegrationIds = new Set(); - const firstRows = options.firstOnly - ? rows.filter((row) => { - if (seenIntegrationIds.has(row.integrationId)) return false; - seenIntegrationIds.add(row.integrationId); - return true; - }) - : rows; - const methods = options.methods; - if (!methods) return firstRows; - return firstRows.filter((row) => { - const method = credentialMethod(row.value); - return method !== undefined && methods.includes(method); - }); + const source = credentialSources.at(-1)?.source; + if (!source) { + if (!unboundWarningShown) { + unboundWarningShown = true; + console.warn("[opencode-quota] credential source is not bound"); + } + return []; + } + return source.readRows({ integrationIds, ...options }); } function canonicalCredentialValueKey(value: unknown): string { @@ -170,8 +456,9 @@ function canonicalCredentialValueKey(value: unknown): string { * same workspace key stored under the `opencode-go` integration and its legacy * `opencode` alias), and rows under `primaryIntegrationId` take precedence over * alias rows so a real credential for another integration that shares the key - * is not reported as an additional connection. Input order is preserved - * otherwise. + * is not reported as an additional connection. A failed row holds only + * `{ type }`, so its connection id keeps each failed login a row of its own. + * Input order is preserved otherwise. */ export function selectConnectionCredentialRows( rows: readonly CredentialRow[], @@ -182,7 +469,10 @@ export function selectConnectionCredentialRows( const seen = new Set(); const selected: CredentialRow[] = []; for (const row of candidates) { - const key = canonicalCredentialValueKey(row.value); + const key = + row.resolveError === undefined + ? canonicalCredentialValueKey(row.value) + : canonicalCredentialValueKey({ failedConnectionId: row.id, value: row.value }); if (seen.has(key)) continue; seen.add(key); selected.push(row); @@ -190,67 +480,9 @@ export function selectConnectionCredentialRows( return selected; } -async function readCredentialDatabase(path: string): Promise { - if (!existsSync(path)) return []; - - let database: SqliteConn | undefined; - try { - database = await openOpenCodeSqliteReadOnly(path); - const rows = database.all>( - "SELECT id, integration_id, label, active, value FROM credential WHERE integration_id IS NOT NULL ORDER BY active DESC, time_updated DESC, id DESC", - ); - const credentials: CredentialRow[] = []; - - for (const row of rows) { - if ( - typeof row.id !== "string" || - typeof row.integration_id !== "string" || - typeof row.label !== "string" || - typeof row.value !== "string" - ) - continue; - const value = parseCredentialValue(row.value); - if (value) { - credentials.push({ - id: row.id, - integrationId: row.integration_id, - label: row.label, - active: row.active === 1, - value, - }); - } - } - - return credentials; - } catch { - return []; - } finally { - database?.close(); - } -} - -function parseCredentialValue(value: string): Record | null { - try { - const parsed = JSON.parse(value); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; - const credential = parsed as Record; - const metadata = credential.metadata; - const authEntry = { - ...(metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {}), - ...credential, - }; - if (authEntry.type === "key" && typeof authEntry.key === "string") { - authEntry.type = "api"; - } - return authEntry; - } catch { - return null; - } -} - /** * Cached auth reader for frequently triggered code paths (e.g. per-question hooks). - * This avoids repeated filesystem reads while keeping auth updates visible quickly. + * This avoids asking OpenCode on every call while keeping login changes visible quickly. */ export async function readAuthFileCached(params: { maxAgeMs: number; diff --git a/src/lib/types.ts b/src/lib/types.ts index ea9ff2fc..bdb74a56 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -304,6 +304,8 @@ export interface CopilotAuthData { expires?: number; /** OpenCode-managed GitHub Enterprise Cloud hostname for this OAuth credential. */ enterpriseUrl?: string; + /** Set when OpenCode could not return this login; see `CredentialRow.resolveError`. */ + resolveError?: string; } export type AlibabaCodingPlanTier = "lite" | "pro"; @@ -324,6 +326,8 @@ export interface AnthropicOAuthAuthData { access?: string; refresh?: string; expires?: number; + /** Set when OpenCode could not return this login; see `CredentialRow.resolveError`. */ + resolveError?: string; [key: string]: unknown; } @@ -372,6 +376,8 @@ export interface AlibabaAuthData { access?: string; tier?: string; plan?: string; + /** Set when OpenCode could not return this login; see `CredentialRow.resolveError`. */ + resolveError?: string; [key: string]: unknown; } @@ -399,6 +405,8 @@ export interface MiniMaxAuthData { type: string; key?: string; access?: string; + /** Set when OpenCode could not return this login; see `CredentialRow.resolveError`. */ + resolveError?: string; } /** diff --git a/src/plugin.ts b/src/plugin.ts index bb1f2c94..01bdf174 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,11 +1,17 @@ /** * OpenCode V2 server plugin: quota slash commands for every client, a diagnostics tool, and - * the quota RPC that computes the TUI surfaces and the terminal command's reports. + * the quota RPC that computes the TUI surfaces and the terminal command's reports. It is the + * only place logins are read, through OpenCode's integration API. */ import { Plugin } from "@opencode/plugin"; import { buildCliShowJson, buildCliShowText, buildCliStatus } from "./lib/cli-reports.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; +import { + bindCredentialSource, + createIntegrationCredentialSource, + notifyCredentialsChanged, +} from "./lib/opencode-auth.js"; import { reconcileDetectedProvidersInGlobalConfig } from "./lib/opencode-config-providers.js"; import { QUOTA_DIALOG_COMMANDS, @@ -259,6 +265,28 @@ export const QuotaToastPlugin = Plugin.define({ ); if (!textLeft) event.result = ""; }); + + // Every login read in this process goes through this location's `ctx.integration`. Bound + // last, so a setup that fails part way leaves no binding behind. OpenCode routes tools, + // commands and RPC calls to the plugin only after setup, so no read comes earlier. + const unbindCredentialSource = bindCredentialSource( + createIntegrationCredentialSource(ctx.integration), + ); + // A login added, changed or switched in OpenCode drops cached logins and failed-login + // entries, so the next read asks OpenCode again. + const credentialEvents = new AbortController(); + void (async () => { + for await (const event of ctx.event.subscribe({ signal: credentialEvents.signal })) { + if (event.type === "credential.updated" || event.type === "credential.switched") { + notifyCredentialsChanged(); + } + } + })().catch(() => {}); + + return () => { + credentialEvents.abort(); + unbindCredentialSource(); + }; }, }); diff --git a/src/providers/anthropic.ts b/src/providers/anthropic.ts index a41a0362..24eb8bfd 100644 --- a/src/providers/anthropic.ts +++ b/src/providers/anthropic.ts @@ -18,7 +18,11 @@ import type { QuotaProviderResult, QuotaToastEntry, } from "../lib/entries.js"; -import { formatCredentialDisplayNames, readCredentialRows } from "../lib/opencode-auth.js"; +import { + credentialRowAuthEntry, + formatCredentialDisplayNames, + readCredentialRows, +} from "../lib/opencode-auth.js"; import { isCanonicalProviderAvailable } from "../lib/provider-availability.js"; import type { AuthData } from "../lib/types.js"; import { @@ -62,12 +66,13 @@ export const anthropicProvider: QuotaProvider = { }; let statusDetails; let acquisitionMethod: QuotaToastEntry["accounting"]["acquisitionMethod"] = "local_cli"; + // A failed login stays in the list so it shows as its own error row. const databaseCredentials = ( await readCredentialRows(["anthropic"], { methods: ["oauth"] }) ).flatMap((row) => { if (row.integrationId !== "anthropic") return []; - const auth = resolveAnthropicOAuth({ anthropic: row.value } as AuthData); - return auth.state === "configured" ? [{ row, auth }] : []; + const auth = resolveAnthropicOAuth({ anthropic: credentialRowAuthEntry(row) } as AuthData); + return auth.state === "configured" || auth.state === "failed" ? [{ row, auth }] : []; }); try { const diagnostics = await getAnthropicDiagnostics(options); @@ -105,7 +110,13 @@ export const anthropicProvider: QuotaProvider = { const results = await Promise.all( databaseCredentials.map(async ({ row, auth }) => ({ row, - result: await queryAnthropicQuotaWithOAuth(auth.accessToken, options.requestTimeoutMs), + result: + auth.state === "failed" + ? { + success: false as const, + error: `Anthropic sign-in could not be read: ${auth.error}. Run \`opencode auth login anthropic\`.`, + } + : await queryAnthropicQuotaWithOAuth(auth.accessToken, options.requestTimeoutMs), })), ); const names = formatCredentialDisplayNames( diff --git a/src/providers/copilot.ts b/src/providers/copilot.ts index 134216de..e2ac2ccd 100644 --- a/src/providers/copilot.ts +++ b/src/providers/copilot.ts @@ -19,6 +19,7 @@ import type { QuotaToastEntry, } from "../lib/entries.js"; import { + credentialRowAuthEntry, formatCredentialDisplayNames, readAuthFileCached, readCredentialRows, @@ -314,12 +315,13 @@ export const copilotProvider: QuotaProvider = { ), ); if (credentials.length > 0) { + // A failed login stays in the list so it shows as its own error row. const results = await Promise.all( credentials.map(async (row) => ({ row, result: await queryCopilotQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs, - authData: { [row.integrationId]: row.value } as AuthData, + authData: { [row.integrationId]: credentialRowAuthEntry(row) } as AuthData, }), })), ); diff --git a/tests/helpers/fake-integration.ts b/tests/helpers/fake-integration.ts new file mode 100644 index 00000000..4d06b17c --- /dev/null +++ b/tests/helpers/fake-integration.ts @@ -0,0 +1,84 @@ +import { vi } from "vitest"; + +/** One stored OpenCode login, in the shape OpenCode 2 keeps it. */ +export type FakeCredential = { + integrationId: string; + /** Connection id (`cred_…` in OpenCode). */ + id: string; + label: string; + /** Whether `list()` shows this integration; unregistered (alias) ids answer only `active()`. */ + registered: boolean; + method: "key" | "oauth"; + /** The stored value: `{ type: "key", key, metadata? }` or `{ type: "oauth", ..., metadata? }`. */ + value: Record; + /** When set, `resolve()` rejects with this message (a failed token refresh). */ + resolveError?: string; +}; + +type FakeConnection = + | { type: "credential"; id: string; label: string; method: "key" | "oauth" } + | { type: "env"; name: string }; + +const FAKE_LOCATION = { directory: "/tmp/opencode-quota-fake-location" }; + +/** + * A fake of the server plugin's `ctx.integration` with OpenCode 2.0.16's Promise shapes: + * `list()` and `get()` return `{ location, data }` envelopes and `get()` rejects for an + * unregistered id; `connection.active()` and `connection.resolve()` return bare values. + * Credentials are listed in the given order, which stands for OpenCode's own order (active + * first, then newest). `envNames` adds env connections to registered ids, after their + * credentials, the way OpenCode does. Every method is a `vi.fn`, so tests can count calls + * or make one reject. + */ +export function createFakeIntegration( + credentials: FakeCredential[], + options: { envNames?: Record } = {}, +) { + const envNames = options.envNames ?? {}; + const registeredIds = [ + ...new Set([ + ...credentials.filter((credential) => credential.registered).map((c) => c.integrationId), + ...Object.keys(envNames), + ]), + ]; + const connectionsFor = (integrationId: string): FakeConnection[] => [ + ...credentials + .filter((credential) => credential.integrationId === integrationId) + .map((credential) => ({ + type: "credential" as const, + id: credential.id, + label: credential.label, + method: credential.method, + })), + ...(registeredIds.includes(integrationId) && envNames[integrationId] + ? [{ type: "env" as const, name: envNames[integrationId] }] + : []), + ]; + const info = (integrationId: string) => ({ + id: integrationId, + name: integrationId, + methods: [], + connections: connectionsFor(integrationId), + }); + + const integration = { + list: vi.fn(async () => ({ location: FAKE_LOCATION, data: registeredIds.map(info) })), + get: vi.fn(async ({ integrationID }: { integrationID: string }) => { + if (!registeredIds.includes(integrationID)) { + throw new Error(`Integration not found: ${integrationID}`); + } + return { location: FAKE_LOCATION, data: info(integrationID) }; + }), + connection: { + active: vi.fn(async (integrationID: string) => connectionsFor(integrationID)[0]), + resolve: vi.fn(async (connection: FakeConnection) => { + if (connection.type === "env") return { type: "key", key: `env:${connection.name}` }; + const credential = credentials.find((candidate) => candidate.id === connection.id); + if (!credential) return undefined; + if (credential.resolveError !== undefined) throw new Error(credential.resolveError); + return structuredClone(credential.value); + }), + }, + }; + return integration; +} diff --git a/tests/lib.alibaba-auth.test.ts b/tests/lib.alibaba-auth.test.ts index eeb7e6b4..cc37f147 100644 --- a/tests/lib.alibaba-auth.test.ts +++ b/tests/lib.alibaba-auth.test.ts @@ -169,6 +169,18 @@ describe("alibaba auth resolution", () => { expect(hasAlibabaAuth(auth as any)).toBe(false); }); + it("returns OpenCode's reason for a login OpenCode could not read", () => { + const auth = { + "alibaba-coding-plan": { type: "api", resolveError: "refresh_failed: HTTP 500" }, + alibaba: { type: "api", key: "alias-key" }, + }; + + expect(resolveAlibabaCodingPlanAuth(auth as any)).toEqual({ + state: "invalid", + error: "OpenCode could not read this login: refresh_failed: HTTP 500", + }); + }); + it("returns invalid for malformed auth entries", () => { expect(resolveAlibabaCodingPlanAuth({ alibaba: "bad-shape" } as any)).toEqual({ state: "invalid", diff --git a/tests/lib.anthropic-auth.test.ts b/tests/lib.anthropic-auth.test.ts index 0a62c8b9..8a474fc1 100644 --- a/tests/lib.anthropic-auth.test.ts +++ b/tests/lib.anthropic-auth.test.ts @@ -67,6 +67,14 @@ describe("anthropic auth resolution", () => { }); }); + it("reports a login OpenCode could not return as failed", () => { + expect( + resolveAnthropicOAuth({ + anthropic: { type: "oauth", resolveError: "refresh_failed: HTTP 500" }, + }), + ).toEqual({ state: "failed", error: "refresh_failed: HTTP 500" }); + }); + it("returns none when auth data is missing", () => { expect(resolveAnthropicOAuth(null)).toEqual({ state: "none" }); expect(resolveAnthropicOAuth({})).toEqual({ state: "none" }); diff --git a/tests/lib.anthropic.test.ts b/tests/lib.anthropic.test.ts index ea70eca5..1ea5c862 100644 --- a/tests/lib.anthropic.test.ts +++ b/tests/lib.anthropic.test.ts @@ -414,6 +414,17 @@ describe("Claude CLI diagnostics", () => { expect(identity).not.toContain("opencode-access-secret"); }); + it("counts an OpenCode login it could not read as present but derives no identity from it", async () => { + mockExecSequence([{ code: "ENOENT", errorMessage: "spawn claude ENOENT" }]); + readAuthFileCachedMock.mockResolvedValue({ + anthropic: { type: "oauth", resolveError: "refresh_failed: HTTP 500" }, + }); + + await expect(hasAnthropicCredentialsConfigured()).resolves.toBe(true); + await expect(resolveAnthropicAuthIdentity()).resolves.toBeNull(); + expect(identityMocks.deriveResolvedAuthIdentity).not.toHaveBeenCalled(); + }); + it("composes both credentials when authenticated Claude may receive OAuth fallback", async () => { setProcessPlatform("linux"); mockExecSequence(authenticatedWithoutQuotaSteps(1)); diff --git a/tests/lib.copilot.test.ts b/tests/lib.copilot.test.ts index 6988e2c8..040174df 100644 --- a/tests/lib.copilot.test.ts +++ b/tests/lib.copilot.test.ts @@ -232,6 +232,27 @@ describe("GitHub Copilot AI Credit accounting", () => { }); }); + it("shows a login OpenCode could not read as an error, present but without identity", async () => { + authMocks.readAuthFile.mockResolvedValue({ + "github-copilot": { type: "oauth", resolveError: "refresh_failed: HTTP 401" }, + copilot: { type: "oauth", access: "alias-token" }, + }); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock as any); + const { hasCopilotQuotaRuntimeAvailable, queryCopilotQuota, resolveCopilotAuthIdentity } = + await import("../src/lib/copilot.js"); + + await expect(queryCopilotQuota()).resolves.toEqual({ + success: false, + error: + "Copilot sign-in could not be read: refresh_failed: HTTP 401. Run `opencode auth login github-copilot`.", + }); + await expect(hasCopilotQuotaRuntimeAvailable()).resolves.toBe(true); + await expect(resolveCopilotAuthIdentity()).resolves.toBeNull(); + expect(identityMocks.deriveResolvedAuthIdentity).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + }); + it("derives only absent Copilot percentages and preserves reported values", async () => { authMocks.readAuthFile.mockResolvedValue({ "github-copilot": { type: "oauth", access: "oauth-token" }, diff --git a/tests/lib.minimax-auth.test.ts b/tests/lib.minimax-auth.test.ts index 111aedb3..40954e37 100644 --- a/tests/lib.minimax-auth.test.ts +++ b/tests/lib.minimax-auth.test.ts @@ -114,6 +114,14 @@ describe("minimax auth resolution", () => { withMiniMaxAuth({ type: "api", key: "", access: "access-token" }), { state: "invalid", error: "MiniMax auth entry present but key is empty" }, ], + [ + "OpenCode could not read the login", + withMiniMaxAuth({ type: "api", resolveError: "refresh_failed: HTTP 500" }), + { + state: "invalid", + error: "OpenCode could not read this login: refresh_failed: HTTP 500", + }, + ], ])("returns %j when %s", (_label, auth, expected) => { expect(resolveMiniMaxAuth(auth as any)).toEqual(expected); }); @@ -316,6 +324,17 @@ describe("minimax auth resolution", () => { }); }); + it("returns OpenCode's reason for a MiniMax China login OpenCode could not read", () => { + expect( + resolveMiniMaxChinaAuth( + withMiniMaxChinaAuth({ type: "api", resolveError: "active_failed: database is locked" }), + ), + ).toEqual({ + state: "invalid", + error: "OpenCode could not read this login: active_failed: database is locked", + }); + }); + it("returns invalid for MiniMax China access-only opencode.db", () => { expect( resolveMiniMaxChinaAuth(withMiniMaxChinaAuth({ type: "api", access: "china-token" })), diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index a8616328..3d84320f 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -1,85 +1,87 @@ -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { DatabaseSync } from "node:sqlite"; import { afterEach, describe, expect, it, vi } from "vitest"; import { resolveAgyAccounts } from "../src/lib/google-agy.js"; import { resolveGeminiCliAccounts } from "../src/lib/google-gemini-cli.js"; import { + bindCredentialSource, clearReadAuthFileCacheForTests, + createIntegrationCredentialSource, credentialRowAuthEntry, formatCredentialDisplayNames, getCredentialDatabasePaths, + getCredentialSourceDiagnostics, + notifyCredentialsChanged, readAuthFile, readAuthFileCached, readCredentialRows, + scrubCredentialErrorText, selectConnectionCredentialRows, } from "../src/lib/opencode-auth.js"; +import { createFakeIntegration, type FakeCredential } from "./helpers/fake-integration.js"; -const temporaryDirectories: string[] = []; +const unbinds: Array<() => void> = []; -afterEach(async () => { +afterEach(() => { + for (const unbind of unbinds.splice(0)) unbind(); + notifyCredentialsChanged(); clearReadAuthFileCacheForTests(); vi.useRealTimers(); vi.unstubAllEnvs(); - await Promise.all( - temporaryDirectories - .splice(0) - .map((directory) => rm(directory, { recursive: true, force: true })), - ); }); -async function createCredentialDatabase(): Promise<{ dataDir: string; databasePath: string }> { - const root = await mkdtemp(join(tmpdir(), "opencode-quota-auth-")); - temporaryDirectories.push(root); - const dataDir = join(root, "opencode"); - await mkdir(dataDir, { recursive: true }); - const databasePath = join(dataDir, "opencode.db"); - const database = new DatabaseSync(databasePath); - database.exec(`CREATE TABLE credential ( - id TEXT PRIMARY KEY, - integration_id TEXT, - label TEXT NOT NULL, - value TEXT NOT NULL, - connector_id TEXT, - method_id TEXT, - active INTEGER, - time_created INTEGER NOT NULL, - time_updated INTEGER NOT NULL - )`); - database - .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, NULL, 1, ?)") - .run( - "copilot", - "github-copilot", - JSON.stringify({ - type: "oauth", - access: "copilot-access", - refresh: "copilot-refresh", - expires: 10, - metadata: { enterpriseUrl: "example.ghe.com" }, - }), - 1, - ); - database - .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, NULL, 1, ?)") - .run( - "openai", - "openai", - JSON.stringify({ - type: "oauth", - access: "openai-access", - refresh: "openai-refresh", - expires: 20, - }), - 2, - ); - database - .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, NULL, 1, ?)") - .run("deepseek", "deepseek", JSON.stringify({ type: "key", key: "deepseek-key" }), 3); - database.close(); - return { dataDir, databasePath }; +function bindFakeIntegration( + credentials: FakeCredential[], + options?: Parameters[1], +) { + const integration = createFakeIntegration(credentials, options); + unbinds.push(bindCredentialSource(createIntegrationCredentialSource(integration as never))); + return integration; +} + +function oauthCredential( + integrationId: string, + id: string, + overrides: Partial = {}, +): FakeCredential { + return { + integrationId, + id, + label: "default", + registered: true, + method: "oauth", + value: { + type: "oauth", + methodID: "device", + refresh: `${id}-refresh`, + access: `${id}-access`, + expires: 10, + }, + ...overrides, + }; +} + +function keyCredential( + integrationId: string, + id: string, + overrides: Partial = {}, +): FakeCredential { + return { + integrationId, + id, + label: "default", + registered: true, + method: "key", + value: { type: "key", key: `${id}-key` }, + ...overrides, + }; +} + +function resolvedIds(integration: ReturnType): string[] { + return integration.connection.resolve.mock.calls.map(([connection]) => + connection.type === "credential" ? connection.id : `env:${connection.name}`, + ); } describe("OpenCode auth reader", () => { @@ -141,200 +143,463 @@ describe("OpenCode auth reader", () => { ).toEqual(["[Z.ai Work]*", "[Z.ai]"]); }); - it("reports the credential database path and honors OPENCODE_DB", async () => { - const { dataDir } = await createCredentialDatabase(); - vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); + it("reports the credential database path and honors OPENCODE_DB", () => { + const dataHome = join(tmpdir(), "opencode-quota-auth-paths"); + vi.stubEnv("XDG_DATA_HOME", dataHome); - expect(getCredentialDatabasePaths()).toEqual([join(dataDir, "opencode.db")]); + expect(getCredentialDatabasePaths()).toEqual([join(dataHome, "opencode", "opencode.db")]); vi.stubEnv("OPENCODE_DB", "custom.db"); - expect(getCredentialDatabasePaths()).toEqual([join(dataDir, "custom.db")]); + expect(getCredentialDatabasePaths()).toEqual([join(dataHome, "opencode", "custom.db")]); }); +}); - it("falls back to OAuth credentials stored in OpenCode's database", async () => { - const { dataDir } = await createCredentialDatabase(); - vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); +describe("integration credential source", () => { + it("reads every connection of a registered id in OpenCode order, values flattened", async () => { + bindFakeIntegration([ + oauthCredential("openai", "cred_personal", { + label: "Personal", + value: { + type: "oauth", + methodID: "chatgpt-browser", + refresh: "personal-refresh", + access: "personal-access", + expires: 20, + metadata: { accountID: "acct_1" }, + }, + }), + oauthCredential("openai", "cred_work", { label: "Work" }), + keyCredential("deepseek", "cred_deepseek", { + value: { type: "key", key: "deepseek-key", metadata: { region: "eu" } }, + }), + ]); - await expect( - readAuthFile({ integrationIds: ["github-copilot", "deepseek", "openai"] }), - ).resolves.toMatchObject({ - "github-copilot": { access: "copilot-access", enterpriseUrl: "example.ghe.com" }, - deepseek: { type: "api", key: "deepseek-key" }, - openai: { access: "openai-access" }, + await expect(readCredentialRows(["openai", "deepseek"])).resolves.toEqual([ + { + id: "cred_personal", + integrationId: "openai", + label: "Personal", + active: true, + value: { + accountID: "acct_1", + type: "oauth", + methodID: "chatgpt-browser", + refresh: "personal-refresh", + access: "personal-access", + expires: 20, + metadata: { accountID: "acct_1" }, + }, + }, + { + id: "cred_work", + integrationId: "openai", + label: "Work", + active: false, + value: { + type: "oauth", + methodID: "device", + refresh: "cred_work-refresh", + access: "cred_work-access", + expires: 10, + }, + }, + { + id: "cred_deepseek", + integrationId: "deepseek", + label: "default", + active: false, + value: { region: "eu", type: "api", key: "deepseek-key", metadata: { region: "eu" } }, + }, + ]); + }); + + it("keeps request-id order across integrations", async () => { + bindFakeIntegration([ + oauthCredential("openai", "cred_openai"), + oauthCredential("github-copilot", "cred_copilot"), + ]); + + expect((await readCredentialRows(["github-copilot", "openai"])).map((row) => row.id)).toEqual([ + "cred_copilot", + "cred_openai", + ]); + await expect(readCredentialRows([])).resolves.toEqual([]); + }); + + it("reads an id OpenCode has not registered through active()", async () => { + const integration = bindFakeIntegration([ + oauthCredential("openai", "cred_openai"), + oauthCredential("codex", "cred_codex_newest", { registered: false }), + oauthCredential("codex", "cred_codex_older", { registered: false }), + ]); + + expect((await readCredentialRows(["openai", "codex"])).map((row) => row.id)).toEqual([ + "cred_openai", + "cred_codex_newest", + ]); + expect(integration.list).toHaveBeenCalledOnce(); + expect(integration.connection.active.mock.calls).toEqual([["codex"]]); + expect(integration.get).not.toHaveBeenCalled(); + }); + + it("skips env connections, which are not stored logins", async () => { + const integration = bindFakeIntegration([keyCredential("openrouter", "cred_openrouter")], { + envNames: { openrouter: "OPENROUTER_API_KEY", deepseek: "DEEPSEEK_API_KEY" }, }); + + expect((await readCredentialRows(["openrouter", "deepseek"])).map((row) => row.id)).toEqual([ + "cred_openrouter", + ]); + await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toBeNull(); + expect(resolvedIds(integration)).toEqual(["cred_openrouter"]); }); - it("exposes every credential row with active rows first", async () => { - const { databasePath } = await createCredentialDatabase(); - const database = new DatabaseSync(databasePath); - database.prepare("UPDATE credential SET label = 'Work', active = 0 WHERE id = 'openai'").run(); - database - .prepare("INSERT INTO credential VALUES (?, ?, ?, ?, NULL, NULL, ?, ?, ?)") - .run( - "openai-active", - "openai", - "Personal", - JSON.stringify({ type: "oauth", access: "personal-access" }), - 1, - 4, - 4, - ); - database.close(); - vi.stubEnv("OPENCODE_DB", databasePath); - - await expect(readCredentialRows(["openai"])).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "openai-active", - integrationId: "openai", - label: "Personal", - active: true, - value: expect.objectContaining({ access: "personal-access" }), - }), - expect.objectContaining({ id: "openai", active: false }), - ]), - ); - const rows = await readCredentialRows(["openai"]); - expect(rows.findIndex((row) => row.id === "openai-active")).toBeLessThan( - rows.findIndex((row) => row.id === "openai"), - ); + it("checks the connection method before resolving", async () => { + const integration = bindFakeIntegration([ + keyCredential("opencode-go", "cred_go"), + oauthCredential("opencode", "cred_console"), + keyCredential("opencode", "cred_workspace"), + ]); + + expect( + (await readCredentialRows(["opencode-go", "opencode"], { methods: ["key"] })).map( + (row) => row.id, + ), + ).toEqual(["cred_go", "cred_workspace"]); + // The Go key read never resolves (and so never refreshes) the Console sign-in. + expect(resolvedIds(integration)).toEqual(["cred_go", "cred_workspace"]); + + expect( + (await readCredentialRows(["opencode"], { methods: ["oauth"] })).map((row) => row.id), + ).toEqual(["cred_console"]); }); - it("ignores legacy auth.json entries", async () => { - const { dataDir } = await createCredentialDatabase(); - vi.stubEnv("XDG_DATA_HOME", join(dataDir, "..")); - await writeFile( - join(dataDir, "auth.json"), - JSON.stringify({ openai: { type: "oauth", access: "file-access" } }), - ); + it("reads one active connection per id with firstOnly and never lists", async () => { + const integration = bindFakeIntegration([ + oauthCredential("openai", "cred_active"), + oauthCredential("openai", "cred_inactive"), + keyCredential("deepseek", "cred_deepseek"), + oauthCredential("codex", "cred_codex", { registered: false }), + ]); + expect( + (await readCredentialRows(["openai", "deepseek", "codex"], { firstOnly: true })).map( + (row) => row.id, + ), + ).toEqual(["cred_active", "cred_deepseek", "cred_codex"]); + expect(integration.list).not.toHaveBeenCalled(); + expect(integration.connection.active.mock.calls).toEqual([["openai"], ["deepseek"], ["codex"]]); + expect(resolvedIds(integration)).toEqual(["cred_active", "cred_deepseek", "cred_codex"]); + + // A mismatched active connection yields nothing and is not resolved. await expect( - readAuthFile({ integrationIds: ["github-copilot", "openai"] }), - ).resolves.toMatchObject({ - "github-copilot": { access: "copilot-access" }, - openai: { access: "openai-access" }, - }); + readCredentialRows(["deepseek"], { firstOnly: true, methods: ["oauth"] }), + ).resolves.toEqual([]); + expect(resolvedIds(integration)).toHaveLength(3); }); -}); -describe("reader requests name their integration ids", () => { - async function addRow( - databasePath: string, - row: { id: string; integrationId: string; active: number; updated: number; value: unknown }, - ): Promise { - const database = new DatabaseSync(databasePath); - database - .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, ?, 1, ?)") - .run(row.id, row.integrationId, JSON.stringify(row.value), row.active, row.updated); - database.close(); - } - - it("returns only rows of the requested ids, in database order", async () => { - const { databasePath } = await createCredentialDatabase(); - vi.stubEnv("OPENCODE_DB", databasePath); - - expect((await readCredentialRows(["openai"])).map((row) => row.id)).toEqual(["openai"]); - // Database order (most recently updated first), not request order. - expect((await readCredentialRows(["github-copilot", "openai"])).map((row) => row.id)).toEqual([ - "openai", - "copilot", + it("reads each id's active login when list() fails, until list() works again", async () => { + const integration = bindFakeIntegration([ + oauthCredential("openai", "cred_active"), + oauthCredential("openai", "cred_inactive"), ]); - await expect(readCredentialRows([])).resolves.toEqual([]); - await expect(readAuthFile({ integrationIds: ["xai"] })).resolves.toBeNull(); - await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toEqual({ - openai: { type: "oauth", access: "openai-access", refresh: "openai-refresh", expires: 20 }, + integration.list.mockRejectedValueOnce(new Error("Credential value failed to decode")); + + expect((await readCredentialRows(["openai"])).map((row) => row.id)).toEqual(["cred_active"]); + expect(getCredentialSourceDiagnostics().lastListError).toEqual({ + at: expect.any(Number), + detail: "Credential value failed to decode", }); + + expect((await readCredentialRows(["openai"])).map((row) => row.id)).toEqual([ + "cred_active", + "cred_inactive", + ]); + expect(getCredentialSourceDiagnostics().lastListError).toBeUndefined(); + }); + + it("stars only the first row of the first requested id that has rows", async () => { + bindFakeIntegration( + [ + oauthCredential("codex", "cred_codex", { registered: false }), + oauthCredential("chatgpt", "cred_chatgpt_1"), + oauthCredential("chatgpt", "cred_chatgpt_2"), + ], + { envNames: { openai: "OPENAI_API_KEY" } }, + ); + + const rows = await readCredentialRows(["openai", "codex", "chatgpt"]); + expect(rows.map((row) => [row.id, row.active])).toEqual([ + ["cred_codex", true], + ["cred_chatgpt_1", false], + ["cred_chatgpt_2", false], + ]); }); - it("keeps only the requested connection methods", async () => { - const { databasePath } = await createCredentialDatabase(); - await addRow(databasePath, { - id: "console", - integrationId: "opencode", - active: 1, - updated: 5, - value: { type: "oauth", access: "console-access", refresh: "console-refresh", expires: 1 }, + it("keeps a failed login in place and resolves it again only after a minute", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(1_000_000); + const failing = oauthCredential("openai", "cred_failing", { + label: "Work", + resolveError: "Token refresh failed: 400", }); - await addRow(databasePath, { - id: "workspace-key", - integrationId: "opencode", - active: 0, - updated: 4, - value: { type: "key", key: "workspace-key" }, + const integration = bindFakeIntegration([failing, oauthCredential("openai", "cred_ok")]); + + const rows = await readCredentialRows(["openai"], { methods: ["oauth"] }); + expect(rows).toEqual([ + { + id: "cred_failing", + integrationId: "openai", + label: "Work", + active: true, + value: { type: "oauth" }, + resolveError: "refresh_failed: Token refresh failed: 400", + }, + expect.objectContaining({ id: "cred_ok", active: false }), + ]); + // First row still wins: the failed active login is not replaced by the next one. + await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toEqual({ + openai: { type: "oauth", resolveError: "refresh_failed: Token refresh failed: 400" }, }); - vi.stubEnv("OPENCODE_DB", databasePath); + expect(getCredentialSourceDiagnostics().failures).toEqual([ + { + integrationId: "openai", + connectionId: "cred_failing", + label: "Work", + category: "refresh_failed", + detail: "Token refresh failed: 400", + at: 1_000_000, + }, + ]); + const failingResolves = () => + resolvedIds(integration).filter((id) => id === "cred_failing").length; + expect(failingResolves()).toBe(1); + + vi.setSystemTime(1_000_000 + 59_999); + await readCredentialRows(["openai"]); + expect(failingResolves()).toBe(1); + + vi.setSystemTime(1_000_000 + 60_000); + await readCredentialRows(["openai"]); + expect(failingResolves()).toBe(2); + + failing.resolveError = undefined; + vi.setSystemTime(1_000_000 + 120_000); + const recovered = await readCredentialRows(["openai"]); + expect(recovered[0]).toMatchObject({ + id: "cred_failing", + value: { access: "cred_failing-access" }, + }); + expect(recovered[0]?.resolveError).toBeUndefined(); + expect(getCredentialSourceDiagnostics().failures).toEqual([]); + }); - const ids = ["opencode", "openai", "deepseek"]; - expect((await readCredentialRows(ids, { methods: ["key"] })).map((row) => row.id)).toEqual([ - "workspace-key", - "deepseek", + it("forgets failed logins when OpenCode reports a credential change", async () => { + const integration = bindFakeIntegration([ + oauthCredential("xai", "cred_xai", { resolveError: "refresh rejected" }), ]); - expect((await readCredentialRows(ids, { methods: ["oauth"] })).map((row) => row.id)).toEqual([ - "console", - "openai", + + await readCredentialRows(["xai"]); + await readCredentialRows(["xai"]); + expect(resolvedIds(integration)).toEqual(["cred_xai"]); + + notifyCredentialsChanged(); + expect(getCredentialSourceDiagnostics().failures).toEqual([]); + await readCredentialRows(["xai"]); + expect(resolvedIds(integration)).toEqual(["cred_xai", "cred_xai"]); + }); + + it("reports an empty resolve as a failed login", async () => { + const integration = bindFakeIntegration([keyCredential("deepseek", "cred_deepseek")]); + integration.connection.resolve.mockResolvedValueOnce(undefined); + + await expect(readCredentialRows(["deepseek"])).resolves.toEqual([ + { + id: "cred_deepseek", + integrationId: "deepseek", + label: "default", + active: true, + value: { type: "api" }, + resolveError: "resolve_empty: OpenCode returned no login", + }, ]); - expect( - (await readCredentialRows(ids, { methods: ["key", "oauth"] })).map((row) => row.id), - ).toEqual(["console", "workspace-key", "deepseek", "openai"]); }); - it("keeps the first (active) row per id when firstOnly is set", async () => { - const { databasePath } = await createCredentialDatabase(); - const database = new DatabaseSync(databasePath); - database.prepare("UPDATE credential SET active = 1 WHERE id = 'openai'").run(); - database.close(); - await addRow(databasePath, { - id: "openai-newer-inactive", - integrationId: "openai", - active: 0, - updated: 9, - value: { type: "oauth", access: "inactive-access" }, - }); - vi.stubEnv("OPENCODE_DB", databasePath); + it("reports a failed active() as a failed row of that id until active() works again", async () => { + const integration = bindFakeIntegration([oauthCredential("openai", "cred_openai")]); + integration.connection.active.mockRejectedValueOnce(new Error("database is locked")); + await expect(readCredentialRows(["openai"], { firstOnly: true })).resolves.toEqual([ + { + id: "openai", + integrationId: "openai", + label: "", + active: true, + value: { type: "oauth" }, + resolveError: "active_failed: database is locked", + }, + ]); + expect(getCredentialSourceDiagnostics().failures).toEqual([ + expect.objectContaining({ + integrationId: "openai", + connectionId: "openai", + category: "active_failed", + detail: "database is locked", + }), + ]); + + integration.connection.active.mockRejectedValueOnce(new Error("database is locked")); + await expect( + readCredentialRows(["opencode-go"], { firstOnly: true, methods: ["key"] }), + ).resolves.toEqual([expect.objectContaining({ value: { type: "api" } })]); + + expect((await readCredentialRows(["openai"], { firstOnly: true }))[0]?.id).toBe("cred_openai"); expect( - (await readCredentialRows(["openai", "deepseek"], { firstOnly: true })).map((row) => row.id), - ).toEqual(["openai", "deepseek"]); - await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toMatchObject({ - openai: { access: "openai-access" }, + getCredentialSourceDiagnostics().failures.map((failure) => failure.connectionId), + ).toEqual(["opencode-go"]); + }); + + it("shares one resolve per connection between concurrent reads", async () => { + const integration = bindFakeIntegration([oauthCredential("openai", "cred_openai")]); + let release!: () => void; + const gate = new Promise((resolve) => { + release = resolve; }); + integration.connection.resolve.mockImplementationOnce(async () => { + await gate; + return { type: "oauth", methodID: "device", refresh: "r", access: "shared", expires: 1 }; + }); + + const reads = Promise.all([ + readCredentialRows(["openai"]), + readCredentialRows(["openai"], { firstOnly: true }), + ]); + await vi.waitFor(() => expect(integration.connection.resolve).toHaveBeenCalledOnce()); + // Let the second read reach the shared resolve before the first one finishes. + await new Promise((resolve) => setTimeout(resolve, 0)); + release(); + const [fanOut, firstOnly] = await reads; + + expect(integration.connection.resolve).toHaveBeenCalledOnce(); + expect(fanOut[0]?.value.access).toBe("shared"); + expect(firstOnly[0]?.value.access).toBe("shared"); }); - it("applies firstOnly before the method filter, so a mismatched active row yields nothing", async () => { - const { databasePath } = await createCredentialDatabase(); - await addRow(databasePath, { - id: "openai-key", - integrationId: "openai", - active: 1, - updated: 9, - value: { type: "key", key: "openai-key" }, + it("never stores or shows a token from OpenCode's error text", async () => { + const jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.c2lnbmF0dXJlLXZhbHVl"; + const opaque = "sk_live_0123456789abcdefghijklmnopqrstuv"; + const integration = bindFakeIntegration([ + oauthCredential("openai", "cred_openai", { + resolveError: `refresh failed\n for ${jwt} using \u001b[31m${opaque}`, + }), + ]); + integration.list.mockRejectedValueOnce(new Error(`bad row ${opaque}`)); + + const rows = await readCredentialRows(["openai"]); + const diagnostics = JSON.stringify(getCredentialSourceDiagnostics()); + + expect(rows[0]?.resolveError).toBe( + "refresh_failed: refresh failed for [redacted] using [redacted]", + ); + for (const text of [rows[0]?.resolveError ?? "", diagnostics]) { + expect(text).not.toContain(jwt); + expect(text).not.toContain(opaque); + expect(text).not.toContain(opaque.slice(-24)); + } + }); + + it("scrubs error text to one short line before cutting it to length", () => { + expect(scrubCredentialErrorText("a\nb\tc\u0007")).toBe("a b c"); + const cut = scrubCredentialErrorText(`${"x ".repeat(55)}${"t".repeat(40)}`); + expect(cut).toBe(`${"x ".repeat(55)}[redacted]`.slice(0, 120)); + expect(cut).not.toContain("tttt"); + }); + + it("reads no login and warns once while no source is bound", async () => { + vi.resetModules(); + const unboundReader = await import("../src/lib/opencode-auth.js"); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await expect(unboundReader.readCredentialRows(["openai"])).resolves.toEqual([]); + await expect(unboundReader.readAuthFile({ integrationIds: ["openai"] })).resolves.toBeNull(); + await expect( + unboundReader.readAuthFileCached({ maxAgeMs: 0, integrationIds: ["openai"] }), + ).resolves.toBeNull(); + + expect(warn.mock.calls).toEqual([["[opencode-quota] credential source is not bound"]]); + expect(unboundReader.getCredentialSourceDiagnostics()).toEqual({ + state: "unbound", + kind: "opencode-integration-api", + failures: [], }); - vi.stubEnv("OPENCODE_DB", databasePath); + warn.mockRestore(); + }); + + it("reads through the last binding and unbinds only its own", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const first = createFakeIntegration([keyCredential("deepseek", "cred_first")]); + const second = createFakeIntegration([keyCredential("deepseek", "cred_second")]); + const unbindFirst = bindCredentialSource(createIntegrationCredentialSource(first as never)); + const unbindSecond = bindCredentialSource(createIntegrationCredentialSource(second as never)); + + expect((await readCredentialRows(["deepseek"]))[0]?.id).toBe("cred_second"); + unbindFirst(); + expect((await readCredentialRows(["deepseek"]))[0]?.id).toBe("cred_second"); + expect(getCredentialSourceDiagnostics().state).toBe("bound"); + unbindSecond(); + unbindSecond(); + await expect(readCredentialRows(["deepseek"])).resolves.toEqual([]); + expect(getCredentialSourceDiagnostics().state).toBe("unbound"); + warn.mockRestore(); + }); +}); + +describe("readAuthFile and readAuthFileCached", () => { + it("maps the first login of each requested id, metadata on top", async () => { + bindFakeIntegration([ + oauthCredential("github-copilot", "cred_copilot", { + value: { + type: "oauth", + methodID: "device", + access: "copilot-access", + refresh: "copilot-access", + expires: 0, + metadata: { enterpriseUrl: "example.ghe.com" }, + }, + }), + keyCredential("deepseek", "cred_deepseek", { value: { type: "key", key: "deepseek-key" } }), + oauthCredential("openai", "cred_openai_active"), + oauthCredential("openai", "cred_openai_inactive"), + ]); await expect( - readCredentialRows(["openai"], { firstOnly: true, methods: ["oauth"] }), - ).resolves.toEqual([]); + readAuthFile({ integrationIds: ["github-copilot", "deepseek", "openai"] }), + ).resolves.toEqual({ + "github-copilot": expect.objectContaining({ + access: "copilot-access", + enterpriseUrl: "example.ghe.com", + }), + deepseek: { type: "api", key: "deepseek-key" }, + openai: expect.objectContaining({ access: "cred_openai_active-access" }), + }); + await expect(readAuthFile({ integrationIds: ["xai"] })).resolves.toBeNull(); }); it("caches auth maps per sorted id list", async () => { vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(1_000_000); - const { databasePath } = await createCredentialDatabase(); - vi.stubEnv("OPENCODE_DB", databasePath); + const openai = oauthCredential("openai", "cred_openai"); + bindFakeIntegration([openai, keyCredential("deepseek", "cred_deepseek")]); await expect( readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai", "deepseek"] }), - ).resolves.toMatchObject({ openai: { access: "openai-access" } }); - const database = new DatabaseSync(databasePath); - database - .prepare("UPDATE credential SET value = ? WHERE id = 'openai'") - .run(JSON.stringify({ type: "oauth", access: "rotated-access" })); - database.close(); + ).resolves.toMatchObject({ openai: { access: "cred_openai-access" } }); + openai.value = { type: "oauth", access: "rotated-access" }; // Same ids in another order hit the same cache entry. await expect( readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["deepseek", "openai"] }), - ).resolves.toMatchObject({ openai: { access: "openai-access" } }); + ).resolves.toMatchObject({ openai: { access: "cred_openai-access" } }); // A different id list has its own entry and reads fresh. await expect( readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai"] }), @@ -346,9 +611,23 @@ describe("reader requests name their integration ids", () => { ).resolves.toMatchObject({ openai: { access: "rotated-access" } }); }); + it("drops cached auth maps when OpenCode reports a credential change", async () => { + const openai = oauthCredential("openai", "cred_openai"); + bindFakeIntegration([openai]); + + await readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai"] }); + openai.value = { type: "oauth", access: "switched-access" }; + notifyCredentialsChanged(); + + await expect( + readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai"] }), + ).resolves.toEqual({ openai: { type: "oauth", access: "switched-access" } }); + }); + it("shares one in-flight read per id list", async () => { - const { databasePath } = await createCredentialDatabase(); - vi.stubEnv("OPENCODE_DB", databasePath); + bindFakeIntegration([ + keyCredential("deepseek", "cred_deepseek", { value: { type: "key", key: "deepseek-key" } }), + ]); const [first, second] = await Promise.all([ readAuthFileCached({ maxAgeMs: 0, integrationIds: ["deepseek"] }), @@ -454,44 +733,55 @@ describe("selectConnectionCredentialRows", () => { "work", ]); }); + + it("keeps two failed logins apart although both hold only { type }", () => { + const failed = (id: string) => ({ + id, + integrationId: "opencode-go", + label: "default", + active: false, + value: { type: "api" }, + resolveError: "refresh_failed: HTTP 500", + }); + + expect( + selectConnectionCredentialRows( + [failed("cred_personal"), failed("cred_work"), failed("cred_work")], + "opencode-go", + ).map((row) => row.id), + ).toEqual(["cred_personal", "cred_work"]); + }); }); describe("Google companion credentials written by OpenCode 2", () => { it("parses the opencode-gemini-auth 2.x and AGY alpha OAuth rows", async () => { - const { databasePath } = await createCredentialDatabase(); - const database = new DatabaseSync(databasePath); - const insert = database.prepare( - "INSERT INTO credential VALUES (?, ?, ?, ?, NULL, NULL, 1, 5, 5)", - ); - // opencode-gemini-auth 2.0.1: `gemini-cli` method on OpenCode 2's `google` integration. - insert.run( - "gemini", - "google", - "user@example.com", - JSON.stringify({ - type: "oauth", - methodID: "gemini-cli", - refresh: "gemini-refresh|gemini-project|", - access: "gemini-access", - expires: 50, - metadata: { email: "user@example.com" }, + bindFakeIntegration([ + // opencode-gemini-auth 2.0.1: `gemini-cli` method on OpenCode 2's `google` integration. + oauthCredential("google", "cred_gemini", { + label: "user@example.com", + value: { + type: "oauth", + methodID: "gemini-cli", + refresh: "gemini-refresh|gemini-project|", + access: "gemini-access", + expires: 50, + metadata: { email: "user@example.com" }, + }, }), - ); - // @anthonyhaussman/opencode-agy-auth 1.2.11-alpha.0: `oauth` method on `google-agy`. - insert.run( - "agy", - "google-agy", - "OAuth", - JSON.stringify({ - type: "oauth", - methodID: "oauth", - refresh: "agy-refresh|agy-project|agy-managed-project", - access: "agy-access", - expires: 60, + // @anthonyhaussman/opencode-agy-auth 1.2.11-alpha.0: `oauth` method on `google-agy`, + // registered only while that plugin is loaded. + oauthCredential("google-agy", "cred_agy", { + label: "OAuth", + registered: false, + value: { + type: "oauth", + methodID: "oauth", + refresh: "agy-refresh|agy-project|agy-managed-project", + access: "agy-access", + expires: 60, + }, }), - ); - database.close(); - vi.stubEnv("OPENCODE_DB", databasePath); + ]); const auth = await readAuthFile({ integrationIds: ["google", "google-agy"] }); expect(resolveGeminiCliAccounts(auth)).toEqual([ diff --git a/tests/lib.opencode-console-auth.test.ts b/tests/lib.opencode-console-auth.test.ts index c9b06522..93baecfb 100644 --- a/tests/lib.opencode-console-auth.test.ts +++ b/tests/lib.opencode-console-auth.test.ts @@ -1,78 +1,61 @@ -import { mkdir, mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { DatabaseSync } from "node:sqlite"; -import { afterEach, describe, expect, it, vi } from "vitest"; - +import { afterEach, describe, expect, it } from "vitest"; + +import { + bindCredentialSource, + clearReadAuthFileCacheForTests, + createIntegrationCredentialSource, + notifyCredentialsChanged, +} from "../src/lib/opencode-auth.js"; import { resolveOpenCodeConsoleAuth } from "../src/lib/opencode-console-auth.js"; +import { createFakeIntegration, type FakeCredential } from "./helpers/fake-integration.js"; -const authMocks = vi.hoisted(() => ({ readCredentialRows: vi.fn() })); +const unbinds: Array<() => void> = []; -// Real SQLite reads by default; single tests swap in fixture rows the SQLite -// reader cannot produce (failed logins). -vi.mock("../src/lib/opencode-auth.js", async (importOriginal) => { - const actual = await importOriginal(); - authMocks.readCredentialRows.mockImplementation(actual.readCredentialRows); - return { ...actual, readCredentialRows: authMocks.readCredentialRows }; +afterEach(() => { + for (const unbind of unbinds.splice(0)) unbind(); + notifyCredentialsChanged(); + clearReadAuthFileCacheForTests(); }); -const temporaryDirectories: string[] = []; - -afterEach(async () => { - vi.unstubAllEnvs(); - await Promise.all( - temporaryDirectories - .splice(0) - .map((directory) => rm(directory, { recursive: true, force: true })), - ); -}); +function bindConsoleLogins(credentials: FakeCredential[]) { + const integration = createFakeIntegration(credentials); + unbinds.push(bindCredentialSource(createIntegrationCredentialSource(integration as never))); + return integration; +} -async function createCredentialDatabaseWith( - integrationId: string, +function consoleLogin( value: Record, -): Promise { - const root = await mkdtemp(join(tmpdir(), "opencode-console-auth-")); - temporaryDirectories.push(root); - const dataDir = join(root, "opencode"); - await mkdir(dataDir, { recursive: true }); - const database = new DatabaseSync(join(dataDir, "opencode.db")); - database.exec(`CREATE TABLE credential ( - id TEXT PRIMARY KEY, - integration_id TEXT, - label TEXT NOT NULL, - value TEXT NOT NULL, - connector_id TEXT, - method_id TEXT, - active INTEGER, - time_created INTEGER NOT NULL, - time_updated INTEGER NOT NULL - )`); - database - .prepare("INSERT INTO credential VALUES (?, ?, 'default', ?, NULL, NULL, NULL, 1, 1)") - .run("cred-console", integrationId, JSON.stringify(value)); - database.close(); - vi.stubEnv("XDG_DATA_HOME", root); - return join(dataDir, "opencode.db"); + overrides: Partial = {}, +): FakeCredential { + return { + integrationId: "opencode", + id: "cred_console", + label: "default", + registered: true, + method: value.type === "key" ? "key" : "oauth", + value, + ...overrides, + }; } describe("OpenCode Console credential reader", () => { it("returns none without console credentials", async () => { - const root = await mkdtemp(join(tmpdir(), "opencode-console-auth-empty-")); - temporaryDirectories.push(root); - vi.stubEnv("XDG_DATA_HOME", root); + bindConsoleLogins([]); await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); }); it("resolves the configured console credential with org metadata", async () => { const expires = Date.now() + 60_000; - await createCredentialDatabaseWith("opencode", { - type: "oauth", - methodID: "server", - access: "console-access", - refresh: "console-refresh", - expires, - metadata: { email: "ran@example.com", orgID: "wrk_1", orgName: "WSC Sports" }, - }); + bindConsoleLogins([ + consoleLogin({ + type: "oauth", + methodID: "server", + access: "console-access", + refresh: "console-refresh", + expires, + metadata: { email: "ran@example.com", orgID: "wrk_1", orgName: "WSC Sports" }, + }), + ]); const state = await resolveOpenCodeConsoleAuth(); expect(state).toEqual({ @@ -90,19 +73,31 @@ describe("OpenCode Console credential reader", () => { }); it("reports expired console credentials", async () => { - await createCredentialDatabaseWith("opencode", { - type: "oauth", - access: "console-access", - expires: Date.now() - 60_000, - metadata: { orgID: "wrk_1" }, - }); + bindConsoleLogins([ + consoleLogin({ + type: "oauth", + methodID: "device", + refresh: "console-refresh", + access: "console-access", + expires: Date.now() - 60_000, + metadata: { orgID: "wrk_1" }, + }), + ]); const state = await resolveOpenCodeConsoleAuth(); expect(state.state).toBe("expired"); }); it("reports invalid credentials without an access token", async () => { - await createCredentialDatabaseWith("opencode", { type: "oauth", access: " " }); + bindConsoleLogins([ + consoleLogin({ + type: "oauth", + methodID: "device", + refresh: "console-refresh", + access: " ", + expires: 0, + }), + ]); await expect(resolveOpenCodeConsoleAuth()).resolves.toMatchObject({ state: "invalid", @@ -111,42 +106,46 @@ describe("OpenCode Console credential reader", () => { }); it("ignores key-shaped opencode rows", async () => { - await createCredentialDatabaseWith("opencode", { type: "key", key: "workspace-key" }); + bindConsoleLogins([consoleLogin({ type: "key", key: "workspace-key" })]); await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); }); it("reads only the active opencode login", async () => { - const databasePath = await createCredentialDatabaseWith("opencode", { - type: "key", - key: "workspace-key", - }); - const database = new DatabaseSync(databasePath); - database.prepare("UPDATE credential SET active = 1 WHERE id = 'cred-console'").run(); - database - .prepare("INSERT INTO credential VALUES (?, 'opencode', 'default', ?, NULL, NULL, 0, 1, 2)") - .run( - "cred-console-inactive", - JSON.stringify({ type: "oauth", access: "console-access", expires: Date.now() + 60_000 }), - ); - database.close(); + const integration = bindConsoleLogins([ + consoleLogin({ type: "key", key: "workspace-key" }, { id: "cred_workspace" }), + consoleLogin( + { + type: "oauth", + methodID: "device", + refresh: "console-refresh", + access: "console-access", + expires: Date.now() + 60_000, + }, + { id: "cred_console_inactive" }, + ), + ]); await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); - expect(authMocks.readCredentialRows).toHaveBeenLastCalledWith(["opencode"], { - firstOnly: true, - }); + expect(integration.list).not.toHaveBeenCalled(); + expect(integration.connection.active.mock.calls).toEqual([["opencode"]]); + expect(integration.connection.resolve.mock.calls).toEqual([ + [{ type: "credential", id: "cred_workspace", label: "default", method: "key" }], + ]); }); it("reports a login OpenCode could not return as invalid", async () => { - authMocks.readCredentialRows.mockResolvedValueOnce([ - { - id: "cred-console", - integrationId: "opencode", - label: "default", - active: true, - value: { type: "oauth" }, - resolveError: "refresh_failed: HTTP 401", - }, + bindConsoleLogins([ + consoleLogin( + { + type: "oauth", + methodID: "device", + refresh: "console-refresh", + access: "console-access", + expires: 0, + }, + { resolveError: "HTTP 401" }, + ), ]); await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ diff --git a/tests/lib.opencode-db-path.test.ts b/tests/lib.opencode-db-path.test.ts index fcfffe13..744d5973 100644 --- a/tests/lib.opencode-db-path.test.ts +++ b/tests/lib.opencode-db-path.test.ts @@ -4,11 +4,7 @@ import { dirname, join } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { - clearReadAuthFileCacheForTests, - getCredentialDatabasePaths, - readAuthFile, -} from "../src/lib/opencode-auth.js"; +import { getCredentialDatabasePaths } from "../src/lib/opencode-auth.js"; import { getOpenCodeDbPath } from "../src/lib/opencode-db-path.js"; import { getOpenCodeDbStats } from "../src/lib/opencode-storage.js"; @@ -17,7 +13,6 @@ const originalPlatform = process.platform; afterEach(async () => { Object.defineProperty(process, "platform", { value: originalPlatform }); - clearReadAuthFileCacheForTests(); vi.unstubAllEnvs(); await Promise.all( temporaryDirectories @@ -32,25 +27,14 @@ async function createRoot(): Promise { return root; } -/** Writes an OpenCode 2 database with one API-key credential and `sessions` sessions. */ -async function createOpenCodeDatabase(path: string, key: string, sessions: number): Promise { +/** Writes an OpenCode 2 database with `sessions` sessions. */ +async function createOpenCodeDatabase(path: string, sessions: number): Promise { await mkdir(dirname(path), { recursive: true }); const database = new DatabaseSync(path); database.exec(` - CREATE TABLE credential ( - id TEXT PRIMARY KEY, - integration_id TEXT, - label TEXT NOT NULL, - value TEXT NOT NULL, - active INTEGER, - time_updated INTEGER NOT NULL - ); CREATE TABLE "session_v2" (id TEXT PRIMARY KEY); CREATE TABLE "session_message" (id TEXT PRIMARY KEY, type TEXT NOT NULL, data TEXT NOT NULL); `); - database - .prepare("INSERT INTO credential VALUES ('deepseek', 'deepseek', 'default', ?, 1, 1)") - .run(JSON.stringify({ type: "key", key })); for (let i = 0; i < sessions; i++) { database.prepare(`INSERT INTO "session_v2" VALUES (?)`).run(`ses_${i}`); } @@ -62,11 +46,7 @@ describe("OpenCode database path", () => { const root = await createRoot(); const home = join(root, "home"); const sandboxData = join(root, "sandbox-data"); - await createOpenCodeDatabase( - join(home, ".local", "share", "opencode", "opencode.db"), - "home-key", - 3, - ); + await createOpenCodeDatabase(join(home, ".local", "share", "opencode", "opencode.db"), 3); Object.defineProperty(process, "platform", { value: "darwin" }); vi.stubEnv("HOME", home); vi.stubEnv("XDG_DATA_HOME", sandboxData); @@ -75,52 +55,46 @@ describe("OpenCode database path", () => { const expected = join(sandboxData, "opencode", "opencode.db"); expect(getOpenCodeDbPath()).toBe(expected); expect(getCredentialDatabasePaths()).toEqual([expected]); - await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toBeNull(); await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: expected, sessionCount: 0, }); }); - it("uses an absolute OPENCODE_DB for credentials and sessions", async () => { + it("uses an absolute OPENCODE_DB for the login database path and sessions", async () => { const root = await createRoot(); const custom = join(root, "elsewhere", "custom.db"); - await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), "default-key", 1); - await createOpenCodeDatabase(custom, "custom-key", 2); + await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), 1); + await createOpenCodeDatabase(custom, 2); vi.stubEnv("XDG_DATA_HOME", join(root, "data")); vi.stubEnv("OPENCODE_DB", custom); expect(getOpenCodeDbPath()).toBe(custom); - await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toMatchObject({ - deepseek: { key: "custom-key" }, - }); + expect(getCredentialDatabasePaths()).toEqual([custom]); await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: custom, sessionCount: 2 }); }); - it("resolves a relative OPENCODE_DB against the data dir for credentials and sessions", async () => { + it("resolves a relative OPENCODE_DB against the data dir for the login database path and sessions", async () => { const root = await createRoot(); const custom = join(root, "data", "opencode", "profiles", "work.db"); - await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), "default-key", 1); - await createOpenCodeDatabase(custom, "work-key", 4); + await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), 1); + await createOpenCodeDatabase(custom, 4); vi.stubEnv("XDG_DATA_HOME", join(root, "data")); vi.stubEnv("OPENCODE_DB", join("profiles", "work.db")); expect(getOpenCodeDbPath()).toBe(custom); - await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toMatchObject({ - deepseek: { key: "work-key" }, - }); + expect(getCredentialDatabasePaths()).toEqual([custom]); await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: custom, sessionCount: 4 }); }); it("reads no database file when OPENCODE_DB is :memory:", async () => { const root = await createRoot(); - await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), "default-key", 1); + await createOpenCodeDatabase(join(root, "data", "opencode", "opencode.db"), 1); vi.stubEnv("XDG_DATA_HOME", join(root, "data")); vi.stubEnv("OPENCODE_DB", ":memory:"); expect(getOpenCodeDbPath()).toBe(":memory:"); expect(getCredentialDatabasePaths()).toEqual([]); - await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toBeNull(); await expect(getOpenCodeDbStats()).resolves.toMatchObject({ dbPath: ":memory:", sessionCount: 0, diff --git a/tests/plugin-rpc.test.ts b/tests/plugin-rpc.test.ts index 75266328..e36d0a1d 100644 --- a/tests/plugin-rpc.test.ts +++ b/tests/plugin-rpc.test.ts @@ -3,6 +3,7 @@ import { rm } from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { DEFAULT_CONFIG } from "../src/lib/types.js"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; import { createAlibabaAuthModuleMock, createConfigModuleMock, @@ -94,6 +95,8 @@ async function setupServer(sessionGet = vi.fn().mockResolvedValue({})) { }), }, rpc: { register }, + integration: createFakeIntegration([]), + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, }; await server.setup(ctx as never); expect(register).toHaveBeenCalledOnce(); diff --git a/tests/plugin-v2.test.ts b/tests/plugin-v2.test.ts index 47e434a2..f1b8d678 100644 --- a/tests/plugin-v2.test.ts +++ b/tests/plugin-v2.test.ts @@ -6,9 +6,16 @@ vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ buildQuotaDialogCommandOutput: buildOutput, })); +import { + clearReadAuthFileCacheForTests, + getCredentialSourceDiagnostics, + readAuthFile, + readAuthFileCached, +} from "../src/lib/opencode-auth.js"; import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-command-specs.js"; import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; import plugin from "../src/plugin.js"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; type RegisteredTool = { name: string; @@ -60,6 +67,8 @@ function createContext() { }), }, rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, + integration: createFakeIntegration([]), + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, }; return { ctx, tools, commands, hooks, calls }; } @@ -279,4 +288,76 @@ describe("V2 server plugin", () => { expect(normal.result).toBeUndefined(); expect(normal.messages[0]).toBe(plain); }); + + it("reads logins through ctx.integration from setup until cleanup", async () => { + const { ctx } = createContext(); + const integration = createFakeIntegration([ + { + integrationId: "deepseek", + id: "cred_deepseek", + label: "default", + registered: true, + method: "key", + value: { type: "key", key: "deepseek-key" }, + }, + ]); + const subscribe = vi.fn((_options: { signal: AbortSignal }) => ({ + async *[Symbol.asyncIterator]() {}, + })); + + const cleanup = await plugin.setup({ ...ctx, integration, event: { subscribe } } as never); + expect(getCredentialSourceDiagnostics().state).toBe("bound"); + await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toEqual({ + deepseek: { type: "api", key: "deepseek-key" }, + }); + const signal = subscribe.mock.calls[0]?.[0].signal; + expect(signal?.aborted).toBe(false); + + await cleanup?.(); + expect(signal?.aborted).toBe(true); + // Only this setup's binding is gone; earlier tests' empty fakes hold no deepseek login. + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toBeNull(); + warn.mockRestore(); + expect(integration.connection.active).toHaveBeenCalledOnce(); + }); + + it.each([ + "credential.updated", + "credential.switched", + ])("drops cached logins when OpenCode reports %s", async (type) => { + clearReadAuthFileCacheForTests(); + const { ctx } = createContext(); + const deepseek = { + integrationId: "deepseek", + id: "cred_deepseek", + label: "default", + registered: true, + method: "key" as const, + value: { type: "key", key: "old-key" } as Record, + }; + const integration = createFakeIntegration([deepseek]); + let emit!: (event: { type: string; data: Record }) => void; + const nextEvent = new Promise<{ type: string; data: Record }>((resolve) => { + emit = resolve; + }); + const subscribe = vi.fn(() => ({ + async *[Symbol.asyncIterator]() { + yield { type: "session.idle", data: {} }; + yield await nextEvent; + }, + })); + const cleanup = await plugin.setup({ ...ctx, integration, event: { subscribe } } as never); + const read = () => readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["deepseek"] }); + + await expect(read()).resolves.toEqual({ deepseek: { type: "api", key: "old-key" } }); + deepseek.value = { type: "key", key: "new-key" }; + await expect(read()).resolves.toEqual({ deepseek: { type: "api", key: "old-key" } }); + + emit({ type, data: { integrationID: "deepseek", credentialID: "cred_deepseek" } }); + await vi.waitFor(async () => + expect(await read()).toEqual({ deepseek: { type: "api", key: "new-key" } }), + ); + await cleanup?.(); + }); }); diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 1afa93b5..5f6d9dae 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { QUOTA_DIALOG_COMMANDS } from "../src/lib/quota-dialog-command-specs.js"; import tuiPlugin from "../src/tui-v2.js"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; // The TUI must never read logins or build reports itself: the server plugin does both. const mocks = vi.hoisted(() => ({ @@ -206,6 +207,8 @@ describe("V2 CLI command boundary", () => { rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })), }, + integration: createFakeIntegration([]), + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, } as never); expect(transform).toHaveBeenCalledOnce(); expect(commandTransform).toHaveBeenCalledOnce(); diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 8a28e9e9..a6557062 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -1,6 +1,7 @@ import { rm } from "fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; import { createAlibabaAuthModuleMock, createPluginTestClient as createClient, @@ -147,6 +148,8 @@ async function startCli() { command: { transform: vi.fn() }, tool: { transform: vi.fn() }, rpc: { register }, + integration: createFakeIntegration([]), + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, } as never); const [, handlers] = register.mock.calls[0] as unknown as [ unknown, diff --git a/tests/plugin.quota-command.test.ts b/tests/plugin.quota-command.test.ts index 748add39..f56ad6ac 100644 --- a/tests/plugin.quota-command.test.ts +++ b/tests/plugin.quota-command.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { QuotaProviderContext } from "../src/lib/entries.js"; import { DEFAULT_CONFIG } from "../src/lib/types.js"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; import { createAlibabaAuthModuleMock, createPluginTestClient as createClient, @@ -71,6 +72,8 @@ async function createV2StatusTool(directory: string) { session: { get: vi.fn().mockResolvedValue({}), hook: vi.fn() }, command: { transform: vi.fn() }, rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, + integration: createFakeIntegration([]), + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, tool: { transform: async ( register: (editor: { add: (tool: { execute: typeof execute }) => void }) => void, @@ -206,6 +209,8 @@ describe("/quota command behavior", () => { rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })), }, + integration: createFakeIntegration([]), + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, }; await server.setup(ctx as never); expect(tool?.name).toBe("quota_status"); diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index e21a16da..43318aff 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -1,6 +1,7 @@ import { rm } from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; import { createConfigModuleMock, createPluginRuntimePathsMockModule, @@ -123,6 +124,8 @@ async function runQuotaStatus(sessionID: string): Promise { }, command: { transform: vi.fn() }, rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, + integration: createFakeIntegration([]), + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, tool: { transform: async (callback: (editor: { add: (tool: typeof quotaTool) => void }) => void) => callback({ diff --git a/tests/providers.anthropic.test.ts b/tests/providers.anthropic.test.ts index eacacf45..7f66e3bc 100644 --- a/tests/providers.anthropic.test.ts +++ b/tests/providers.anthropic.test.ts @@ -73,6 +73,58 @@ describe("anthropic provider", () => { ]); }); + it("shows a login OpenCode could not read as its own error row", async () => { + const { getAnthropicDiagnostics, queryAnthropicQuota, queryAnthropicQuotaWithOAuth } = + await import("../src/lib/anthropic.js"); + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + (readCredentialRows as any).mockResolvedValueOnce([ + { + id: "failed-active", + integrationId: "anthropic", + label: "default", + active: true, + value: { type: "oauth" }, + resolveError: "refresh_failed: HTTP 500", + }, + { + id: "valid-inactive", + integrationId: "anthropic", + label: "Work", + active: false, + value: { type: "oauth", access: "valid" }, + }, + ]); + (getAnthropicDiagnostics as any).mockResolvedValueOnce({ + installed: false, + authStatus: "unknown", + quotaSupported: false, + quotaSource: "none", + checkedCommands: [], + }); + (queryAnthropicQuotaWithOAuth as any).mockResolvedValueOnce({ + success: true, + five_hour: { percentRemaining: 80 }, + seven_day: { percentRemaining: 70 }, + }); + + const out = await anthropicProvider.fetch({} as any); + + expect(queryAnthropicQuotaWithOAuth).toHaveBeenCalledOnce(); + expect(queryAnthropicQuotaWithOAuth).toHaveBeenCalledWith("valid", undefined); + expect(queryAnthropicQuota).not.toHaveBeenCalled(); + expect(out.errors).toEqual([ + { + label: "[Claude]*", + message: + "Anthropic sign-in could not be read: refresh_failed: HTTP 500. Run `opencode auth login anthropic`.", + }, + ]); + expect(out.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ + ["[Claude Work]", "valid-inactive"], + ["[Claude Work]", "valid-inactive"], + ]); + }); + it("reports the credential store that answered the usage probe", async () => { const { getAnthropicDiagnostics, queryAnthropicQuota } = await import( "../src/lib/anthropic.js" diff --git a/tests/providers.copilot.test.ts b/tests/providers.copilot.test.ts index 16a4161a..870a173e 100644 --- a/tests/providers.copilot.test.ts +++ b/tests/providers.copilot.test.ts @@ -175,6 +175,60 @@ describe("copilot provider", () => { expect(out.statusDetails).toContainEqual({ key: "billing_scope", value: "user" }); }); + it("shows a login OpenCode could not read as its own error row", async () => { + const { getCopilotQuotaAuthDiagnostics, queryCopilotQuota } = await import( + "../src/lib/copilot.js" + ); + const diagnostics = { + ...(getCopilotQuotaAuthDiagnostics as any)(null), + effectiveSource: "oauth", + }; + (getCopilotQuotaAuthDiagnostics as any) + .mockReturnValueOnce(diagnostics) + .mockReturnValueOnce(diagnostics); + authMocks.readCredentialRows.mockResolvedValueOnce([ + { + id: "copilot-alice", + integrationId: "github-copilot", + label: "alice", + active: true, + value: { type: "oauth" }, + resolveError: "refresh_failed: HTTP 401", + }, + { + id: "copilot-bob", + integrationId: "github-copilot", + label: "bob", + active: false, + value: { type: "oauth", access: "bob-token" }, + }, + ]); + const signInError = + "Copilot sign-in could not be read: refresh_failed: HTTP 401. Run `opencode auth login github-copilot`."; + (queryCopilotQuota as any).mockResolvedValueOnce({ success: false, error: signInError }); + (queryCopilotQuota as any).mockResolvedValueOnce({ + success: true, + mode: "user_quota", + unit: "premium_interactions", + used: 600, + total: 1_000, + percentRemaining: 40, + authority: "provider_reported", + plan: "enterprise", + }); + + const out = await copilotProvider.fetch({} as any); + + expect( + (queryCopilotQuota as any).mock.calls.map(([options]: any[]) => options.authData), + ).toEqual([ + { "github-copilot": { type: "oauth", resolveError: "refresh_failed: HTTP 401" } }, + { "github-copilot": { type: "oauth", access: "bob-token" } }, + ]); + expect(out.errors).toEqual([{ label: "[Copilot alice]*", message: signInError }]); + expect(visibleEntries(out.entries, "copilot")[0]?.group).toBe("[Copilot bob] (enterprise)"); + }); + it("renders pooled organization credits plus a real additional-usage budget", async () => { const { queryCopilotQuota } = await import("../src/lib/copilot.js"); (queryCopilotQuota as any).mockResolvedValueOnce({ diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index c9b2fdc7..5d12fa9d 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -275,6 +275,35 @@ describe("opencode-go provider", () => { expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); }); + it("keeps two failed database logins as two error rows", async () => { + const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); + const failed = (id: string, label: string, active: boolean) => ({ + id, + integrationId: "opencode-go", + label, + active, + value: { type: "api" }, + resolveError: "active_failed: database is locked", + }); + (readCredentialRows as any).mockResolvedValueOnce([ + failed("cred_personal", "Personal", true), + failed("cred_work", "Work", false), + ]); + mocks.resolveOpenCodeGoAuth.mockImplementation((auth: any) => ({ + state: "invalid", + error: `OpenCode could not read this login: ${auth["opencode-go"].resolveError}`, + })); + + const out = await runFetch(); + + const message = "OpenCode could not read this login: active_failed: database is locked"; + expect(out.errors).toEqual([ + { label: "[OpenCode Go Personal]*", message }, + { label: "[OpenCode Go Work]", message }, + ]); + expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); + }); + it("reports one Go connection when the same key is stored under both integrations", async () => { const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); const workspaceCredential = { type: "key", key: "workspace-key" }; diff --git a/tests/tui-dist-import-graph.test.ts b/tests/tui-dist-import-graph.test.ts index 0d9f1862..2cf6ee8d 100644 --- a/tests/tui-dist-import-graph.test.ts +++ b/tests/tui-dist-import-graph.test.ts @@ -37,7 +37,8 @@ describe("tui dist import graph", () => { // node:sqlite is allowed: the session and token-history storage (opencode-storage.js -> // opencode-sqlite.js) is reachable through config.js -> quota-providers.js -> // quota-stats.js, but it opens a database only when called, and the TUI never calls it. - // It reads no logins; only the login reader's SQL names the credential table. + // It reads no logins: logins come only through OpenCode's plugin API, and no reachable + // module may query OpenCode's credential table. expect( reached.filter((file) => readFileSync(resolve(distDir, file), "utf8").includes("FROM credential"), diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 87aaa17d..3b24804d 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -12,6 +12,7 @@ import { PHASE5_SECRET_CANARIES, phase5JsonResponse, } from "./fixtures/v4-phase5-integration.js"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; import { createAlibabaAuthModuleMock, createConfigModuleMock, @@ -167,6 +168,8 @@ async function setupV2Surfaces(client: ReturnType, provider }, command: { transform: vi.fn() }, rpc: { register }, + integration: createFakeIntegration([]), + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, tool: { transform: vi.fn(async (callback: (editor: { add(value: RegisteredTool): void }) => void) => { callback({ From 21d77c2d5df39b8708ac1aecf5624befc983731d Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:32:52 +0200 Subject: [PATCH 70/97] fix(go): resolve the OpenCode Console login through the integration API The Console reader follows OpenCode's own pattern: only the active `opencode` connection counts, it must be an OAuth sign-in (an API key there reports `none` with reason `not_oauth`), and OpenCode's plugin API refreshes its token when it is read. A sign-in OpenCode cannot return is `invalid`. The credential gains `accountId`, and the new `consoleBaseUrl` and `consoleHeaders` helpers send Console requests to the login's server, with `x-org-id` only when the login has an org. OpenCode Go's Console status request uses those helpers. When the Console sign-in is `invalid`, Go uses its API-key path and keeps the scrubbed Console error in its status details. `readAuthFile` and `readAuthFileCached` take an optional `methods` filter, which is part of the cache key. OpenCode Go's key lookup and the remote-API key lookup read key logins only, so they never resolve (and so never refresh) an OAuth sign-in such as the Console login under `opencode`. Co-authored-by: Ran Sagy --- src/lib/opencode-auth.ts | 23 ++++-- src/lib/opencode-console-auth.ts | 40 ++++++--- src/lib/opencode-go-auth.ts | 5 +- src/lib/opencode-go.ts | 20 ++--- src/lib/quota-providers-remote.ts | 4 +- src/providers/opencode-go.ts | 28 +++++-- tests/lib.opencode-auth.test.ts | 31 +++++++ tests/lib.opencode-console-auth.test.ts | 90 ++++++++++++++++++++- tests/lib.opencode-go-auth.test.ts | 62 ++++++++++++++ tests/lib.opencode-go.test.ts | 73 ++++++++++++++++- tests/lib.quota-providers-auth.test.ts | 41 ++++++++++ tests/provider-surfaces.opencode-go.test.ts | 51 +++++++++++- tests/providers.opencode-go.test.ts | 53 +++++++++++- 13 files changed, 477 insertions(+), 44 deletions(-) diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 0a98e5a0..ef9f0c6c 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -140,7 +140,7 @@ export function formatCredentialDisplayNames( }); } -/** Cached auth maps keyed by the sorted, joined integration id list. */ +/** Cached auth maps keyed by the sorted integration id list and the method filter. */ const authCache = new Map(); /** @@ -402,11 +402,18 @@ export function credentialRowAuthEntry(row: CredentialRow): Record { - const rows = await readCredentialRows(params.integrationIds, { firstOnly: true }); + const rows = await readCredentialRows(params.integrationIds, { + methods: params.methods, + firstOnly: true, + }); const auth: Record = {}; for (const row of rows) { if (!(row.integrationId in auth)) auth[row.integrationId] = credentialRowAuthEntry(row); @@ -487,9 +494,12 @@ export function selectConnectionCredentialRows( export async function readAuthFileCached(params: { maxAgeMs: number; integrationIds: readonly string[]; + methods?: readonly CredentialMethod[]; }): Promise { const maxAgeMs = Math.max(0, params.maxAgeMs); - const cacheKey = [...params.integrationIds].sort().join(","); + const cacheKey = `${[...params.integrationIds].sort().join(",")}|${ + params.methods ? [...params.methods].sort().join(",") : "any" + }`; const cached = authCache.get(cacheKey); const now = Date.now(); @@ -502,7 +512,10 @@ export async function readAuthFileCached(params: { } const inFlight = (async () => { - const value = await readAuthFile({ integrationIds: params.integrationIds }); + const value = await readAuthFile({ + integrationIds: params.integrationIds, + methods: params.methods, + }); authCache.set(cacheKey, { timestamp: Date.now(), value }); return value; })(); diff --git a/src/lib/opencode-console-auth.ts b/src/lib/opencode-console-auth.ts index 87aa5c81..bb0465c7 100644 --- a/src/lib/opencode-console-auth.ts +++ b/src/lib/opencode-console-auth.ts @@ -1,15 +1,16 @@ /** * OpenCode Console credential reader * - * OpenCode 2 stores its OpenCode Console account in the credential database - * under the `opencode` integration as an OAuth entry created by the device - * flow (`client_id: opencode-cli`, base `https://opencode.ai/console`). - * Consumers use the access token as `Authorization: Bearer` against console - * APIs and `metadata.orgID` for org-scoped routes. + * OpenCode 2 keeps its OpenCode Console sign-in (`opencode auth login opencode`) + * under the `opencode` integration as an OAuth connection created by the device + * flow (`client_id: opencode-cli`, default server `https://opencode.ai/console`, + * stored as `metadata.server`). Consumers call the console APIs with + * `consoleBaseUrl` and `consoleHeaders`: the access token as + * `Authorization: Bearer`, plus `metadata.orgID` as `x-org-id` when present. * - * The reader is intentionally read-only: the CLI refreshes the token via - * `/console/auth/device/token` on its own; an expired token is reported as - * `expired` so callers can tell the user to re-run `opencode auth login`. + * The login is read through OpenCode's plugin API, which refreshes a token that + * is about to expire (and saves the new one) before returning it. A failed + * refresh is reported as `invalid`. */ import { readCredentialRows } from "./opencode-auth.js"; @@ -21,6 +22,7 @@ export interface OpenCodeConsoleCredential { accessToken: string; refreshToken?: string; expiresAt?: number; + accountId?: string; orgId?: string; orgName?: string; email?: string; @@ -28,7 +30,7 @@ export interface OpenCodeConsoleCredential { } export type OpenCodeConsoleAuthState = - | { state: "none" } + | { state: "none"; reason?: "not_oauth" } | { state: "configured"; credential: OpenCodeConsoleCredential } | { state: "expired"; credential: OpenCodeConsoleCredential } | { state: "invalid"; error: string }; @@ -38,12 +40,13 @@ export async function resolveOpenCodeConsoleAuth(params?: { }): Promise { // Only the active Console login counts, the way OpenCode itself picks it. const rows = await readCredentialRows([OPENCODE_CONSOLE_INTEGRATION_ID], { firstOnly: true }); - const row = rows.find((row) => row.integrationId === OPENCODE_CONSOLE_INTEGRATION_ID); + const row = rows[0]; if (!row) return { state: "none" }; if (row.resolveError !== undefined) return { state: "invalid", error: row.resolveError }; const value = row.value; - if (value.type !== "oauth") return { state: "none" }; + // An API key under `opencode` (service account or workspace key) is not a sign-in. + if (value.type !== "oauth") return { state: "none", reason: "not_oauth" }; const accessToken = typeof value.access === "string" ? value.access.trim() : ""; if (!accessToken) { @@ -56,6 +59,7 @@ export async function resolveOpenCodeConsoleAuth(params?: { refreshToken: typeof value.refresh === "string" && value.refresh.trim() ? value.refresh : undefined, expiresAt: typeof value.expires === "number" ? value.expires : undefined, + accountId: typeof metadata?.accountID === "string" ? metadata.accountID : undefined, orgId: typeof metadata?.orgID === "string" ? metadata.orgID : undefined, orgName: typeof metadata?.orgName === "string" ? metadata.orgName : undefined, email: typeof metadata?.email === "string" ? metadata.email : undefined, @@ -69,3 +73,17 @@ export async function resolveOpenCodeConsoleAuth(params?: { return { state: "configured", credential }; } + +/** The Console server the login belongs to. */ +export function consoleBaseUrl(credential: OpenCodeConsoleCredential): string { + return credential.server ?? OPENCODE_CONSOLE_BASE_URL; +} + +/** Console API request headers; `x-org-id` only when the login has an org. */ +export function consoleHeaders(credential: OpenCodeConsoleCredential): Record { + return { + Authorization: `Bearer ${credential.accessToken}`, + Accept: "application/json", + ...(typeof credential.orgId === "string" ? { "x-org-id": credential.orgId } : {}), + }; +} diff --git a/src/lib/opencode-go-auth.ts b/src/lib/opencode-go-auth.ts index f1a2762c..9343ac0f 100644 --- a/src/lib/opencode-go-auth.ts +++ b/src/lib/opencode-go-auth.ts @@ -43,7 +43,10 @@ const openCodeGoAuthResolver = createProviderApiKeyResolver readAuthFileCached({ maxAgeMs, integrationIds: OPENCODE_GO_AUTH_KEYS }), + // Key logins only: the active `opencode` login is usually the OpenCode Console + // sign-in, and resolving it here could refresh its token for nothing. + readAuth: (maxAgeMs) => + readAuthFileCached({ maxAgeMs, integrationIds: OPENCODE_GO_AUTH_KEYS, methods: ["key"] }), getCredentialDatabasePaths, }, }); diff --git a/src/lib/opencode-go.ts b/src/lib/opencode-go.ts index ab20a13a..453d9555 100644 --- a/src/lib/opencode-go.ts +++ b/src/lib/opencode-go.ts @@ -1,6 +1,10 @@ import { sanitizeDisplayText } from "./display-sanitize.js"; import { fetchWithTimeout } from "./http.js"; -import { OPENCODE_CONSOLE_BASE_URL } from "./opencode-console-auth.js"; +import { + consoleBaseUrl, + consoleHeaders, + type OpenCodeConsoleCredential, +} from "./opencode-console-auth.js"; import type { OpenCodeGoResult, OpenCodeGoWindow, OpenCodeGoWindowKey } from "./types.js"; const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage"; @@ -201,8 +205,6 @@ export async function queryOpenCodeGoQuota( } } -const OPENCODE_CONSOLE_GO_STATUS_URL = `${OPENCODE_CONSOLE_BASE_URL}/api/go/status`; - function asMicroCents(value: unknown): number | null { const parsed = typeof value === "string" ? Number(value) : typeof value === "number" ? value : Number.NaN; @@ -245,20 +247,18 @@ function normalizeConsoleMeter( * * The Console tracks Go plan access (five-hour, weekly, and monthly meters) * for the member that owns the OAuth credential, so this works for fresh - * accounts without any pre-2.0 workspace API key. + * accounts without any pre-2.0 workspace API key. The request goes to the + * login's Console server, scoped to the login's org when it has one. */ export async function queryOpenCodeGoConsoleStatus( - credential: { accessToken: string }, + credential: OpenCodeConsoleCredential, options: { requestTimeoutMs?: number } = {}, ): Promise { try { - return await fetchWithTimeout(OPENCODE_CONSOLE_GO_STATUS_URL, { + return await fetchWithTimeout(`${consoleBaseUrl(credential)}/api/go/status`, { request: { method: "GET", - headers: { - Authorization: `Bearer ${credential.accessToken}`, - Accept: "application/json", - }, + headers: consoleHeaders(credential), }, timeoutMs: options.requestTimeoutMs, consume: async (response) => { diff --git a/src/lib/quota-providers-remote.ts b/src/lib/quota-providers-remote.ts index dbd17d78..f56902b8 100644 --- a/src/lib/quota-providers-remote.ts +++ b/src/lib/quota-providers-remote.ts @@ -103,7 +103,9 @@ export async function resolveQuotaProviderApiKey( configJsoncSource: "opencode.jsonc", getConfigCandidates: getGlobalOpencodeConfigCandidatePaths, auth: { - readAuth: () => readAuthFile({ integrationIds: [source.providerId] }), + // Key logins only: `providerId` can name an integration with OAuth sign-ins + // (for example `opencode`), and resolving one could refresh its token for nothing. + readAuth: () => readAuthFile({ integrationIds: [source.providerId], methods: ["key"] }), authKeys: [source.providerId], authSource: "opencode.db", }, diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index 1aaa883a..d9444e50 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -11,12 +11,10 @@ import { credentialRowAuthEntry, formatCredentialDisplayNames, readCredentialRows, + scrubCredentialErrorText, selectConnectionCredentialRows, } from "../lib/opencode-auth.js"; -import { - OPENCODE_CONSOLE_BASE_URL, - resolveOpenCodeConsoleAuth, -} from "../lib/opencode-console-auth.js"; +import { consoleBaseUrl, resolveOpenCodeConsoleAuth } from "../lib/opencode-console-auth.js"; import { queryOpenCodeGoConsoleStatus, queryOpenCodeGoQuota } from "../lib/opencode-go.js"; import { DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, @@ -148,7 +146,7 @@ export const opencodeGoProvider: QuotaProvider = { { key: "console_auth_state", value: "configured" }, { key: "console_server", - value: consoleAuth.credential.server ?? OPENCODE_CONSOLE_BASE_URL, + value: consoleBaseUrl(consoleAuth.credential), }, { key: "go_source", value: "console" }, { key: "selected_windows", value: windows.join(",") }, @@ -178,8 +176,8 @@ export const opencodeGoProvider: QuotaProvider = { } if (consoleAuth.state === "expired") { - // An expired console token cannot be refreshed by the plugin; fall back - // to the legacy path, which also still accepts workspace API keys. + // OpenCode refreshes the console token when it is read, so this is rare; + // fall back to the legacy path, which also still accepts workspace API keys. const diagnostics = await getOpenCodeGoAuthDiagnostics({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, }); @@ -189,6 +187,22 @@ export const opencodeGoProvider: QuotaProvider = { ]); } + if (consoleAuth.state === "invalid") { + // OpenCode could not return the console sign-in (for example its refresh + // failed); fall back to the legacy API-key path and surface the console + // error in diagnostics. + const diagnostics = await getOpenCodeGoAuthDiagnostics({ + maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, + }); + return await fetchOpenCodeGoLegacy(ctx, diagnostics, [ + ...authStatusDetails(diagnostics), + { key: "console_auth_state", value: "invalid" }, + { key: "console_error", value: scrubCredentialErrorText(consoleAuth.error) }, + { key: "go_source", value: "legacy_key" }, + { key: "selected_windows", value: windows.join(",") }, + ]); + } + const diagnostics = await getOpenCodeGoAuthDiagnostics({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, }); diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 3d84320f..54df5796 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -611,6 +611,37 @@ describe("readAuthFile and readAuthFileCached", () => { ).resolves.toMatchObject({ openai: { access: "rotated-access" } }); }); + it("leaves out an active login of another method without resolving it", async () => { + const integration = bindFakeIntegration([ + keyCredential("opencode-go", "cred_go"), + oauthCredential("opencode", "cred_console"), + ]); + + await expect( + readAuthFile({ integrationIds: ["opencode-go", "opencode"], methods: ["key"] }), + ).resolves.toEqual({ "opencode-go": { type: "api", key: "cred_go-key" } }); + // The Go key lookup never resolves (and so never refreshes) the Console sign-in. + expect(resolvedIds(integration)).toEqual(["cred_go"]); + expect(integration.list).not.toHaveBeenCalled(); + }); + + it("caches auth maps per method filter as well as per id list", async () => { + const integration = bindFakeIntegration([oauthCredential("opencode", "cred_console")]); + + await expect( + readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["opencode"], methods: ["key"] }), + ).resolves.toBeNull(); + // Same ids without a filter have their own entry, so the sign-in is read. + await expect( + readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["opencode"] }), + ).resolves.toMatchObject({ opencode: { access: "cred_console-access" } }); + // The key-only entry stays cached and empty. + await expect( + readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["opencode"], methods: ["key"] }), + ).resolves.toBeNull(); + expect(resolvedIds(integration)).toEqual(["cred_console"]); + }); + it("drops cached auth maps when OpenCode reports a credential change", async () => { const openai = oauthCredential("openai", "cred_openai"); bindFakeIntegration([openai]); diff --git a/tests/lib.opencode-console-auth.test.ts b/tests/lib.opencode-console-auth.test.ts index 93baecfb..dbd8045e 100644 --- a/tests/lib.opencode-console-auth.test.ts +++ b/tests/lib.opencode-console-auth.test.ts @@ -6,7 +6,11 @@ import { createIntegrationCredentialSource, notifyCredentialsChanged, } from "../src/lib/opencode-auth.js"; -import { resolveOpenCodeConsoleAuth } from "../src/lib/opencode-console-auth.js"; +import { + consoleBaseUrl, + consoleHeaders, + resolveOpenCodeConsoleAuth, +} from "../src/lib/opencode-console-auth.js"; import { createFakeIntegration, type FakeCredential } from "./helpers/fake-integration.js"; const unbinds: Array<() => void> = []; @@ -53,7 +57,12 @@ describe("OpenCode Console credential reader", () => { access: "console-access", refresh: "console-refresh", expires, - metadata: { email: "ran@example.com", orgID: "wrk_1", orgName: "WSC Sports" }, + metadata: { + accountID: "acc_1", + email: "ran@example.com", + orgID: "wrk_1", + orgName: "WSC Sports", + }, }), ]); @@ -64,6 +73,7 @@ describe("OpenCode Console credential reader", () => { accessToken: "console-access", refreshToken: "console-refresh", expiresAt: expires, + accountId: "acc_1", orgId: "wrk_1", orgName: "WSC Sports", email: "ran@example.com", @@ -108,7 +118,10 @@ describe("OpenCode Console credential reader", () => { it("ignores key-shaped opencode rows", async () => { bindConsoleLogins([consoleLogin({ type: "key", key: "workspace-key" })]); - await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); + await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ + state: "none", + reason: "not_oauth", + }); }); it("reads only the active opencode login", async () => { @@ -126,7 +139,10 @@ describe("OpenCode Console credential reader", () => { ), ]); - await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ state: "none" }); + await expect(resolveOpenCodeConsoleAuth()).resolves.toEqual({ + state: "none", + reason: "not_oauth", + }); expect(integration.list).not.toHaveBeenCalled(); expect(integration.connection.active.mock.calls).toEqual([["opencode"]]); expect(integration.connection.resolve.mock.calls).toEqual([ @@ -153,4 +169,70 @@ describe("OpenCode Console credential reader", () => { error: "refresh_failed: HTTP 401", }); }); + + it("uses the active sign-in and resolves no other login", async () => { + const signIn = (id: string, orgID: string) => + consoleLogin( + { + type: "oauth", + methodID: "device", + refresh: `${id}-refresh`, + access: `${id}-access`, + expires: Date.now() + 60_000, + metadata: { orgID }, + }, + { id }, + ); + const integration = bindConsoleLogins([ + signIn("cred_active", "wrk_active"), + signIn("cred_other", "wrk_other"), + ]); + + await expect(resolveOpenCodeConsoleAuth()).resolves.toMatchObject({ + state: "configured", + credential: { accessToken: "cred_active-access", orgId: "wrk_active" }, + }); + expect(integration.connection.resolve).toHaveBeenCalledOnce(); + }); + + it("keeps the Console server the sign-in belongs to", async () => { + bindConsoleLogins([ + consoleLogin({ + type: "oauth", + methodID: "device", + refresh: "console-refresh", + access: "console-access", + expires: Date.now() + 60_000, + metadata: { server: "https://console.example.test" }, + }), + ]); + + const state = await resolveOpenCodeConsoleAuth(); + if (state.state !== "configured") throw new Error(`unexpected state ${state.state}`); + expect(consoleBaseUrl(state.credential)).toBe("https://console.example.test"); + }); +}); + +describe("Console request helpers", () => { + it("uses the login's server, else the default Console server", () => { + expect(consoleBaseUrl({ accessToken: "a", server: "https://console.example.test" })).toBe( + "https://console.example.test", + ); + expect(consoleBaseUrl({ accessToken: "a" })).toBe("https://opencode.ai/console"); + }); + + it("sends the bearer token, and the org only when the login has one", () => { + expect(consoleHeaders({ accessToken: "console-access", orgId: "wrk_1" })).toEqual({ + Authorization: "Bearer console-access", + Accept: "application/json", + "x-org-id": "wrk_1", + }); + expect(consoleHeaders({ accessToken: "console-access" })).toEqual({ + Authorization: "Bearer console-access", + Accept: "application/json", + }); + expect(Object.keys(consoleHeaders({ accessToken: "console-access" }))).not.toContain( + "x-org-id", + ); + }); }); diff --git a/tests/lib.opencode-go-auth.test.ts b/tests/lib.opencode-go-auth.test.ts index c457c9f5..1b526ada 100644 --- a/tests/lib.opencode-go-auth.test.ts +++ b/tests/lib.opencode-go-auth.test.ts @@ -2,6 +2,7 @@ import { dirname, join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; import { createProviderAvailabilityContext } from "./helpers/provider-test-harness.js"; import { createRuntimePathsMockModule, @@ -288,9 +289,69 @@ describe("OpenCode Go auth resolution", () => { expect(authMocks.readAuthFileCached).toHaveBeenCalledWith({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, integrationIds: ["opencode-go", "opencode"], + methods: ["key"], }); }); + it("never resolves the OpenCode Console sign-in while looking for a Go key", async () => { + const actual = await vi.importActual( + "../src/lib/opencode-auth.js", + ); + const consoleSignIn = { + integrationId: "opencode", + id: "cred_console", + label: "default", + registered: true, + method: "oauth" as const, + value: { + type: "oauth", + methodID: "device", + access: "console-access", + refresh: "console-refresh", + expires: 0, + }, + }; + const workspaceKey = { + integrationId: "opencode", + id: "cred_workspace", + label: "default", + registered: true, + method: "key" as const, + value: { type: "key", key: "workspace-key" }, + }; + authMocks.readAuthFileCached.mockImplementation(actual.readAuthFileCached); + + const signedIn = createFakeIntegration([consoleSignIn, workspaceKey]); + const unbindSignedIn = actual.bindCredentialSource( + actual.createIntegrationCredentialSource(signedIn as never), + ); + try { + await expect(resolveOpenCodeGoAuthCached({ maxAgeMs: 0 })).resolves.toEqual({ + state: "none", + }); + expect(signedIn.connection.active.mock.calls).toEqual([["opencode-go"], ["opencode"]]); + expect(signedIn.connection.resolve).not.toHaveBeenCalled(); + } finally { + unbindSignedIn(); + actual.notifyCredentialsChanged(); + } + + // A key login that is active under `opencode` is still read. + const keyOnly = createFakeIntegration([workspaceKey]); + const unbindKeyOnly = actual.bindCredentialSource( + actual.createIntegrationCredentialSource(keyOnly as never), + ); + try { + await expect(resolveOpenCodeGoAuthCached({ maxAgeMs: 0 })).resolves.toEqual({ + state: "configured", + apiKey: "workspace-key", + }); + } finally { + unbindKeyOnly(); + actual.notifyCredentialsChanged(); + } + }); + it("uses a fixed unsupported-type error without leaking type or key secrets", async () => { const secret = "distinctive-go-secret"; const auth = authEntry({ type: secret, key: secret }); @@ -316,6 +377,7 @@ describe("OpenCode Go auth resolution", () => { expect(authMocks.readAuthFileCached).toHaveBeenLastCalledWith({ maxAgeMs: 0, integrationIds: ["opencode-go", "opencode"], + methods: ["key"], }); const { opencodeGoProvider } = await import("../src/providers/opencode-go.js"); diff --git a/tests/lib.opencode-go.test.ts b/tests/lib.opencode-go.test.ts index 9c3d619d..c67d3ce3 100644 --- a/tests/lib.opencode-go.test.ts +++ b/tests/lib.opencode-go.test.ts @@ -22,7 +22,7 @@ vi.mock("../src/lib/http.js", () => ({ fetchWithTimeout: mocks.fetchWithTimeout, })); -import { queryOpenCodeGoQuota } from "../src/lib/opencode-go.js"; +import { queryOpenCodeGoConsoleStatus, queryOpenCodeGoQuota } from "../src/lib/opencode-go.js"; type WindowKey = "rolling" | "weekly" | "monthly"; @@ -440,3 +440,74 @@ describe("queryOpenCodeGoQuota", () => { }); }); }); + +describe("queryOpenCodeGoConsoleStatus", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + function mockConsoleStatus(): void { + const meter = (usedMicroCents: number) => ({ + limitMicroCents: 1_000, + usedMicroCents, + resetsAt: "2026-10-01T00:00:00Z", + }); + mocks.fetchResponse.mockResolvedValueOnce({ + ok: true, + text: vi.fn().mockResolvedValue( + JSON.stringify({ + access: { meters: { fiveHour: meter(0), week: meter(250), month: meter(500) } }, + }), + ), + }); + } + + it("asks the login's Console server, scoped to the login's org", async () => { + mockConsoleStatus(); + + const result = await queryOpenCodeGoConsoleStatus( + { accessToken: "console-access", orgId: "wrk_1", server: "https://console.example.test" }, + { requestTimeoutMs: 4321 }, + ); + + expect(mocks.fetchWithTimeout).toHaveBeenCalledWith( + "https://console.example.test/api/go/status", + { + request: { + method: "GET", + headers: { + Authorization: "Bearer console-access", + Accept: "application/json", + "x-org-id": "wrk_1", + }, + }, + timeoutMs: 4321, + consume: expect.any(Function), + }, + ); + expect(result).toMatchObject({ + success: true, + rolling: { percentRemaining: 100 }, + weekly: { percentRemaining: 75 }, + monthly: { percentRemaining: 50 }, + }); + }); + + it("uses the default Console server and no org header when the login has neither", async () => { + mockConsoleStatus(); + + await queryOpenCodeGoConsoleStatus({ accessToken: "console-access" }); + + expect(mocks.fetchWithTimeout).toHaveBeenCalledWith( + "https://opencode.ai/console/api/go/status", + { + request: { + method: "GET", + headers: { Authorization: "Bearer console-access", Accept: "application/json" }, + }, + timeoutMs: undefined, + consume: expect.any(Function), + }, + ); + }); +}); diff --git a/tests/lib.quota-providers-auth.test.ts b/tests/lib.quota-providers-auth.test.ts index c311242f..1eb0b572 100644 --- a/tests/lib.quota-providers-auth.test.ts +++ b/tests/lib.quota-providers-auth.test.ts @@ -14,8 +14,14 @@ vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ vi.mock("fs", () => ({ existsSync: vi.fn() })); vi.mock("fs/promises", () => ({ readFile: vi.fn() })); +import { + bindCredentialSource, + createIntegrationCredentialSource, + notifyCredentialsChanged, +} from "../src/lib/opencode-auth.js"; import type { RemoteApiQuotaProviderDefinition } from "../src/lib/quota-providers.js"; import { resolveQuotaProviderApiKey } from "../src/lib/quota-providers-remote.js"; +import { createFakeIntegration } from "./helpers/fake-integration.js"; function source( overrides: Partial = {}, @@ -160,4 +166,39 @@ describe("quota provider trusted auth binding", () => { expect(result.checkedPaths).not.toContain(workspaceJson); expect(JSON.stringify(result)).not.toMatch(/workspace-secret|derived-secret/); }); + + it("reads only a key login for the providerId and never resolves an OAuth sign-in", async () => { + const integration = createFakeIntegration([ + { + integrationId: "opencode", + id: "cred_console", + label: "default", + registered: true, + method: "oauth", + value: { type: "oauth", methodID: "device", access: "console-access", expires: 0 }, + }, + { + integrationId: "provider-one", + id: "cred_provider_one", + label: "default", + registered: true, + method: "key", + value: { type: "key", key: "db-secret" }, + }, + ]); + const unbind = bindCredentialSource(createIntegrationCredentialSource(integration as never)); + try { + const signIn = await resolveQuotaProviderApiKey( + source({ providerId: "opencode", apiKeyEnv: undefined }), + ); + expect(signIn.key).toBeUndefined(); + expect(integration.connection.resolve).not.toHaveBeenCalled(); + + const keyLogin = await resolveQuotaProviderApiKey(source({ apiKeyEnv: undefined })); + expect(keyLogin).toMatchObject({ key: "db-secret", source: "opencode.db" }); + } finally { + unbind(); + notifyCredentialsChanged(); + } + }); }); diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 43318aff..83392feb 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -110,7 +110,10 @@ function successfulResult() { }; } -async function runQuotaStatus(sessionID: string): Promise { +async function runQuotaStatus( + sessionID: string, + integration = createFakeIntegration([]), +): Promise { let quotaTool: | { execute: (input: unknown, context: { sessionID: string }) => Promise<{ content: string }> } | undefined; @@ -124,7 +127,7 @@ async function runQuotaStatus(sessionID: string): Promise { }, command: { transform: vi.fn() }, rpc: { register: vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })) }, - integration: createFakeIntegration([]), + integration, event: { subscribe: () => ({ async *[Symbol.asyncIterator]() {} }) }, tool: { transform: async (callback: (editor: { add: (tool: typeof quotaTool) => void }) => void) => @@ -217,6 +220,50 @@ describe("OpenCode Go shared projections", () => { expect(status).not.toContain(TEST_TOKEN); }); + it("uses the Go key and keeps the console error when the console sign-in cannot be refreshed", async () => { + const integration = createFakeIntegration([ + { + integrationId: "opencode", + id: "cred_console", + label: "default", + registered: true, + method: "oauth", + value: { + type: "oauth", + methodID: "device", + access: "distinctive-console-access", + refresh: "distinctive-console-refresh", + expires: 0, + }, + resolveError: "HTTP 401", + }, + ]); + + const status = await runQuotaStatus("opencode-go-console-invalid", integration); + const output = await collectQuotaProjection(); + expectCanonicalPercentOrder(output); + expect(status).toContain("live_entry_1: 5h: percent_remaining=88"); + for (const secret of [ + TEST_TOKEN, + "distinctive-console-access", + "distinctive-console-refresh", + ]) { + expect(status).not.toContain(secret); + expect(output).not.toContain(secret); + } + + const result = await provider.fetch(createProviderAvailabilityContext()); + expect(result.statusDetails).toEqual( + expect.arrayContaining([ + { key: "console_auth_state", value: "invalid" }, + { key: "console_error", value: "refresh_failed: HTTP 401" }, + { key: "go_source", value: "legacy_key" }, + ]), + ); + // The failed sign-in is resolved once, then not again within the minute. + expect(integration.connection.resolve).toHaveBeenCalledOnce(); + }); + it("hides a not-subscribed result on every display and keeps it visible in safe diagnostics", async () => { mocks.queryOpenCodeGoQuota.mockResolvedValue({ success: false, diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index 5d12fa9d..500bd53b 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -34,8 +34,8 @@ vi.mock("../src/lib/opencode-go.js", async (importOriginal) => { }; }); -vi.mock("../src/lib/opencode-console-auth.js", () => ({ - OPENCODE_CONSOLE_BASE_URL: "https://opencode.ai/console", +vi.mock("../src/lib/opencode-console-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), resolveOpenCodeConsoleAuth: mocks.resolveOpenCodeConsoleAuth, })); @@ -204,6 +204,55 @@ describe("opencode-go provider", () => { ); }); + it("reports the console server the sign-in belongs to", async () => { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "configured", + credential: { accessToken: "console-access", server: "https://console.example.test" }, + }); + consoleSuccess(); + + const out = await runFetch(); + + expect(out.statusDetails).toContainEqual({ + key: "console_server", + value: "https://console.example.test", + }); + }); + + it("uses the legacy key path with the console error when the sign-in cannot be read", async () => { + const jwt = `eyJ${"a".repeat(24)}.${"b".repeat(24)}.${"c".repeat(24)}`; + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "invalid", + error: `refresh_failed: HTTP 401 ${jwt}`, + }); + + const out = await runFetch(); + + expect(mocks.queryOpenCodeGoConsoleStatus).not.toHaveBeenCalled(); + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + expectAttemptedWithNoErrors(out); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "auth_state", value: "configured" }, + { key: "console_auth_state", value: "invalid" }, + { key: "console_error", value: "refresh_failed: HTTP 401 [redacted]" }, + { key: "go_source", value: "legacy_key" }, + { key: "selected_windows", value: "rolling,weekly,monthly" }, + ]), + ); + expect(JSON.stringify(out)).not.toContain(jwt); + }); + + it("keeps the plain key path when the opencode login is an API key", async () => { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ state: "none", reason: "not_oauth" }); + + const out = await runFetch(); + + expect(mocks.queryOpenCodeGoConsoleStatus).not.toHaveBeenCalled(); + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + expect(out.statusDetails?.map((detail) => detail.key)).not.toContain("console_auth_state"); + }); + it("returns not attempted for absent auth without calling the API", async () => { mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValueOnce(diagnostics("none")); mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ state: "none" }); From c2d5fd6d1d926680037953907be97b04f94bea3a Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:48:19 +0200 Subject: [PATCH 71/97] feat(zen): read the OpenCode Console login and org budget OpenCode Zen no longer needs a pasted browser cookie. It reads the active OpenCode Console sign-in (`opencode auth login opencode`) through the shared Console resolver and calls the Console with a Bearer token at the sign-in's server, sending `x-org-id` only when the sign-in has an org. This hand-ports the HTTP layer of PR #294: the five routes including `budgets/org`, the org-budget preference with its reset date normalized to ISO, a 403 as a per-route error, 3xx, 401 or HTML as a session error, and redaction of the token and org id. Zen now shows automatically for Console users. Without a sign-in, auto mode stays quiet, and listing `opencode` in `enabledProviders` shows the sign-in hint. An expired or failed sign-in shows an error row. /quota_status lists console_auth_state, console_server, console_org, monthly_usage_usd and budget_source, never the token. As on main (#216), a month without a usable limit shows a primary "Monthly spend" row next to the balance. The cache identity is the org (else the account, else the token) plus the Console URL. The updater reports the old opencode-quota/opencode.json file as `obsolete-zen-file` and always reports the OPENCODE_WORKSPACE_ID / OPENCODE_AUTH_COOKIE names, pointing both at the Console sign-in, as on main. The cookie reader and its tests are deleted. Co-authored-by: Victor M Varela --- README.md | 5 +- docs/readme/providers.md | 31 +- docs/readme/troubleshooting.md | 16 +- docs/readme/updating.md | 14 +- src/lib/opencode-zen-config.ts | 128 -------- src/lib/opencode-zen.ts | 125 +++++-- src/lib/provider-registration.ts | 7 +- src/lib/scoped-update-migration.ts | 32 +- src/lib/scoped-update.ts | 12 +- src/providers/cache-policies.ts | 24 +- src/providers/opencode-zen.ts | 148 ++++++--- tests/helpers/provider-assertions.ts | 12 +- tests/lib.opencode-zen-config.test.ts | 302 ----------------- tests/lib.opencode-zen.test.ts | 310 ++++++++++++++++-- tests/lib.provider-metadata.test.ts | 14 +- tests/lib.quota-status.test.ts | 42 +-- tests/lib.scoped-update-migration.test.ts | 33 +- tests/lib.scoped-update.test.ts | 19 +- tests/providers.cache-policies.test.ts | 77 +++++ tests/providers.opencode-zen.surfaces.test.ts | 36 +- tests/providers.opencode-zen.test.ts | 310 ++++++++++++++---- tests/quota-render-data.test.ts | 60 ++++ 22 files changed, 1025 insertions(+), 732 deletions(-) delete mode 100644 src/lib/opencode-zen-config.ts delete mode 100644 tests/lib.opencode-zen-config.test.ts diff --git a/README.md b/README.md index d4cbb057..998ea73b 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ The updater always shows a preview first. `--yes` applies only the safe edits it > - Web and Desktop get the slash commands (report posts in the chat) but no toasts or panels. > - One `"plugin"` entry in `opencode.json` loads the server and the TUI; no `tui.json` entry is needed. > - TUI slash reports now open in a popup. Set `tuiCommandDisplay: "inline"` to keep them in the chat. +> - OpenCode Zen uses your OpenCode Console sign-in (`opencode auth login opencode`); the copied Console cookie file is no longer read. See [OpenCode Zen setup](docs/readme/providers.md#opencode-zen). > > Details: [Moving to OpenCode 2](docs/readme/updating.md#moving-to-opencode-2). @@ -168,7 +169,7 @@ Use the CLI for setup, updates, terminal checks, and custom providers. | Ollama Cloud | Automatic | Remote API | Quota and usage | | OpenAI | Automatic | Remote API | Quota | | OpenCode Go | Automatic | Remote API | Quota | -| OpenCode Zen | [Needs setup](docs/readme/providers.md#opencode-zen) | Dashboard API | Budget and balance | +| OpenCode Zen | Automatic | Remote API | Budget and balance | | OpenRouter | Automatic | Remote API | Budget and spend | | Synthetic | Automatic | Remote API | Quota | | xAI SuperGrok | Automatic | Remote API | Quota | @@ -188,7 +189,7 @@ Use the CLI for setup, updates, terminal checks, and custom providers. | Google AGY | [Needs setup](docs/readme/providers.md#google-agy-quick-setup) | Remote API | Quota | | NanoGPT | Automatic | Remote API | Quota and balance | | OpenAI | Automatic | Remote API | Quota | -| OpenCode Zen | [Needs setup](docs/readme/providers.md#opencode-zen) | Dashboard API | Budget and balance | +| OpenCode Zen | Automatic | Remote API | Budget and balance | | OpenRouter | Automatic | Remote API | Budget and spend | | Synthetic | Automatic | Remote API | Quota | | xAI SuperGrok | Automatic | Remote API | Quota | diff --git a/docs/readme/providers.md b/docs/readme/providers.md index ff73755d..120a1bf2 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -31,7 +31,7 @@ Most providers work automatically. `Automatic` means OpenCode Quota reuses the c | Ollama Cloud | Automatic | Remote API | Quota and usage | | OpenAI | Automatic | Remote API | Quota | | OpenCode Go | Automatic | Remote API | Quota | -| OpenCode Zen | [Needs setup](#opencode-zen) | Dashboard API | Budget and balance | +| OpenCode Zen | Automatic | Remote API | Budget and balance | | OpenRouter | Automatic | Remote API | Budget and spend | | Synthetic | Automatic | Remote API | Quota | | xAI | Automatic | Remote API | Quota | @@ -51,7 +51,7 @@ Most providers work automatically. `Automatic` means OpenCode Quota reuses the c | Google AGY | [Needs setup](#google-agy-quick-setup) | Remote API | Quota | | NanoGPT | Automatic | Remote API | Quota and balance | | OpenAI | Automatic | Remote API | Quota | -| OpenCode Zen | [Needs setup](#opencode-zen) | Dashboard API | Budget and balance | +| OpenCode Zen | Automatic | Remote API | Budget and balance | | OpenRouter | Automatic | Remote API | Budget and spend | | Synthetic | Automatic | Remote API | Quota | | xAI | Automatic | Remote API | Quota | @@ -624,29 +624,18 @@ The updater reports obsolete `OPENCODE_GO_WORKSPACE_ID`, `OPENCODE_GO_AUTH_COOKI ### OpenCode Zen -OpenCode Zen reads billing and usage from the OpenCode Console API (`opencode.ai/console/api/...`). These routes are unofficial, so OpenCode may change them. Provide your workspace ID and Console session cookie via the plugin config file `~/.config/opencode/opencode-quota/opencode.json`: +OpenCode Zen reads billing and usage from unofficial OpenCode Console routes (`/api/billing/status`, `/api/billing/account`, `/api/billing/auto-recharge`, `/api/budgets/org`, and `/api/usage/cost-by-day`), so OpenCode may change them. It uses your OpenCode Console sign-in from OpenCode 2; no cookie or workspace ID is needed. -```json -{ - "workspaceId": "wrk_your-workspace-id", - "consoleSessionCookie": "your-console-session-cookie" -} -``` - -Find both values in your browser: +1. In a terminal, run `opencode auth login opencode`, sign in to the Console in your browser, and pick the organization to track. +2. To track another organization, sign in again and pick it. With several saved Console sign-ins, `opencode auth switch opencode` chooses the active one, and Zen follows it. +3. Check it with `/quota_status` in OpenCode, or `opencode-quota status` in a terminal while OpenCode runs. -1. Open https://opencode.ai/console and sign in. -2. `workspaceId`: copy the `wrk_...` part of the Console URL. -3. `consoleSessionCookie`: open Developer Tools → **Application** (Firefox: **Storage**) → **Cookies** → `https://opencode.ai`, then copy the **Value** of `__Host-console_session`. +When Zen reads the sign-in, OpenCode refreshes its token if needed. If the sign-in fails or the Console rejects it, Zen asks you to run `opencode auth login opencode` again. Without a Console sign-in (an OpenCode API key alone is not one), auto mode skips Zen; include `opencode` in `enabledProviders` to see the sign-in hint instead. -When the cookie expires, Zen reports `OpenCode Console session expired or invalid`; paste a fresh value. The old `authCookie` key no longer works and is reported as a setup error. OpenCode Quota never reads browser cookie stores. +> The old `opencode-quota/opencode.json` file (`workspaceId` + `consoleSessionCookie`) and the `OPENCODE_WORKSPACE_ID` / `OPENCODE_AUTH_COOKIE` variables are no longer read. Remove them after Zen works; see [Updating safely](updating.md#opencode-zen-findings). -> The credentials are read only from this config file. They are not read from -> the `OPENCODE_WORKSPACE_ID` / `OPENCODE_AUTH_COOKIE` environment variables, -> which collide with the OpenCode client's workspace feature. The updater may -> cautiously report those names when no supported global file exists; review -> [Updating safely](updating.md#opencode-zen-findings) before changing them. +The monthly budget prefers the org budget route (`/api/budgets/org`): when it reports a positive budget limit and usable spend, the **Monthly budget** percentage uses those values plus the org budget reset date. If the org budget route fails or the org has no budget, Zen falls back to the Console credit limit (`/api/billing/account`) plus the current month's usage costs (`/api/usage/cost-by-day`); a failed org-budget route is listed as an error but never removes the balance row. -Set `opencodeMonthlyLimit` in `opencode-quota/quota-toast.json` to override the monthly budget from the Console credit limit. With valid monthly usage and a positive Console/configured limit, Zen shows a primary **Monthly budget** percentage with used, limit, and locally derived remaining USD facts. The current account balance is separate and supplementary; without a valid budget percentage, that balance becomes the primary row. **Auto-reload** is a supplementary enabled/disabled row. Its raw amount and trigger remain diagnostics. The balance is required; if the credit-limit, auto-reload, or usage route fails, Zen still shows the balance, omits only the rows that need the failed data, and lists each failed route as an error. +Set `opencodeMonthlyLimit` in `opencode-quota/quota-toast.json` to override the monthly budget from either source. With valid monthly usage and a positive Console/configured limit, Zen shows a primary **Monthly budget** percentage with used, limit, and locally derived remaining USD facts. The current account balance is separate and supplementary; without a valid budget percentage, that balance becomes a primary row, next to a primary **Monthly spend** row when this month's usage is known. **Auto-reload** is a supplementary enabled/disabled row. Its raw amount and trigger remain diagnostics. The balance is required; if the credit-limit, auto-reload, or usage route fails, Zen still shows the balance, omits only the rows that need the failed data, and lists each failed route as an error. Use root `accountingDetail: "detailed"` to admit the supplementary balance and auto-reload rows. At runtime, the removed `opencodeZenDisplay` key remains diagnostic-only. The explicit `update` command can migrate recognized file-backed `default` and `detailed` values; unsupported cases remain unchanged for manual review. See [Updating safely](updating.md#what-can-change-automatically). diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 2cce3fb8..9becae0e 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -55,7 +55,7 @@ The updater preserves unrelated settings, comments, and plugins where targeted e | Update result | What to do | | --- | --- | | Obsolete OpenCode Go source | Configure `OPENCODE_API_KEY`, trusted global `provider.opencode-go.options.apiKey`, fallback `provider.opencode.options.apiKey`, or `opencode auth login opencode-go`. Verify it, then manually remove the reported old variable/file. Workspace/cookie material cannot become an API key. | -| Ambiguous OpenCode Zen environment names | Decide whether the names belong to Zen or OpenCode's workspace feature. If they are Zen credentials, create and protect the supported global `opencode-quota/opencode.json` manually. Never paste the values into output or reports. | +| Old OpenCode Zen file or environment names | Run `opencode auth login opencode` and verify Zen works. Then remove the reported old `opencode-quota/opencode.json` file manually. Remove `OPENCODE_WORKSPACE_ID` / `OPENCODE_AUTH_COOKIE` only if they held Zen credentials; they may belong to OpenCode's workspace feature. Never paste the values into output or reports. | | Unsupported display migration | Fix the reported invalid, duplicate, or ambiguous config manually. Use root `accountingDetail: "summary"` or `"detailed"`; do not share the rejected value. | | Update race or partial-write failure | No package cache was deleted. Read the error's exact changed-path list, inspect those files, fix the cause, and rerun `update --dry-run` for a fresh plan. Do not restore over concurrent edits blindly. | @@ -258,6 +258,20 @@ Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*
+
+OpenCode Zen + +Run `/quota_status` and check the `opencode_zen` section. `console_auth_state` shows whether OpenCode returned your Console sign-in, `console_server` and `console_org` show which Console and organization Zen reads, and `budget_source` shows whether the monthly budget came from the org budget (`org_budget`) or the credit limit plus this month's usage (`credit_limit`). `live_fetch_error` lists failed Console routes. The token is never shown. + +| Symptom | Fix | +| --- | --- | +| Zen does not appear | Run `opencode auth login opencode` and sign in to the Console. An OpenCode API key alone is not a Console sign-in. To see a hint instead of nothing, include `opencode` in `enabledProviders`. | +| `OpenCode Console sign-in failed` or `session expired or invalid` | Run `opencode auth login opencode` again. | +| Wrong organization | Run `opencode auth switch opencode` to pick another saved sign-in, or sign in again and pick the organization. Check `console_org`. | +| `OpenCode Console error 403` | Your sign-in cannot read that route for this organization. Zen still shows the other rows, unless the route is `billing/status` (the balance). | + +
+
Token reports diff --git a/docs/readme/updating.md b/docs/readme/updating.md index 038abee5..11a924bd 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -121,18 +121,14 @@ Workspace/cookie material cannot be converted into the official API key. Do not ### OpenCode Zen findings -`OPENCODE_WORKSPACE_ID` and `OPENCODE_AUTH_COOKIE` are ambiguous names: they may come from an older Zen setup, but they may instead belong to OpenCode's workspace feature. Current quota code ignores them. The updater reports them only when it finds no supported global `opencode-quota/opencode.json` path, and it does not read or move their values. +OpenCode Zen now uses your OpenCode Console sign-in: run `opencode auth login opencode` (and `opencode auth switch opencode` to pick a saved organization), then verify with `/quota_status` or terminal `opencode-quota status`. See [OpenCode Zen setup](providers.md#opencode-zen). -First decide whether those variables really contain Zen credentials. If they do, the old `auth` cookie no longer works after the OpenCode Console redesign: create the supported file under your global OpenCode config directory with a fresh `__Host-console_session` cookie ([how to find it](providers.md#opencode-zen)). The usual path is `~/.config/opencode/opencode-quota/opencode.json`: +The updater reports two leftovers from the old workspace/cookie setup without reading or moving their values: -```json -{ - "workspaceId": "wrk_your-workspace-id", - "consoleSessionCookie": "your-console-session-cookie" -} -``` +- A global `opencode-quota/opencode.json` file: it is no longer read. Remove it manually after Zen works. +- `OPENCODE_WORKSPACE_ID` / `OPENCODE_AUTH_COOKIE`: current quota code ignores them. They may come from an older Zen setup or belong to OpenCode's workspace feature. Remove them only if they held Zen credentials. -Use placeholders while documenting or sharing the setup; never share the real values. Restrict file access to your user account, verify with `/quota_status` or terminal `opencode-quota status`, and only then remove obsolete environment declarations manually. If the variables belong to OpenCode's workspace feature, leave them with that feature instead of treating them as Zen credentials. +Never share the real values. ## Cancellation, failures, and reruns diff --git a/src/lib/opencode-zen-config.ts b/src/lib/opencode-zen-config.ts deleted file mode 100644 index 4f98d6b8..00000000 --- a/src/lib/opencode-zen-config.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { readFile } from "fs/promises"; -import { join } from "path"; - -import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; - -export interface OpenCodeZenConfig { - workspaceId: string; - consoleSessionCookie: string; -} - -export type ResolvedOpenCodeZenConfig = - | { state: "none" } - | { state: "configured"; config: OpenCodeZenConfig; source: string } - | { state: "incomplete"; source: string; missing: string } - | { state: "invalid"; source: string; error: string }; - -export interface OpenCodeZenConfigDiagnostics { - state: ResolvedOpenCodeZenConfig["state"]; - source: string | null; - missing: string | null; - error: string | null; - checkedPaths: string[]; -} - -type ReadConfigFileResult = - | { state: "missing" } - | { state: "loaded"; config: Partial } - | { state: "invalid"; error: string }; - -const LEGACY_AUTH_COOKIE_ERROR = - "authCookie no longer works after the OpenCode Console redesign; paste the __Host-console_session cookie as consoleSessionCookie"; - -function getConfigCandidatePaths(): string[] { - return [join(getOpencodeRuntimeDirs().configDir, "opencode-quota", "opencode.json")]; -} - -function getConfigFileError(error: unknown): string { - if (error instanceof SyntaxError) { - return "Failed to parse JSON"; - } - if (error instanceof Error && error.message) { - return `Failed to read config file: ${error.message}`; - } - return `Failed to read config file: ${String(error)}`; -} - -async function readConfigFile(path: string): Promise { - try { - const data = await readFile(path, "utf8"); - const parsed = JSON.parse(data) as Record; - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return { state: "invalid", error: "Config file must contain a JSON object" }; - } - return { state: "loaded", config: parsed as Partial }; - } catch (error) { - if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") { - return { state: "missing" }; - } - return { state: "invalid", error: getConfigFileError(error) }; - } -} - -export async function resolveOpenCodeZenConfig(): Promise { - for (const path of getConfigCandidatePaths()) { - const fileResult = await readConfigFile(path); - if (fileResult.state === "missing") continue; - if (fileResult.state === "invalid") { - return { state: "invalid", source: path, error: fileResult.error }; - } - - const workspaceId = - typeof fileResult.config.workspaceId === "string" ? fileResult.config.workspaceId.trim() : ""; - const consoleSessionCookie = - typeof fileResult.config.consoleSessionCookie === "string" - ? fileResult.config.consoleSessionCookie.trim() - : ""; - - if (!consoleSessionCookie && "authCookie" in fileResult.config) { - return { state: "invalid", source: path, error: LEGACY_AUTH_COOKIE_ERROR }; - } - - if (workspaceId && consoleSessionCookie) { - return { - state: "configured", - config: { workspaceId, consoleSessionCookie }, - source: path, - }; - } - - return { - state: "incomplete", - source: path, - missing: workspaceId ? "consoleSessionCookie" : "workspaceId", - }; - } - - return { state: "none" }; -} - -let cachedConfig: ResolvedOpenCodeZenConfig | null = null; -let cachedAt = 0; - -export const DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS = 30_000; - -export async function resolveOpenCodeZenConfigCached(params?: { - maxAgeMs?: number; -}): Promise { - const maxAgeMs = Math.max(0, params?.maxAgeMs ?? DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS); - const now = Date.now(); - if (cachedConfig && now - cachedAt < maxAgeMs) { - return cachedConfig; - } - - cachedConfig = await resolveOpenCodeZenConfig(); - cachedAt = now; - return cachedConfig; -} - -export async function getOpenCodeZenConfigDiagnostics(): Promise { - const resolved = await resolveOpenCodeZenConfig(); - return { - state: resolved.state, - source: "source" in resolved ? resolved.source : null, - missing: "missing" in resolved ? resolved.missing : null, - error: "error" in resolved ? resolved.error : null, - checkedPaths: getConfigCandidatePaths(), - }; -} diff --git a/src/lib/opencode-zen.ts b/src/lib/opencode-zen.ts index b880dca7..eaa71a10 100644 --- a/src/lib/opencode-zen.ts +++ b/src/lib/opencode-zen.ts @@ -1,11 +1,14 @@ import { sanitizeDisplayText } from "./display-sanitize.js"; import { fetchWithTimeout } from "./http.js"; +import { + consoleBaseUrl, + consoleHeaders, + type OpenCodeConsoleCredential, +} from "./opencode-console-auth.js"; -const CONSOLE_API_URL = "https://opencode.ai/console/api"; -const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0"; const CONSOLE_TIMEOUT_MS = 10_000; const SESSION_ERROR = - "OpenCode Console session expired or invalid — paste a fresh __Host-console_session cookie as consoleSessionCookie"; + "OpenCode Console session expired or invalid. Run `opencode auth login opencode` to sign in again."; /** * The OpenCode Console reports amounts in micro-cents: @@ -13,17 +16,26 @@ const SESSION_ERROR = */ export const OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR = 100_000_000; +/** + * Where the monthly limit and usage come from: the org budget (budgets/org), or the + * credit limit (billing/account) plus this month's usage costs (usage/cost-by-day). + */ +export type OpenCodeZenBudgetSource = "org_budget" | "credit_limit"; + export interface OpenCodeZenBillingData { balance: number; - /** Credit limit in USD; null when the account has no limit or billing/account failed. */ + /** Monthly limit in USD from `budgetSource`; null when there is no limit or its route failed. */ monthlyLimit: number | null; - /** Current-month usage in billing units; null when usage/cost-by-day failed. */ + /** Current-month usage in billing units from `budgetSource`; null when its route failed. */ monthlyUsage: number | null; lastPayment: number | null; /** Auto-reload state; null (unknown) when billing/auto-recharge failed. */ reload: boolean | null; reloadAmount: number | null; reloadTrigger: number | null; + /** Org-budget reset ISO timestamp; only set when budgets/org supplies the monthly budget. */ + budgetResetIso: string | null; + budgetSource: OpenCodeZenBudgetSource; } /** @@ -38,6 +50,7 @@ type ConsoleRoute = | "billing/status" | "billing/account" | "billing/auto-recharge" + | "budgets/org" | "usage/cost-by-day"; type ConsoleRouteResult = { success: true; data: T } | { success: false; error: string }; @@ -114,7 +127,42 @@ function parseMonthlyUsage(json: unknown, now: Date): number { return Number.isFinite(total) ? total : invalidResponse(); } -function sanitizeMessage(text: string, secrets: string[] = [], maxLength = 120): string { +/** Parses the org budget; a null limit means the org has no budget configured. */ +function parseOrgBudget(json: unknown): { + limitMicroCents: number | null; + spentMicroCents: number | null; + resetsAt: string | null; +} { + const budget = asRecord(json); + if (!budget) return invalidResponse(); + + const limitValue = budget.limitMicroCents; + if (limitValue === undefined) return invalidResponse(); + const limit = limitValue === null ? null : parseMicroCents(limitValue); + if (limitValue !== null && limit === null) return invalidResponse(); + + const spentValue = budget.spentMicroCents; + if (spentValue === undefined) return invalidResponse(); + const spent = spentValue === null ? null : parseMicroCents(spentValue); + if (spentValue !== null && spent === null) return invalidResponse(); + + const resetsAt = budget.resetsAt; + if (resetsAt !== null && resetsAt !== undefined && typeof resetsAt !== "string") { + return invalidResponse(); + } + + return { + limitMicroCents: limit, + spentMicroCents: spent, + resetsAt: typeof resetsAt === "string" ? resetsAt : null, + }; +} + +function sanitizeMessage( + text: string, + secrets: Array = [], + maxLength = 120, +): string { let sanitized = sanitizeDisplayText(text).replace(/\s+/g, " ").trim(); for (const secret of secrets) { if (secret) sanitized = sanitized.split(secret).join("[redacted]"); @@ -124,30 +172,22 @@ function sanitizeMessage(text: string, secrets: string[] = [], maxLength = 120): async function fetchConsoleRoute(params: { route: ConsoleRoute; - workspaceId: string; - consoleSessionCookie: string; + credential: OpenCodeConsoleCredential; timeoutMs: number; parse: (json: unknown) => T; }): Promise> { try { - return await fetchWithTimeout(`${CONSOLE_API_URL}/${params.route}`, { + return await fetchWithTimeout(`${consoleBaseUrl(params.credential)}/api/${params.route}`, { request: { method: "GET", redirect: "manual", - headers: { - "User-Agent": USER_AGENT, - Accept: "application/json", - Cookie: `__Host-console_session=${params.consoleSessionCookie}`, - "x-org-id": params.workspaceId, - }, + headers: consoleHeaders(params.credential), }, timeoutMs: params.timeoutMs, consume: async (response): Promise> => { - if ( - (response.status >= 300 && response.status < 400) || - response.status === 401 || - response.status === 403 - ) { + // Redirects and 401 mean the whole session is gone; a 403 is treated as + // a per-route failure so optional routes can fall back gracefully. + if ((response.status >= 300 && response.status < 400) || response.status === 401) { return { success: false, error: SESSION_ERROR }; } if (!response.ok) { @@ -173,28 +213,27 @@ async function fetchConsoleRoute(params: { }); } catch (error) { const message = sanitizeMessage(error instanceof Error ? error.message : String(error), [ - params.consoleSessionCookie, - params.workspaceId, + params.credential.accessToken, + params.credential.orgId, ]); return { success: false, error: `OpenCode Console ${params.route} request failed: ${message}` }; } } export async function queryOpenCodeZenQuota( - workspaceId: string, - consoleSessionCookie: string, + credential: OpenCodeConsoleCredential, options: { requestTimeoutMs?: number } = {}, ): Promise { const request = { - workspaceId, - consoleSessionCookie, + credential, timeoutMs: options.requestTimeoutMs ?? CONSOLE_TIMEOUT_MS, }; const now = new Date(); - const [balance, creditLimit, autoRecharge, monthlyUsage] = await Promise.all([ + const [balance, creditLimit, autoRecharge, orgBudget, monthlyUsage] = await Promise.all([ fetchConsoleRoute({ ...request, route: "billing/status", parse: parseBalance }), fetchConsoleRoute({ ...request, route: "billing/account", parse: parseCreditLimit }), fetchConsoleRoute({ ...request, route: "billing/auto-recharge", parse: parseAutoRecharge }), + fetchConsoleRoute({ ...request, route: "budgets/org", parse: parseOrgBudget }), fetchConsoleRoute({ ...request, route: "usage/cost-by-day", @@ -202,22 +241,48 @@ export async function queryOpenCodeZenQuota( }), ]); - const optional = [creditLimit, autoRecharge, monthlyUsage]; + const optional = [creditLimit, autoRecharge, orgBudget, monthlyUsage]; if ([balance, ...optional].some((result) => !result.success && result.error === SESSION_ERROR)) { return { success: false, error: SESSION_ERROR }; } if (!balance.success) return balance; + // Prefer the org budget when it supplies a positive limit and usable spend; + // otherwise fall back to the credit limit plus this month's usage costs. + let monthlyLimit = creditLimit.success ? creditLimit.data : null; + let usage = monthlyUsage.success ? monthlyUsage.data : null; + let budgetResetIso: string | null = null; + let budgetSource: OpenCodeZenBudgetSource = "credit_limit"; + if (orgBudget.success) { + const { limitMicroCents, spentMicroCents, resetsAt } = orgBudget.data; + if ( + limitMicroCents !== null && + limitMicroCents > 0 && + spentMicroCents !== null && + spentMicroCents >= 0 + ) { + monthlyLimit = limitMicroCents / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; + usage = spentMicroCents; + budgetSource = "org_budget"; + // Normalize to canonical ISO so downstream result validation never sees a + // parseable-but-non-ISO reset (e.g. "0") and drops the whole result. + const resetTimeMs = resetsAt === null ? Number.NaN : Date.parse(resetsAt); + budgetResetIso = Number.isFinite(resetTimeMs) ? new Date(resetTimeMs).toISOString() : null; + } + } + return { success: true, data: { balance: balance.data, - monthlyLimit: creditLimit.success ? creditLimit.data : null, - monthlyUsage: monthlyUsage.success ? monthlyUsage.data : null, + monthlyLimit, + monthlyUsage: usage, lastPayment: null, ...(autoRecharge.success ? autoRecharge.data : { reload: null, reloadAmount: null, reloadTrigger: null }), + budgetResetIso, + budgetSource, }, errors: optional.flatMap((result) => (result.success ? [] : [result.error])), }; diff --git a/src/lib/provider-registration.ts b/src/lib/provider-registration.ts index 46a398ec..c3272273 100644 --- a/src/lib/provider-registration.ts +++ b/src/lib/provider-registration.ts @@ -339,12 +339,11 @@ export const QUOTA_PROVIDER_REGISTRATION_SOURCE = [ runtimeIds: ["opencode", "opencode-zen"], synonyms: ["opencode-zen"], shape: { - autoSetup: "needs_quick_setup", - authentication: "state_only", + autoSetup: "yes", + authentication: "opencode_auth_oauth_token", quota: "remote_api", - quickSetupAnchor: "opencode-zen", notes: - "Reads the OpenCode Console billing API; requires workspaceId and consoleSessionCookie", + "Reads the OpenCode Console billing, budget and usage APIs with your OpenCode Console sign-in", }, }, { diff --git a/src/lib/scoped-update-migration.ts b/src/lib/scoped-update-migration.ts index 3ea99bb2..4c1b87c0 100644 --- a/src/lib/scoped-update-migration.ts +++ b/src/lib/scoped-update-migration.ts @@ -35,7 +35,10 @@ export type ScopedUpdateManualFinding = | { kind: "ambiguous-zen-env"; names: Array<"OPENCODE_WORKSPACE_ID" | "OPENCODE_AUTH_COOKIE">; - suggestedPath: string; + } + | { + kind: "obsolete-zen-file"; + path: string; } | { kind: "display-migration-manual"; @@ -71,7 +74,8 @@ export const OBSOLETE_GO_ENV_NAMES = [ export const AMBIGUOUS_ZEN_ENV_NAMES = ["OPENCODE_WORKSPACE_ID", "OPENCODE_AUTH_COOKIE"] as const; export const OBSOLETE_GO_FILE = "opencode-quota/opencode-go.json"; -export const SUPPORTED_ZEN_FILE = "opencode-quota/opencode.json"; +/** OpenCode Zen workspace/cookie file, replaced by the `opencode auth login opencode` sign-in. */ +export const OBSOLETE_ZEN_FILE = "opencode-quota/opencode.json"; export interface ScopedUpdateMigrationCandidate { path: string; @@ -439,25 +443,23 @@ export async function auditObsoleteUpdateSources(params: { } const goPath = join(params.configDir, OBSOLETE_GO_FILE); - const zenPath = join(params.configDir, SUPPORTED_ZEN_FILE); + const zenPath = join(params.configDir, OBSOLETE_ZEN_FILE); const [goPresent, zenPresent] = await Promise.all([ knownPathExists(goPath, "inspect obsolete OpenCode Go source"), - knownPathExists(zenPath, "inspect supported OpenCode Zen source"), + knownPathExists(zenPath, "inspect obsolete OpenCode Zen source"), ]); if (goPresent) { findings.push({ kind: "obsolete-go-file", path: goPath }); } - if (!zenPresent) { - const names = AMBIGUOUS_ZEN_ENV_NAMES.filter((name) => Object.hasOwn(params.env, name)); - if (names.length > 0) { - findings.push({ - kind: "ambiguous-zen-env", - names: [...names], - suggestedPath: zenPath, - }); - } + const zenNames = AMBIGUOUS_ZEN_ENV_NAMES.filter((name) => Object.hasOwn(params.env, name)); + if (zenNames.length > 0) { + findings.push({ kind: "ambiguous-zen-env", names: [...zenNames] }); + } + + if (zenPresent) { + findings.push({ kind: "obsolete-zen-file", path: zenPath }); } return findings; @@ -481,7 +483,9 @@ function manualFindingSortKey(finding: ScopedUpdateManualFinding): string { case "obsolete-go-file": return `${finding.kind}\u0000${finding.path}`; case "ambiguous-zen-env": - return `${finding.kind}\u0000${finding.suggestedPath}\u0000${finding.names.join("\u0000")}`; + return `${finding.kind}\u0000${finding.names.join("\u0000")}`; + case "obsolete-zen-file": + return `${finding.kind}\u0000${finding.path}`; case "display-migration-manual": return `${finding.kind}\u0000${finding.path}\u0000${finding.container}\u0000${finding.reason}`; case "migration-file-uninspectable": diff --git a/src/lib/scoped-update.ts b/src/lib/scoped-update.ts index e3906487..a497dd1c 100644 --- a/src/lib/scoped-update.ts +++ b/src/lib/scoped-update.ts @@ -456,17 +456,19 @@ function formatSafeAction(action: ScopedUpdateSafeAction): string { const OBSOLETE_GO_GUIDANCE = "OpenCode Go no longer uses this workspace/cookie source, and it cannot be converted into the official API key. Configure OPENCODE_API_KEY, trusted global provider.opencode-go.options.apiKey, fallback provider.opencode.options.apiKey, or run opencode auth login opencode-go. This updater will not read, copy, or delete credentials; remove the old variable/file manually after the supported key works."; +const ZEN_CONSOLE_GUIDANCE = + "OpenCode Zen now uses the OpenCode Console sign-in: run opencode auth login opencode (and opencode auth switch opencode to pick a saved organization), then verify with opencode-quota status."; + function formatManualFinding(finding: ScopedUpdateManualFinding): string { switch (finding.kind) { case "obsolete-go-env": return ` ${finding.name}: ${OBSOLETE_GO_GUIDANCE}`; case "obsolete-go-file": return ` ${displayUpdatePath(finding.path)}: ${OBSOLETE_GO_GUIDANCE}`; - case "ambiguous-zen-env": { - const names = finding.names.join(" and "); - const suggestedPath = displayUpdatePath(finding.suggestedPath); - return ` ${names}: These environment names may be from an older OpenCode Zen setup, but they may also belong to OpenCode's workspace feature. Current quota code ignores them, and no supported global opencode-quota/opencode.json was found. Review the variables, then create and protect the supported file manually only if they are Zen credentials. Suggested path: ${suggestedPath}. This updater will not read, print, or move their values.`; - } + case "ambiguous-zen-env": + return ` ${finding.names.join(" and ")}: These environment names may be from an older OpenCode Zen setup, but they may also belong to OpenCode's workspace feature. Current quota code ignores them. ${ZEN_CONSOLE_GUIDANCE} If they held Zen credentials, remove them manually after Zen works; if they belong to OpenCode's workspace feature, leave them. This updater will not read, print, or move their values.`; + case "obsolete-zen-file": + return ` ${displayUpdatePath(finding.path)}: OpenCode Zen no longer reads this workspace/cookie file. ${ZEN_CONSOLE_GUIDANCE} This updater will not read, copy, or delete credentials; remove the old file manually after Zen works.`; case "display-migration-manual": { const path = displayUpdatePath(finding.path); switch (finding.reason) { diff --git a/src/providers/cache-policies.ts b/src/providers/cache-policies.ts index 31b11db6..7176d351 100644 --- a/src/providers/cache-policies.ts +++ b/src/providers/cache-policies.ts @@ -27,14 +27,11 @@ import { import { resolveNanoGptApiKey } from "../lib/nanogpt-config.js"; import { resolveOllamaCloudApiKey } from "../lib/ollama-cloud-config.js"; import { resolveOpenAIAuthIdentity } from "../lib/openai.js"; +import { consoleBaseUrl, resolveOpenCodeConsoleAuth } from "../lib/opencode-console-auth.js"; import { DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, resolveOpenCodeGoAuthCached, } from "../lib/opencode-go-auth.js"; -import { - DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS, - resolveOpenCodeZenConfigCached, -} from "../lib/opencode-zen-config.js"; import { resolveOpenRouterAuthIdentity } from "../lib/openrouter.js"; import type { CanonicalQuotaProviderId } from "../lib/provider-registration.js"; import type { @@ -234,15 +231,16 @@ export const PROVIDER_CACHE_POLICIES = { return resolved.state === "configured" ? { credential: resolved.apiKey } : null; }), opencode: resolvedCredentialPolicy("opencode", async () => { - const resolved = await resolveOpenCodeZenConfigCached({ - maxAgeMs: DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS, - }); - return resolved.state === "configured" - ? { - credential: resolved.config.workspaceId, - principalKind: "stable-id", - } - : null; + const resolved = await resolveOpenCodeConsoleAuth(); + if (resolved.state !== "configured") return null; + const { credential } = resolved; + // The identity is the org (else the account, else the token) plus the Console URL: + // org ids can collide across self-hosted Console URLs. + return { + credential: credential.orgId ?? credential.accountId ?? credential.accessToken, + principalKind: credential.orgId || credential.accountId ? "stable-id" : "credential", + qualifiers: [consoleBaseUrl(credential)], + }; }), "ollama-cloud": resolvedCredentialPolicy("ollama-cloud", async () => { const resolved = await resolveOllamaCloudApiKey(); diff --git a/src/providers/opencode-zen.ts b/src/providers/opencode-zen.ts index e0337333..adc98dfb 100644 --- a/src/providers/opencode-zen.ts +++ b/src/providers/opencode-zen.ts @@ -1,26 +1,27 @@ -import { sanitizeDisplayText } from "../lib/display-sanitize.js"; import type { AccountingMetadata, QuotaProvider, QuotaProviderContext, QuotaProviderResult, + QuotaProviderStatusDetail, QuotaToastEntry, } from "../lib/entries.js"; +import { scrubCredentialErrorText } from "../lib/opencode-auth.js"; +import { + consoleBaseUrl, + type OpenCodeConsoleCredential, + resolveOpenCodeConsoleAuth, +} from "../lib/opencode-console-auth.js"; import { OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR, queryOpenCodeZenQuota, } from "../lib/opencode-zen.js"; -import { - DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS, - getOpenCodeZenConfigDiagnostics, - resolveOpenCodeZenConfigCached, -} from "../lib/opencode-zen-config.js"; import { normalizeQuotaProviderId } from "../lib/provider-metadata.js"; import { attemptedErrorResult, attemptedResult, - configStatusDetails, notAttemptedResult, + statusDetailsFromRecord, withStatusDetails, } from "./result-helpers.js"; @@ -28,24 +29,58 @@ const OPENCODE_PROVIDER_LABEL = "OpenCode"; const OPENCODE_ZEN_GROUP = "OpenCode Zen"; const OPENCODE_ZEN_BALANCE_ACCOUNTING: AccountingMetadata = { resultType: "balance", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "provider_reported", }; const OPENCODE_ZEN_BUDGET_ACCOUNTING: AccountingMetadata = { resultType: "budget", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "locally_derived", }; +const OPENCODE_ZEN_SPEND_ACCOUNTING: AccountingMetadata = { + resultType: "spend", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", +}; const OPENCODE_ZEN_STATUS_ACCOUNTING: AccountingMetadata = { resultType: "status", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "provider_reported", }; const USD_UNIT = { kind: "currency", code: "USD" } as const; +const LOGIN_HINT = "Run `opencode auth login opencode`."; + +/** True when the user listed `opencode` in `enabledProviders` instead of using auto mode. */ +function isExplicitlyEnabled(ctx: QuotaProviderContext): boolean { + return ( + Array.isArray(ctx.config.enabledProviders) && ctx.config.enabledProviders.includes("opencode") + ); +} + +function signInFailedResult(state: "expired" | "invalid", detail: string): QuotaProviderResult { + return withStatusDetails( + attemptedErrorResult( + OPENCODE_PROVIDER_LABEL, + `OpenCode Console sign-in failed: ${detail}. ${LOGIN_HINT}`, + ), + [{ key: "console_auth_state", value: state }], + ); +} + +/** Never includes the token. */ +function consoleStatusDetails(credential: OpenCodeConsoleCredential): QuotaProviderStatusDetail[] { + return statusDetailsFromRecord({ + console_auth_state: "configured", + console_server: consoleBaseUrl(credential), + console_org: credential.orgName ?? credential.orgId ?? "(none)", + }); +} + function zenUsdDecimal(value: number): string { const fixed = value.toFixed(8); return fixed.replace(/0+$/u, "").replace(/\.$/u, ""); @@ -54,11 +89,13 @@ function zenUsdDecimal(value: number): string { export const opencodeZenProvider: QuotaProvider = { id: "opencode", - async isAvailable(_ctx: QuotaProviderContext): Promise { - const config = await resolveOpenCodeZenConfigCached({ - maxAgeMs: DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS, - }); - return config.state === "configured"; + async isAvailable(ctx: QuotaProviderContext): Promise { + const consoleAuth = await resolveOpenCodeConsoleAuth(); + // Without a Console sign-in, auto mode stays quiet; an explicit + // enabledProviders entry keeps Zen so fetch() can show the sign-in hint. + if (consoleAuth.state === "none") return isExplicitlyEnabled(ctx); + // An expired or failed sign-in stays available so fetch() can show its hint. + return true; }, matchesCurrentModel(model: string): boolean { @@ -67,46 +104,39 @@ export const opencodeZenProvider: QuotaProvider = { }, async fetch(ctx: QuotaProviderContext): Promise { - const diagnostics = await getOpenCodeZenConfigDiagnostics(); - const statusDetails = configStatusDetails({ - ...diagnostics, - error: diagnostics.error ? sanitizeDisplayText(diagnostics.error) : undefined, - }); - const config = await resolveOpenCodeZenConfigCached({ - maxAgeMs: DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS, - }); - - if (config.state === "none") return withStatusDetails(notAttemptedResult(), statusDetails); + const consoleAuth = await resolveOpenCodeConsoleAuth(); - if (config.state === "incomplete") { + if (consoleAuth.state === "none") { + const noneStatusDetails = [{ key: "console_auth_state", value: "none" }]; + if (!isExplicitlyEnabled(ctx)) { + return withStatusDetails(notAttemptedResult(), noneStatusDetails); + } return withStatusDetails( attemptedErrorResult( OPENCODE_PROVIDER_LABEL, - `Missing ${config.missing} (source: ${config.source})`, + `No OpenCode Console sign-in found. ${LOGIN_HINT}`, ), - statusDetails, + noneStatusDetails, ); } - if (config.state === "invalid") { - return withStatusDetails( - attemptedErrorResult( - OPENCODE_PROVIDER_LABEL, - `Invalid config (${config.source}): ${config.error}`, - ), - statusDetails, - ); + if (consoleAuth.state === "invalid") { + return signInFailedResult("invalid", scrubCredentialErrorText(consoleAuth.error)); } - const result = await queryOpenCodeZenQuota( - config.config.workspaceId, - config.config.consoleSessionCookie, - { - requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured - ? ctx.config.requestTimeoutMs - : undefined, - }, - ); + if (consoleAuth.state === "expired") { + // OpenCode refreshes the Console token when it is read, so this is rare. + return signInFailedResult("expired", "the sign-in expired"); + } + + const { credential } = consoleAuth; + const statusDetails = consoleStatusDetails(credential); + + const result = await queryOpenCodeZenQuota(credential, { + requestTimeoutMs: ctx.config?.requestTimeoutMsConfigured + ? ctx.config.requestTimeoutMs + : undefined, + }); if (!result.success) { return withStatusDetails(attemptedErrorResult(OPENCODE_PROVIDER_LABEL, result.error), [ @@ -123,13 +153,13 @@ export const opencodeZenProvider: QuotaProvider = { ? null : result.data.monthlyUsage / OPENCODE_ZEN_BILLING_UNITS_PER_DOLLAR; + const hasMonthlyUsage = + monthlyUsageUsd !== null && Number.isFinite(monthlyUsageUsd) && monthlyUsageUsd >= 0; const hasMonthlyBudget = effectiveMonthlyLimit !== null && Number.isFinite(effectiveMonthlyLimit) && effectiveMonthlyLimit > 0 && - monthlyUsageUsd !== null && - Number.isFinite(monthlyUsageUsd) && - monthlyUsageUsd >= 0; + hasMonthlyUsage; const entries: QuotaToastEntry[] = []; if (hasMonthlyBudget) { @@ -139,6 +169,7 @@ export const opencodeZenProvider: QuotaProvider = { name: "zen-monthly-budget", group: OPENCODE_ZEN_GROUP, percentRemaining: Math.min(100, (monthlyRemainingUsd / effectiveMonthlyLimit) * 100), + ...(result.data.budgetResetIso ? { resetTimeIso: result.data.budgetResetIso } : {}), semantic: { metric: { kind: "window", window: "month" }, prominence: "primary", @@ -159,6 +190,19 @@ export const opencodeZenProvider: QuotaProvider = { }, }, }); + } else if (hasMonthlyUsage) { + // No limit to measure against: still show this month's real spend. + entries.push({ + accounting: OPENCODE_ZEN_SPEND_ACCOUNTING, + kind: "quantity", + name: "zen-monthly-spend", + group: OPENCODE_ZEN_GROUP, + semantic: { + metric: { kind: "window", window: "month" }, + prominence: "primary", + }, + quantity: { decimal: zenUsdDecimal(monthlyUsageUsd), unit: USD_UNIT }, + }); } entries.push({ @@ -199,12 +243,10 @@ export const opencodeZenProvider: QuotaProvider = { : `USD ${zenUsdDecimal(result.data.monthlyLimit)}`, }, { - key: "last_payment_usd", - value: - result.data.lastPayment === null - ? "(none)" - : `USD ${zenUsdDecimal(result.data.lastPayment)}`, + key: "monthly_usage_usd", + value: monthlyUsageUsd === null ? "(unknown)" : `USD ${zenUsdDecimal(monthlyUsageUsd)}`, }, + { key: "budget_source", value: result.data.budgetSource }, { key: "auto_reload", value: result.data.reload === null ? "(unknown)" : String(result.data.reload), diff --git a/tests/helpers/provider-assertions.ts b/tests/helpers/provider-assertions.ts index 6da95904..70c2742b 100644 --- a/tests/helpers/provider-assertions.ts +++ b/tests/helpers/provider-assertions.ts @@ -268,19 +268,25 @@ export const PROVIDER_ACCOUNTING_LEDGER: Record ({ - getOpencodeRuntimeDirs: vi.fn(), -})); - -vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirs: runtimePathMocks.getOpencodeRuntimeDirs, -})); - -const originalEnv = process.env; -const tempRoots: string[] = []; - -async function createConfigDirs(): Promise<[string, string]> { - const root = await mkdtemp(join(tmpdir(), "opencode-zen-config-")); - tempRoots.push(root); - const primary = join(root, "primary"); - const fallback = join(root, "fallback"); - await mkdir(join(primary, "opencode-quota"), { recursive: true }); - await mkdir(join(fallback, "opencode-quota"), { recursive: true }); - return [primary, fallback]; -} - -function configPath(configDir: string): string { - return join(configDir, "opencode-quota", "opencode.json"); -} - -describe("opencode-zen config resolution", () => { - beforeEach(() => { - vi.resetModules(); - vi.clearAllMocks(); - process.env = { ...originalEnv }; - delete process.env.OPENCODE_WORKSPACE_ID; - delete process.env.OPENCODE_AUTH_COOKIE; - delete process.env.OPENCODE_GO_WORKSPACE_ID; - delete process.env.OPENCODE_GO_AUTH_COOKIE; - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ - configDir: join(tmpdir(), "opencode-zen-config-missing"), - }); - }); - - afterEach(async () => { - process.env = originalEnv; - for (const root of tempRoots.splice(0)) { - await rm(root, { recursive: true, force: true }); - } - }); - - it("ignores OPENCODE_* environment variables and reads only the config file", async () => { - process.env.OPENCODE_WORKSPACE_ID = "wrk_env"; - process.env.OPENCODE_AUTH_COOKIE = "cookie-env"; - const [primary] = await createConfigDirs(); - const path = configPath(primary); - await writeFile( - path, - JSON.stringify({ workspaceId: "wrk_file", consoleSessionCookie: "cookie-file" }), - ); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); - - await expect(resolveOpenCodeZenConfig()).resolves.toEqual({ - state: "configured", - config: { workspaceId: "wrk_file", consoleSessionCookie: "cookie-file" }, - source: path, - }); - }); - - it("ignores OPENCODE_* environment variables when no config file exists", async () => { - process.env.OPENCODE_WORKSPACE_ID = "wrk_env"; - process.env.OPENCODE_AUTH_COOKIE = "cookie-env"; - - const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); - - await expect(resolveOpenCodeZenConfig()).resolves.toEqual({ state: "none" }); - }); - - it("reads the first trusted runtime config file", async () => { - const [primary] = await createConfigDirs(); - const path = configPath(primary); - await writeFile( - path, - JSON.stringify({ workspaceId: " wrk_file ", consoleSessionCookie: " cookie-file " }), - ); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); - - await expect(resolveOpenCodeZenConfig()).resolves.toEqual({ - state: "configured", - config: { workspaceId: "wrk_file", consoleSessionCookie: "cookie-file" }, - source: path, - }); - }); - - it("returns incomplete for missing and wrong-type file fields", async () => { - const [primary] = await createConfigDirs(); - const path = configPath(primary); - await writeFile(path, JSON.stringify({ workspaceId: 123, consoleSessionCookie: "cookie" })); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); - - await expect(resolveOpenCodeZenConfig()).resolves.toEqual({ - state: "incomplete", - source: path, - missing: "workspaceId", - }); - }); - - it("reports an old authCookie key without using its value", async () => { - const [primary] = await createConfigDirs(); - const path = configPath(primary); - await writeFile(path, JSON.stringify({ workspaceId: "wrk_file", authCookie: "old-cookie" })); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { getOpenCodeZenConfigDiagnostics, resolveOpenCodeZenConfig } = await import( - "../src/lib/opencode-zen-config.js" - ); - const expectedError = - "authCookie no longer works after the OpenCode Console redesign; paste the __Host-console_session cookie as consoleSessionCookie"; - - await expect(resolveOpenCodeZenConfig()).resolves.toEqual({ - state: "invalid", - source: path, - error: expectedError, - }); - const diagnostics = await getOpenCodeZenConfigDiagnostics(); - expect(diagnostics).toMatchObject({ state: "invalid", error: expectedError }); - expect(JSON.stringify(diagnostics)).not.toContain("old-cookie"); - }); - - it("ignores an old authCookie key once consoleSessionCookie is set", async () => { - const [primary] = await createConfigDirs(); - const path = configPath(primary); - await writeFile( - path, - JSON.stringify({ - workspaceId: "wrk_file", - authCookie: "old-cookie", - consoleSessionCookie: "session-file", - }), - ); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); - - await expect(resolveOpenCodeZenConfig()).resolves.toEqual({ - state: "configured", - config: { workspaceId: "wrk_file", consoleSessionCookie: "session-file" }, - source: path, - }); - }); - - it("stops at the first invalid config instead of falling through", async () => { - const [primary, fallback] = await createConfigDirs(); - await writeFile(configPath(primary), "[]"); - await writeFile( - configPath(fallback), - JSON.stringify({ workspaceId: "wrk_ok", consoleSessionCookie: "cookie-ok" }), - ); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); - - await expect(resolveOpenCodeZenConfig()).resolves.toEqual({ - state: "invalid", - source: configPath(primary), - error: "Config file must contain a JSON object", - }); - }); - - it("does not include malformed credential text in JSON parse errors", async () => { - const [primary] = await createConfigDirs(); - const path = configPath(primary); - await writeFile(path, '{"consoleSessionCookie":super-secret}'); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { resolveOpenCodeZenConfig } = await import("../src/lib/opencode-zen-config.js"); - - await expect(resolveOpenCodeZenConfig()).resolves.toEqual({ - state: "invalid", - source: path, - error: "Failed to parse JSON", - }); - }); - - it("keeps file-only resolution and public diagnostics unchanged after source audit", async () => { - const [primary] = await createConfigDirs(); - const path = configPath(primary); - const obsoleteGoPath = join(primary, "opencode-quota", "opencode-go.json"); - const zenWorkspaceCanary = "zen-file-workspace-canary"; - const zenCookieCanary = "zen-file-cookie-canary"; - const goFileCanary = "obsolete-go-file-canary"; - process.env.OPENCODE_WORKSPACE_ID = "zen-env-workspace-canary"; - process.env.OPENCODE_AUTH_COOKIE = "zen-env-cookie-canary"; - process.env.OPENCODE_GO_WORKSPACE_ID = "go-env-workspace-canary"; - process.env.OPENCODE_GO_AUTH_COOKIE = "go-env-cookie-canary"; - await writeFile( - path, - JSON.stringify({ workspaceId: zenWorkspaceCanary, consoleSessionCookie: zenCookieCanary }), - ); - await writeFile(obsoleteGoPath, JSON.stringify({ authCookie: goFileCanary })); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { getOpenCodeZenConfigDiagnostics, resolveOpenCodeZenConfig } = await import( - "../src/lib/opencode-zen-config.js" - ); - const beforeResult = await resolveOpenCodeZenConfig(); - const beforeDiagnostics = await getOpenCodeZenConfigDiagnostics(); - const zenBytesBeforeAudit = await readFile(path); - const goBytesBeforeAudit = await readFile(obsoleteGoPath); - const { auditObsoleteUpdateSources } = await import("../src/lib/scoped-update-migration.js"); - const findings = await auditObsoleteUpdateSources({ - env: process.env, - configDir: primary, - }); - expect(await readFile(path)).toEqual(zenBytesBeforeAudit); - expect(await readFile(obsoleteGoPath)).toEqual(goBytesBeforeAudit); - expect(process.env.OPENCODE_WORKSPACE_ID).toBe("zen-env-workspace-canary"); - expect(process.env.OPENCODE_AUTH_COOKIE).toBe("zen-env-cookie-canary"); - expect(process.env.OPENCODE_GO_WORKSPACE_ID).toBe("go-env-workspace-canary"); - expect(process.env.OPENCODE_GO_AUTH_COOKIE).toBe("go-env-cookie-canary"); - const afterResult = await resolveOpenCodeZenConfig(); - const afterDiagnostics = await getOpenCodeZenConfigDiagnostics(); - - expect(beforeResult).toEqual({ - state: "configured", - config: { workspaceId: zenWorkspaceCanary, consoleSessionCookie: zenCookieCanary }, - source: path, - }); - expect(afterResult).toEqual(beforeResult); - expect(afterDiagnostics).toEqual(beforeDiagnostics); - expect(findings).toEqual( - expect.arrayContaining([ - { kind: "obsolete-go-env", name: "OPENCODE_GO_WORKSPACE_ID" }, - { kind: "obsolete-go-env", name: "OPENCODE_GO_AUTH_COOKIE" }, - { kind: "obsolete-go-file", path: obsoleteGoPath }, - ]), - ); - expect(findings).not.toContainEqual(expect.objectContaining({ kind: "ambiguous-zen-env" })); - - const publicBoundary = JSON.stringify({ findings, beforeDiagnostics, afterDiagnostics }); - for (const canary of [ - zenWorkspaceCanary, - zenCookieCanary, - goFileCanary, - "zen-env-workspace-canary", - "zen-env-cookie-canary", - "go-env-workspace-canary", - "go-env-cookie-canary", - ]) { - expect(publicBoundary).not.toContain(canary); - } - }); - - it("caches the resolved credentials within the configured TTL", async () => { - const [primary] = await createConfigDirs(); - const path = configPath(primary); - await writeFile( - path, - JSON.stringify({ workspaceId: "wrk_initial", consoleSessionCookie: "cookie-initial" }), - ); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { resolveOpenCodeZenConfigCached } = await import("../src/lib/opencode-zen-config.js"); - const first = await resolveOpenCodeZenConfigCached({ maxAgeMs: 5_000 }); - await writeFile( - path, - JSON.stringify({ workspaceId: "wrk_changed", consoleSessionCookie: "cookie-changed" }), - ); - - await expect(resolveOpenCodeZenConfigCached({ maxAgeMs: 5_000 })).resolves.toEqual(first); - }); - - it("reports diagnostics without exposing credential values", async () => { - const [primary] = await createConfigDirs(); - const path = configPath(primary); - await writeFile( - path, - JSON.stringify({ workspaceId: "wrk_secret", consoleSessionCookie: "cookie-secret" }), - ); - runtimePathMocks.getOpencodeRuntimeDirs.mockReturnValue({ configDir: primary }); - - const { getOpenCodeZenConfigDiagnostics } = await import("../src/lib/opencode-zen-config.js"); - const diagnostics = await getOpenCodeZenConfigDiagnostics(); - - expect(diagnostics).toEqual({ - state: "configured", - source: path, - missing: null, - error: null, - checkedPaths: [path], - }); - expect(JSON.stringify(diagnostics)).not.toContain("cookie-secret"); - expect(JSON.stringify(diagnostics)).not.toContain("wrk_secret"); - }); -}); diff --git a/tests/lib.opencode-zen.test.ts b/tests/lib.opencode-zen.test.ts index 6f0285e9..117f36f1 100644 --- a/tests/lib.opencode-zen.test.ts +++ b/tests/lib.opencode-zen.test.ts @@ -22,11 +22,17 @@ vi.mock("../src/lib/http.js", () => ({ fetchWithTimeout: mocks.fetchWithTimeout, })); +import type { OpenCodeConsoleCredential } from "../src/lib/opencode-console-auth.js"; import { queryOpenCodeZenQuota } from "../src/lib/opencode-zen.js"; const CONSOLE_API = "https://opencode.ai/console/api"; const SESSION_ERROR = - "OpenCode Console session expired or invalid — paste a fresh __Host-console_session cookie as consoleSessionCookie"; + "OpenCode Console session expired or invalid. Run `opencode auth login opencode` to sign in again."; + +const credential: OpenCodeConsoleCredential = { + accessToken: "st_secret-token", + orgId: "wrk_abc", +}; // Payloads captured from a real account by the maintainer (org id replaced). const STATUS = { @@ -52,6 +58,12 @@ const AUTO_RECHARGE = { pending: false, failureReason: null, }; +const ORG_BUDGET = { + limitMicroCents: "6000000000", + spentMicroCents: "617355570", + exceeded: false, + resetsAt: "2026-10-01T00:00:00.000Z", +}; function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { @@ -60,16 +72,18 @@ function json(body: unknown, status = 200): Response { }); } -function routes(overrides: Record Response> = {}): void { +function routes(overrides: Record Response> = {}, consoleApi = CONSOLE_API): void { const payloads: Record Response> = { "billing/status": () => json(STATUS), "billing/account": () => json(ACCOUNT), "billing/auto-recharge": () => json(AUTO_RECHARGE), + "budgets/org": () => json(ORG_BUDGET), "usage/cost-by-day": () => json([]), ...overrides, }; mocks.fetchResponse.mockImplementation(async (url: string) => { - const route = url.slice(`${CONSOLE_API}/`.length); + if (!url.startsWith(`${consoleApi}/`)) throw new Error(`unexpected url ${url}`); + const route = url.slice(`${consoleApi}/`.length); const payload = payloads[route]; if (!payload) throw new Error(`unexpected url ${url}`); return payload(); @@ -88,15 +102,16 @@ describe("queryOpenCodeZenQuota", () => { vi.useRealTimers(); }); - it("calls the four Console routes with the session cookie and org id", async () => { + it("calls the five Console routes with the Bearer token and org id", async () => { routes(); - await queryOpenCodeZenQuota("wrk_abc", "session-value", { requestTimeoutMs: 4_000 }); + await queryOpenCodeZenQuota(credential, { requestTimeoutMs: 4_000 }); expect(mocks.fetchWithTimeout.mock.calls.map(([url]) => url).sort()).toEqual([ `${CONSOLE_API}/billing/account`, `${CONSOLE_API}/billing/auto-recharge`, `${CONSOLE_API}/billing/status`, + `${CONSOLE_API}/budgets/org`, `${CONSOLE_API}/usage/cost-by-day`, ]); for (const [, options] of mocks.fetchWithTimeout.mock.calls) { @@ -106,38 +121,152 @@ describe("queryOpenCodeZenQuota", () => { redirect: "manual", headers: { Accept: "application/json", - Cookie: "__Host-console_session=session-value", + Authorization: "Bearer st_secret-token", "x-org-id": "wrk_abc", }, }, timeoutMs: 4_000, }); } + for (const [, options] of mocks.fetchWithTimeout.mock.calls) { + expect(options.request.headers).not.toHaveProperty("Cookie"); + } + }); + + it("sends no x-org-id header when the sign-in has no org", async () => { + routes(); + + const result = await queryOpenCodeZenQuota({ accessToken: "st_secret-token" }); + + expect(result.success).toBe(true); + expect(mocks.fetchWithTimeout).toHaveBeenCalledTimes(5); + for (const [, options] of mocks.fetchWithTimeout.mock.calls) { + expect(options.request.headers).toEqual({ + Accept: "application/json", + Authorization: "Bearer st_secret-token", + }); + } + }); + + it("calls the Console server saved with the sign-in", async () => { + const selfHostedApi = "https://console.self-hosted.example/api"; + routes({}, selfHostedApi); + + const result = await queryOpenCodeZenQuota({ + ...credential, + server: "https://console.self-hosted.example", + }); + + expect(result.success).toBe(true); + expect(mocks.fetchWithTimeout.mock.calls.map(([url]) => url).sort()).toEqual([ + `${selfHostedApi}/billing/account`, + `${selfHostedApi}/billing/auto-recharge`, + `${selfHostedApi}/billing/status`, + `${selfHostedApi}/budgets/org`, + `${selfHostedApi}/usage/cost-by-day`, + ]); }); it("parses the real empty-account payloads", async () => { routes(); - await expect(queryOpenCodeZenQuota("wrk_abc", "session-value")).resolves.toEqual({ + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ success: true, data: { balance: 0, - monthlyLimit: null, - monthlyUsage: 0, + monthlyLimit: 60, + monthlyUsage: 617_355_570, + lastPayment: null, + reload: false, + reloadAmount: 20, + reloadTrigger: 5, + budgetResetIso: "2026-10-01T00:00:00.000Z", + budgetSource: "org_budget", + }, + errors: [], + }); + }); + + it("prefers the org budget for the monthly limit, spend, and reset date", async () => { + routes({ + "billing/status": () => json({ ...STATUS, balanceMicroCents: "1822921472" }), + "billing/account": () => json({ ...ACCOUNT, creditLimitMicroCents: "10000000000" }), + "usage/cost-by-day": () => json([{ date: "2026-09-24", totalCostMicroCents: "75000000" }]), + }); + + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ + success: true, + data: { + balance: 1_822_921_472, + monthlyLimit: 60, + monthlyUsage: 617_355_570, lastPayment: null, reload: false, reloadAmount: 20, reloadTrigger: 5, + budgetResetIso: "2026-10-01T00:00:00.000Z", + budgetSource: "org_budget", }, errors: [], }); }); + it("falls back to the credit limit and usage costs when the org budget has no limit", async () => { + routes({ + "billing/status": () => json({ ...STATUS, balanceMicroCents: "4250000000" }), + "billing/account": () => json({ ...ACCOUNT, creditLimitMicroCents: "10000000000" }), + "usage/cost-by-day": () => json([{ date: "2026-09-24", totalCostMicroCents: "75000000" }]), + "budgets/org": () => json({ limitMicroCents: null, spentMicroCents: null, resetsAt: null }), + }); + + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ + success: true, + data: { + balance: 4_250_000_000, + monthlyLimit: 100, + monthlyUsage: 75_000_000, + lastPayment: null, + reload: false, + reloadAmount: 20, + reloadTrigger: 5, + budgetResetIso: null, + budgetSource: "credit_limit", + }, + errors: [], + }); + }); + + it("falls back and lists an error when the org budget route fails", async () => { + routes({ + "billing/status": () => json({ ...STATUS, balanceMicroCents: "4250000000" }), + "billing/account": () => json({ ...ACCOUNT, creditLimitMicroCents: "10000000000" }), + "usage/cost-by-day": () => json([{ date: "2026-09-24", totalCostMicroCents: "75000000" }]), + "budgets/org": () => new Response("server error", { status: 500 }), + }); + + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ + success: true, + data: { + balance: 4_250_000_000, + monthlyLimit: 100, + monthlyUsage: 75_000_000, + lastPayment: null, + reload: false, + reloadAmount: 20, + reloadTrigger: 5, + budgetResetIso: null, + budgetSource: "credit_limit", + }, + errors: ["OpenCode Console budgets/org error 500"], + }); + }); + it("keeps micro-cents as billing units and sums only the current month's costs", async () => { routes({ "billing/status": () => json({ ...STATUS, balanceMicroCents: "4250000000" }), "billing/account": () => json({ ...ACCOUNT, creditLimitMicroCents: "10000000000" }), "billing/auto-recharge": () => json({ ...AUTO_RECHARGE, enabled: true }), + "budgets/org": () => json({ limitMicroCents: null, spentMicroCents: null, resetsAt: null }), "usage/cost-by-day": () => json([ { date: "2026-08-31", totalCostMicroCents: "900000000" }, @@ -146,7 +275,7 @@ describe("queryOpenCodeZenQuota", () => { ]), }); - await expect(queryOpenCodeZenQuota("wrk_abc", "session-value")).resolves.toEqual({ + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ success: true, data: { balance: 4_250_000_000, @@ -156,6 +285,8 @@ describe("queryOpenCodeZenQuota", () => { reload: true, reloadAmount: 20, reloadTrigger: 5, + budgetResetIso: null, + budgetSource: "credit_limit", }, errors: [], }); @@ -164,15 +295,36 @@ describe("queryOpenCodeZenQuota", () => { it.each([ [ "billing/account", - { monthlyLimit: null, monthlyUsage: 75_000_000, reload: true, reloadAmount: 20 }, + { + monthlyLimit: 60, + monthlyUsage: 617_355_570, + reload: true, + reloadAmount: 20, + budgetResetIso: "2026-10-01T00:00:00.000Z", + budgetSource: "org_budget", + }, ], [ "billing/auto-recharge", - { monthlyLimit: 100, monthlyUsage: 75_000_000, reload: null, reloadAmount: null }, + { + monthlyLimit: 60, + monthlyUsage: 617_355_570, + reload: null, + reloadAmount: null, + budgetResetIso: "2026-10-01T00:00:00.000Z", + budgetSource: "org_budget", + }, ], [ "usage/cost-by-day", - { monthlyLimit: 100, monthlyUsage: null, reload: true, reloadAmount: 20 }, + { + monthlyLimit: 60, + monthlyUsage: 617_355_570, + reload: true, + reloadAmount: 20, + budgetResetIso: "2026-10-01T00:00:00.000Z", + budgetSource: "org_budget", + }, ], ])("keeps the balance when optional %s fails", async (route, expected) => { routes({ @@ -183,7 +335,7 @@ describe("queryOpenCodeZenQuota", () => { [route]: () => new Response("server error", { status: 500 }), }); - const result = await queryOpenCodeZenQuota("wrk_abc", "session-value"); + const result = await queryOpenCodeZenQuota(credential); expect(result).toEqual({ success: true, @@ -202,10 +354,11 @@ describe("queryOpenCodeZenQuota", () => { routes({ "billing/account": failed, "billing/auto-recharge": failed, + "budgets/org": failed, "usage/cost-by-day": failed, }); - await expect(queryOpenCodeZenQuota("wrk_abc", "session-value")).resolves.toEqual({ + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ success: true, data: { balance: 0, @@ -215,10 +368,13 @@ describe("queryOpenCodeZenQuota", () => { reload: null, reloadAmount: null, reloadTrigger: null, + budgetResetIso: null, + budgetSource: "credit_limit", }, errors: [ "OpenCode Console billing/account error 500", "OpenCode Console billing/auto-recharge error 500", + "OpenCode Console budgets/org error 500", "OpenCode Console usage/cost-by-day error 500", ], }); @@ -227,7 +383,7 @@ describe("queryOpenCodeZenQuota", () => { it("clamps a negative balance to zero", async () => { routes({ "billing/status": () => json({ ...STATUS, balanceMicroCents: "-14496" }) }); - const result = await queryOpenCodeZenQuota("wrk_abc", "session-value"); + const result = await queryOpenCodeZenQuota(credential); expect(result).toMatchObject({ success: true, data: { balance: 0 } }); }); @@ -238,7 +394,6 @@ describe("queryOpenCodeZenQuota", () => { () => new Response(null, { status: 302, headers: { location: "/console/login" } }), ], ["401", () => new Response("unauthorized", { status: 401 })], - ["403", () => new Response("forbidden", { status: 403 })], [ "login page", () => @@ -252,23 +407,117 @@ describe("queryOpenCodeZenQuota", () => { "billing/status": sessionResponse, "billing/account": sessionResponse, "billing/auto-recharge": sessionResponse, + "budgets/org": sessionResponse, "usage/cost-by-day": sessionResponse, }); - const result = await queryOpenCodeZenQuota("wrk_abc", "session-secret"); + const result = await queryOpenCodeZenQuota(credential); expect(result).toEqual({ success: false, error: SESSION_ERROR }); - expect(JSON.stringify(result)).not.toContain("session-secret"); + expect(JSON.stringify(result)).not.toContain("st_secret-token"); + }); + + it("fails the query when the required billing/status route returns 403", async () => { + routes({ "billing/status": () => new Response("forbidden", { status: 403 }) }); + + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ + success: false, + error: "OpenCode Console billing/status error 403", + }); + }); + + it("keeps the balance when only budgets/org is forbidden", async () => { + routes({ + "billing/status": () => json({ ...STATUS, balanceMicroCents: "4250000000" }), + "billing/account": () => json({ ...ACCOUNT, creditLimitMicroCents: "10000000000" }), + "usage/cost-by-day": () => json([{ date: "2026-09-24", totalCostMicroCents: "75000000" }]), + "budgets/org": () => new Response("forbidden", { status: 403 }), + }); + + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ + success: true, + data: { + balance: 4_250_000_000, + monthlyLimit: 100, + monthlyUsage: 75_000_000, + lastPayment: null, + reload: false, + reloadAmount: 20, + reloadTrigger: 5, + budgetResetIso: null, + budgetSource: "credit_limit", + }, + errors: ["OpenCode Console budgets/org error 403"], + }); + }); + + it("falls back to the credit limit when the org budget spend is negative", async () => { + routes({ + "billing/status": () => json({ ...STATUS, balanceMicroCents: "4250000000" }), + "billing/account": () => json({ ...ACCOUNT, creditLimitMicroCents: "10000000000" }), + "usage/cost-by-day": () => json([{ date: "2026-09-24", totalCostMicroCents: "75000000" }]), + "budgets/org": () => json({ ...ORG_BUDGET, spentMicroCents: "-5" }), + }); + + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ + success: true, + data: { + balance: 4_250_000_000, + monthlyLimit: 100, + monthlyUsage: 75_000_000, + lastPayment: null, + reload: false, + reloadAmount: 20, + reloadTrigger: 5, + budgetResetIso: null, + budgetSource: "credit_limit", + }, + errors: [], + }); + }); + + it("keeps a usable org budget but omits an invalid reset date", async () => { + routes({ "budgets/org": () => json({ ...ORG_BUDGET, resetsAt: "not-a-date" }) }); + + const result = await queryOpenCodeZenQuota(credential); + + expect(result).toMatchObject({ + success: true, + data: { + balance: 0, + monthlyLimit: 60, + monthlyUsage: 617_355_570, + budgetResetIso: null, + budgetSource: "org_budget", + }, + errors: [], + }); + }); + + it("normalizes a parseable non-ISO reset date to canonical ISO", async () => { + // "2026-10-01T00:00:00" parses but lacks the UTC offset required of ISO; + // it must be canonicalized, never passed through as-is. + routes({ "budgets/org": () => json({ ...ORG_BUDGET, resetsAt: "2026-10-01T00:00:00" }) }); + + const result = await queryOpenCodeZenQuota(credential); + expect(result.success).toBe(true); + if (!result.success) return; + + expect(result.data.budgetResetIso).toBe(new Date("2026-10-01T00:00:00").toISOString()); + expect(result.data.budgetResetIso?.endsWith("Z")).toBe(true); + expect(result.data).toMatchObject({ balance: 0, monthlyLimit: 60, monthlyUsage: 617_355_570 }); + expect(result.errors).toEqual([]); }); it.each([ "billing/account", "billing/auto-recharge", + "budgets/org", "usage/cost-by-day", ])("reports an expired session when only %s is rejected", async (route) => { routes({ [route]: () => new Response("unauthorized", { status: 401 }) }); - await expect(queryOpenCodeZenQuota("wrk_abc", "session-value")).resolves.toEqual({ + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ success: false, error: SESSION_ERROR, }); @@ -278,14 +527,14 @@ describe("queryOpenCodeZenQuota", () => { const secretBody = "private-body-session-secret"; routes({ "billing/status": () => new Response(secretBody, { status: 500 }) }); - const result = await queryOpenCodeZenQuota("wrk_abc", "session-secret"); + const result = await queryOpenCodeZenQuota(credential); expect(result).toEqual({ success: false, error: "OpenCode Console billing/status error 500", }); expect(JSON.stringify(result)).not.toContain(secretBody); - expect(JSON.stringify(result)).not.toContain("session-secret"); + expect(JSON.stringify(result)).not.toContain("st_secret-token"); }); it.each([ @@ -295,7 +544,7 @@ describe("queryOpenCodeZenQuota", () => { ])("returns a stable parse error for a %s billing/status response", async (_name, payload) => { routes({ "billing/status": payload }); - await expect(queryOpenCodeZenQuota("wrk_abc", "session-value")).resolves.toEqual({ + await expect(queryOpenCodeZenQuota(credential)).resolves.toEqual({ success: false, error: "Could not parse OpenCode Console billing/status response", }); @@ -305,6 +554,9 @@ describe("queryOpenCodeZenQuota", () => { ["billing/account", () => json({ orgId: "wrk_ABC" })], ["billing/account", () => json({ ...ACCOUNT, creditLimitMicroCents: "9".repeat(309) })], ["billing/auto-recharge", () => json({ ...AUTO_RECHARGE, enabled: "no" })], + ["budgets/org", () => json([])], + ["budgets/org", () => json({ limitMicroCents: "abc", spentMicroCents: "0" })], + ["budgets/org", () => json({ limitMicroCents: "6000000000", resetsAt: 7 })], ["usage/cost-by-day", () => json({ days: [] })], ["usage/cost-by-day", () => json([{ date: "2026-09-01", totalCostMicroCents: "abc" }])], [ @@ -322,7 +574,7 @@ describe("queryOpenCodeZenQuota", () => { ])("lists a stable parse error for a malformed %s response", async (route, payload) => { routes({ [route]: payload }); - const result = await queryOpenCodeZenQuota("wrk_abc", "session-value"); + const result = await queryOpenCodeZenQuota(credential); expect(result).toMatchObject({ success: true, @@ -331,19 +583,19 @@ describe("queryOpenCodeZenQuota", () => { }); }); - it("sanitizes network and timeout errors and redacts configured secrets", async () => { + it("sanitizes network and timeout errors and redacts the Bearer token and org id", async () => { mocks.fetchResponse.mockRejectedValue( - new Error("\u001b[31mtimeout for wrk_secret with session-secret\nretry\u001b[0m"), + new Error("\u001b[31mtimeout for wrk_abc with st_secret-token\nretry\u001b[0m"), ); - const result = await queryOpenCodeZenQuota("wrk_secret", "session-secret"); + const result = await queryOpenCodeZenQuota(credential); expect(result).toEqual({ success: false, error: "OpenCode Console billing/status request failed: timeout for [redacted] with [redacted] retry", }); - expect(JSON.stringify(result)).not.toContain("wrk_secret"); - expect(JSON.stringify(result)).not.toContain("session-secret"); + expect(JSON.stringify(result)).not.toContain("wrk_abc"); + expect(JSON.stringify(result)).not.toContain("st_secret-token"); }); }); diff --git a/tests/lib.provider-metadata.test.ts b/tests/lib.provider-metadata.test.ts index 727ca751..6d930f17 100644 --- a/tests/lib.provider-metadata.test.ts +++ b/tests/lib.provider-metadata.test.ts @@ -185,12 +185,11 @@ describe("provider-metadata", () => { }, { id: "opencode", - autoSetup: "needs_quick_setup", - authentication: "state_only", + autoSetup: "yes", + authentication: "opencode_auth_oauth_token", quota: "remote_api", - quickSetupAnchor: "opencode-zen", notes: - "Reads the OpenCode Console billing API; requires workspaceId and consoleSessionCookie", + "Reads the OpenCode Console billing, budget and usage APIs with your OpenCode Console sign-in", }, { id: "ollama-cloud", @@ -440,12 +439,11 @@ describe("provider-metadata", () => { }); expect(getQuotaProviderShape("opencode-zen")).toEqual({ id: "opencode", - autoSetup: "needs_quick_setup", - authentication: "state_only", + autoSetup: "yes", + authentication: "opencode_auth_oauth_token", quota: "remote_api", - quickSetupAnchor: "opencode-zen", notes: - "Reads the OpenCode Console billing API; requires workspaceId and consoleSessionCookie", + "Reads the OpenCode Console billing, budget and usage APIs with your OpenCode Console sign-in", }); expect(getQuotaProviderShape("kilo")).toEqual({ id: "kilo", diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index 81588b0c..af0d7737 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -1202,26 +1202,30 @@ describe("buildQuotaStatusReport", () => { expect(section).not.toContain("workspace/private"); }); - it("reports OpenCode Zen config and live billing details without exposing credentials", async () => { + it("reports OpenCode Zen Console sign-in and live billing details without exposing credentials", async () => { const report = await buildOpenCodeZenStatusReport({ providerLiveProbes: [ makeProviderSuccessProbe("opencode", { - config_state: "configured", - config_source: "env(OPENCODE_*)", - balance_usd: "$42.50", - monthly_limit_usd: "$50.00", - last_payment_usd: "$20.00", + console_auth_state: "configured", + console_server: "https://opencode.ai/console", + console_org: "Acme", + balance_usd: "USD 42.5", + monthly_limit_usd: "USD 50", + monthly_usage_usd: "USD 11.82", + budget_source: "org_budget", }), ], }); expect(report).toContain("opencode_zen:"); - expect(report).toContain("- config_state: configured"); - expect(report).toContain("- config_source: env(OPENCODE_*)"); - expect(report).toContain("- balance_usd: $42.50"); - expect(report).toContain("- monthly_limit_usd: $50.00"); - expect(report).toContain("- last_payment_usd: $20.00"); - expect(report).not.toContain("wrk-secret"); + expect(report).toContain("- console_auth_state: configured"); + expect(report).toContain("- console_server: https://opencode.ai/console"); + expect(report).toContain("- console_org: Acme"); + expect(report).toContain("- balance_usd: USD 42.5"); + expect(report).toContain("- monthly_limit_usd: USD 50"); + expect(report).toContain("- monthly_usage_usd: USD 11.82"); + expect(report).toContain("- budget_source: org_budget"); + expect(report).not.toContain("st-secret-token"); }); it("does not retry a failed OpenCode Zen live probe", async () => { @@ -1234,8 +1238,8 @@ describe("buildQuotaStatusReport", () => { entries: [], errors: [{ label: "OpenCode", message: "Request timeout after 10s" }], statusDetails: makeStatusDetails({ - config_state: "configured", - config_source: "env(OPENCODE_*)", + console_auth_state: "configured", + console_server: "https://opencode.ai/console", }), }, }, @@ -1245,21 +1249,17 @@ describe("buildQuotaStatusReport", () => { expect(report).toContain("- live_probe: error"); }); - it("reports a fixed OpenCode Zen parse error without attempting a live fetch", async () => { + it("reports a missing OpenCode Console sign-in without attempting a live fetch", async () => { const report = await buildOpenCodeZenStatusReport({ providerLiveProbes: [ makeProviderProbe("opencode", { - statusDetails: makeStatusDetails({ - config_state: "invalid", - config_error: "Failed to parse JSON", - }), + statusDetails: makeStatusDetails({ console_auth_state: "none" }), }), ], }); expect(report).toContain("opencode_zen:"); - expect(report).toContain("- config_state: invalid"); - expect(report).toContain("- config_error: Failed to parse JSON"); + expect(report).toContain("- console_auth_state: none"); }); it("reports safe Xiaomi config and partial live summaries without exposing cookie data", async () => { diff --git a/tests/lib.scoped-update-migration.test.ts b/tests/lib.scoped-update-migration.test.ts index c09bbb9a..99b46fe8 100644 --- a/tests/lib.scoped-update-migration.test.ts +++ b/tests/lib.scoped-update-migration.test.ts @@ -14,10 +14,10 @@ import { LEGACY_DISPLAY_MAPPINGS, OBSOLETE_GO_ENV_NAMES, OBSOLETE_GO_FILE, + OBSOLETE_ZEN_FILE, resolveScopedUpdateMigrationBoundary, type ScopedUpdateManualFinding, type ScopedUpdateSafeAction, - SUPPORTED_ZEN_FILE, sortScopedUpdateManualFindings, sortScopedUpdateSafeActions, } from "../src/lib/scoped-update-migration.js"; @@ -468,7 +468,7 @@ describe("obsolete source audit", () => { expect(OBSOLETE_GO_ENV_NAMES).toEqual(["OPENCODE_GO_WORKSPACE_ID", "OPENCODE_GO_AUTH_COOKIE"]); expect(AMBIGUOUS_ZEN_ENV_NAMES).toEqual(["OPENCODE_WORKSPACE_ID", "OPENCODE_AUTH_COOKIE"]); expect(OBSOLETE_GO_FILE).toBe("opencode-quota/opencode-go.json"); - expect(SUPPORTED_ZEN_FILE).toBe("opencode-quota/opencode.json"); + expect(OBSOLETE_ZEN_FILE).toBe("opencode-quota/opencode.json"); }); it.each([ @@ -529,31 +529,24 @@ describe("obsolete source audit", () => { configDir: root, }); - expect(findings).toEqual([ - { - kind: "ambiguous-zen-env", - names, - suggestedPath: join(root, SUPPORTED_ZEN_FILE), - }, - ]); + expect(findings).toEqual([{ kind: "ambiguous-zen-env", names }]); expect(JSON.stringify(findings)).not.toContain("zen-secret-canary"); }); it.each([ - "malformed", - "incomplete", + "file", "symlink", - ])("suppresses ambiguous Zen findings when the supported file path exists: %s", async (kind) => { + ])("reports the obsolete Zen %s and still reports ambiguous Zen names", async (kind) => { const root = tempDir(); const configDir = join(root, "config"); - const supportedPath = join(configDir, SUPPORTED_ZEN_FILE); + const obsoletePath = join(configDir, OBSOLETE_ZEN_FILE); if (kind === "symlink") { const target = join(root, "target.json"); - write(target, "supported-file-secret-canary"); - mkdirSync(dirname(supportedPath), { recursive: true }); - symlinkSync(target, supportedPath); + write(target, "zen-file-secret-canary"); + mkdirSync(dirname(obsoletePath), { recursive: true }); + symlinkSync(target, obsoletePath); } else { - write(supportedPath, kind === "malformed" ? "{" : "{}"); + write(obsoletePath, "zen-file-secret-canary"); } const findings = await auditObsoleteUpdateSources({ @@ -564,7 +557,11 @@ describe("obsolete source audit", () => { configDir, }); - expect(findings).toEqual([]); + expect(findings).toEqual([ + { kind: "ambiguous-zen-env", names: ["OPENCODE_WORKSPACE_ID", "OPENCODE_AUTH_COOKIE"] }, + { kind: "obsolete-zen-file", path: obsoletePath }, + ]); + expect(JSON.stringify(findings)).not.toMatch(/secret/u); }); }); diff --git a/tests/lib.scoped-update.test.ts b/tests/lib.scoped-update.test.ts index 17b3c8ca..bd9151f8 100644 --- a/tests/lib.scoped-update.test.ts +++ b/tests/lib.scoped-update.test.ts @@ -557,10 +557,7 @@ describe("scoped update config planning", () => { expect.arrayContaining([ { kind: "obsolete-go-env", name: "OPENCODE_GO_AUTH_COOKIE" }, { kind: "obsolete-go-file", path: obsoleteFile }, - expect.objectContaining({ - kind: "ambiguous-zen-env", - names: ["OPENCODE_WORKSPACE_ID"], - }), + { kind: "ambiguous-zen-env", names: ["OPENCODE_WORKSPACE_ID"] }, ]), ); expect(JSON.stringify(plan.manualFindings)).not.toContain("secret-canary"); @@ -571,7 +568,7 @@ describe("scoped update config planning", () => { const config = join(f.project, "opencode.json"); const malformedSidecar = join(f.project, "opencode-quota", "quota-toast.jsonc"); const obsoleteGoFile = join(f.global, "opencode-quota", "opencode-go.json"); - const supportedZenFile = join(f.global, "opencode-quota", "opencode.json"); + const obsoleteZenFile = join(f.global, "opencode-quota", "opencode.json"); const authFile = join(f.global, "auth.json"); const canaries = [ "go-workspace-value-canary", @@ -579,7 +576,7 @@ describe("scoped update config planning", () => { "zen-workspace-value-canary", "zen-cookie-value-canary", "legacy-go-file-content-canary", - "supported-zen-file-content-canary", + "obsolete-zen-file-content-canary", "provider-api-key-canary", "auth-json-key-canary", "invalid-display-value-canary", @@ -592,8 +589,8 @@ describe("scoped update config planning", () => { write(malformedSidecar, `{"opencodeZenDisplay":"migration-parser-content-canary",`); write(obsoleteGoFile, `{"authCookie":"legacy-go-file-content-canary"}`); write( - supportedZenFile, - `{"workspaceId":"supported-zen-file-content-canary","consoleSessionCookie":"supported-zen-file-content-canary"}`, + obsoleteZenFile, + `{"workspaceId":"obsolete-zen-file-content-canary","consoleSessionCookie":"obsolete-zen-file-content-canary"}`, ); write(authFile, `{"opencode-go":{"type":"api","key":"auth-json-key-canary"}}`); const env = { @@ -621,6 +618,12 @@ describe("scoped update config planning", () => { logs: log.mock.calls, }); for (const canary of canaries) expect(publicSuccess).not.toContain(canary); + expect(plan.manualFindings).toContainEqual({ + kind: "obsolete-zen-file", + path: obsoleteZenFile, + }); + expect(preview.join("\n")).toContain("run opencode auth login opencode"); + expect(preview.join("\n")).not.toContain("create and protect"); const failed = fixture(); write( diff --git a/tests/providers.cache-policies.test.ts b/tests/providers.cache-policies.test.ts index e0a4223a..a1fd8e5b 100644 --- a/tests/providers.cache-policies.test.ts +++ b/tests/providers.cache-policies.test.ts @@ -4,6 +4,7 @@ const cachePolicyMocks = vi.hoisted(() => ({ resolveGlobal: vi.fn(), resolveCn: vi.fn(), deriveIdentity: vi.fn(async (params: unknown) => JSON.stringify(params)), + resolveConsoleAuth: vi.fn(), })); vi.mock("../src/lib/kimi-auth.js", () => ({ @@ -16,6 +17,11 @@ vi.mock("../src/lib/kimi-auth.js", () => ({ resolveKimiCnAuth: vi.fn(), })); +vi.mock("../src/lib/opencode-console-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + resolveOpenCodeConsoleAuth: cachePolicyMocks.resolveConsoleAuth, +})); + vi.mock("../src/lib/resolved-auth-identity.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -104,6 +110,77 @@ describe("provider cache policies", () => { expect(cachePolicyMocks.resolveCn).toHaveBeenCalledTimes(2); }); + it("derives distinct OpenCode Zen identities for the same org on different Console URLs", async () => { + const policy = PROVIDER_CACHE_POLICIES.opencode; + if (policy.kind !== "resolved-auth") throw new Error("Expected a resolved-auth policy"); + + const ctx = { config: {} } as never; + const cacheContext = {} as never; + cachePolicyMocks.resolveConsoleAuth.mockResolvedValue({ + state: "configured", + credential: { accessToken: "st_secret-token", orgId: "wrk_shared", accountId: "acc_1" }, + }); + const cloudIdentity = await policy.resolveIdentity(ctx, cacheContext); + + cachePolicyMocks.resolveConsoleAuth.mockResolvedValue({ + state: "configured", + credential: { + accessToken: "st_self-hosted-token", + orgId: "wrk_shared", + accountId: "acc_1", + server: "https://console.self-hosted.example", + }, + }); + const selfHostedIdentity = await policy.resolveIdentity(ctx, cacheContext); + + expect(cloudIdentity).not.toBeNull(); + expect(selfHostedIdentity).not.toBeNull(); + expect(cloudIdentity).not.toBe(selfHostedIdentity); + expect(cachePolicyMocks.deriveIdentity).toHaveBeenCalledWith({ + providerId: "opencode", + principal: { kind: "stable-id", value: "wrk_shared" }, + qualifiers: ["https://opencode.ai/console"], + }); + expect(cachePolicyMocks.deriveIdentity).toHaveBeenCalledWith({ + providerId: "opencode", + principal: { kind: "stable-id", value: "wrk_shared" }, + qualifiers: ["https://console.self-hosted.example"], + }); + }); + + it.each([ + [ + { accessToken: "st_secret-token", accountId: "acc_1" }, + { kind: "stable-id", value: "acc_1" }, + ], + [{ accessToken: "st_secret-token" }, { kind: "credential", value: "st_secret-token" }], + ])("uses the account, then the token, when the Console sign-in has no org (%j)", async (credential, principal) => { + const policy = PROVIDER_CACHE_POLICIES.opencode; + if (policy.kind !== "resolved-auth") throw new Error("Expected a resolved-auth policy"); + + cachePolicyMocks.resolveConsoleAuth.mockResolvedValue({ state: "configured", credential }); + await policy.resolveIdentity({ config: {} } as never, {} as never); + + expect(cachePolicyMocks.deriveIdentity).toHaveBeenCalledWith({ + providerId: "opencode", + principal, + qualifiers: ["https://opencode.ai/console"], + }); + }); + + it.each([ + { state: "none" }, + { state: "expired", credential: { accessToken: "st_secret-token", orgId: "wrk_shared" } }, + { state: "invalid", error: "refresh_failed: boom" }, + ])("returns no OpenCode Zen identity without a configured Console sign-in (%j)", async (resolution) => { + const policy = PROVIDER_CACHE_POLICIES.opencode; + if (policy.kind !== "resolved-auth") throw new Error("Expected a resolved-auth policy"); + + cachePolicyMocks.resolveConsoleAuth.mockResolvedValue(resolution); + await expect(policy.resolveIdentity({ config: {} } as never, {} as never)).resolves.toBeNull(); + expect(cachePolicyMocks.deriveIdentity).not.toHaveBeenCalled(); + }); + it("attaches the exhaustive policy to the stable provider singleton", () => { const first = getProviders(); const second = getProviders(); diff --git a/tests/providers.opencode-zen.surfaces.test.ts b/tests/providers.opencode-zen.surfaces.test.ts index dec8172a..eedd8dbb 100644 --- a/tests/providers.opencode-zen.surfaces.test.ts +++ b/tests/providers.opencode-zen.surfaces.test.ts @@ -5,14 +5,14 @@ import { renderAccountingFourSurfaces } from "./helpers/accounting-four-surface. const budgetAccounting = { resultType: "budget", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "locally_derived", } as const; const providerAccounting = { resultType: "balance", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "provider_reported", } as const; @@ -62,6 +62,18 @@ const primaryBalance: QuotaToastEntry = { }, }; +const monthlySpend: QuotaToastEntry = { + kind: "quantity", + accounting: { ...providerAccounting, resultType: "spend" }, + name: "zen-monthly-spend", + group: "OpenCode Zen", + semantic: { + metric: { kind: "window", window: "month" }, + prominence: "primary", + }, + quantity: { decimal: "11.8202", unit: { kind: "currency", code: "USD" } }, +}; + const autoReload: QuotaToastEntry = { kind: "boolean", accounting: { ...providerAccounting, resultType: "status" }, @@ -114,6 +126,26 @@ describe("OpenCode Zen structured four-surface formatting", () => { expect(primaryBalance).not.toHaveProperty("value"); }); + it("renders current-month spend next to the balance when there is no monthly limit", () => { + const outputs = renderAccountingFourSurfaces({ + data: { entries: [monthlySpend, primaryBalance], errors: [] }, + accountingDetail: "summary", + toastMaxWidth: 50, + toastNarrowAt: 42, + compactMaxWidth: 200, + }); + + for (const output of Object.values(outputs)) { + expect(output).toContain("OpenCode Zen"); + expect(output).toContain("Monthly spend"); + expect(output).toContain("USD 11.82"); + } + for (const output of [outputs.command, outputs.toast, outputs.sidebar]) { + expect(output).toContain("Current balance"); + expect(output).toContain("USD 42.50"); + } + }); + it("renders detailed basis and supplementary values within each surface width", () => { const outputs = renderAccountingFourSurfaces({ data: { entries: [budget, balance, autoReload], errors: [] }, diff --git a/tests/providers.opencode-zen.test.ts b/tests/providers.opencode-zen.test.ts index 598ac65b..21f4656b 100644 --- a/tests/providers.opencode-zen.test.ts +++ b/tests/providers.opencode-zen.test.ts @@ -8,34 +8,42 @@ import { const mocks = vi.hoisted(() => ({ queryOpenCodeZenQuota: vi.fn(), - resolveOpenCodeZenConfigCached: vi.fn(), + resolveOpenCodeConsoleAuth: vi.fn(), + fetchResponse: vi.fn(), + realQueryOpenCodeZenQuota: null as + | null + | typeof import("../src/lib/opencode-zen.js").queryOpenCodeZenQuota, +})); + +vi.mock("../src/lib/http.js", () => ({ + fetchWithTimeout: async ( + url: string, + options: { consume: (response: Response) => Promise }, + ) => { + const response = await mocks.fetchResponse(url); + return await options.consume(response); + }, })); vi.mock("../src/lib/opencode-zen.js", async (importOriginal) => { const original = await importOriginal(); + mocks.realQueryOpenCodeZenQuota = original.queryOpenCodeZenQuota; return { ...original, queryOpenCodeZenQuota: mocks.queryOpenCodeZenQuota, }; }); -vi.mock("../src/lib/opencode-zen-config.js", () => ({ - DEFAULT_OPENCODE_ZEN_CONFIG_CACHE_MAX_AGE_MS: 30_000, - resolveOpenCodeZenConfigCached: mocks.resolveOpenCodeZenConfigCached, - getOpenCodeZenConfigDiagnostics: vi.fn(async () => ({ - state: "configured", - source: "test", - missing: null, - error: null, - checkedPaths: [], - })), +vi.mock("../src/lib/opencode-console-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + resolveOpenCodeConsoleAuth: mocks.resolveOpenCodeConsoleAuth, })); import { opencodeZenProvider } from "../src/providers/opencode-zen.js"; const balanceAccounting = { resultType: "balance", - acquisitionMethod: "dashboard_scrape", + acquisitionMethod: "remote_api", ownership: "maintained", authority: "provider_reported", } as const; @@ -49,6 +57,12 @@ const statusAccounting = { resultType: "status", } as const; +const consoleCredential = { + accessToken: "st_secret-token", + orgId: "wrk_123", + orgName: "Acme", +}; + function balanceEntry(prominence: "primary" | "supplementary") { return { accounting: balanceAccounting, @@ -84,6 +98,7 @@ function budgetEntry( limit?: string; remaining?: string; limitAuthority?: "provider_reported" | "user_configured"; + resetTimeIso?: string; } = {}, ) { return { @@ -91,6 +106,7 @@ function budgetEntry( name: "zen-monthly-budget", group: "OpenCode Zen", percentRemaining: options.percentRemaining ?? 94.25, + ...(options.resetTimeIso ? { resetTimeIso: options.resetTimeIso } : {}), semantic: { metric: { kind: "window", window: "month" }, prominence: "primary", @@ -121,12 +137,10 @@ function budgetEntry( } as const; } -function configured(): void { - mocks.resolveOpenCodeZenConfigCached.mockResolvedValueOnce({ - state: "configured", - config: { workspaceId: "wrk_123", consoleSessionCookie: "cookie-abc" }, - source: "env(OPENCODE_*)", - }); +const LOGIN_HINT = "Run `opencode auth login opencode`."; + +function configured(credential: Record = consoleCredential): void { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ state: "configured", credential }); } function success(overrides: Record = {}, errors: string[] = []): void { @@ -141,6 +155,8 @@ function success(overrides: Record = {}, errors: string[] = []) reload: false, reloadAmount: null, reloadTrigger: null, + budgetResetIso: null, + budgetSource: "credit_limit", ...overrides, }, }); @@ -160,20 +176,22 @@ describe("opencode Zen provider", () => { }); it.each([ - [ - { - state: "configured", - config: { workspaceId: "wrk", consoleSessionCookie: "cookie" }, - source: "env(OPENCODE_*)", - }, - true, - ], - [{ state: "incomplete", source: "env", missing: "consoleSessionCookie" }, false], - [{ state: "invalid", source: "/tmp/opencode.json", error: "broken" }, false], - [{ state: "none" }, false], - ])("reports availability for config state %j", async (configState, expected) => { - mocks.resolveOpenCodeZenConfigCached.mockResolvedValueOnce(configState); - await expect(opencodeZenProvider.isAvailable(context())).resolves.toBe(expected); + [{ state: "configured", credential: consoleCredential }, "auto", true], + [{ state: "configured", credential: consoleCredential }, ["opencode"], true], + [{ state: "expired", credential: consoleCredential }, "auto", true], + [{ state: "expired", credential: consoleCredential }, ["opencode"], true], + [{ state: "invalid", error: "refresh_failed: boom" }, "auto", true], + [{ state: "invalid", error: "refresh_failed: boom" }, ["opencode"], true], + [{ state: "none" }, "auto", false], + [{ state: "none" }, ["opencode"], true], + [{ state: "none" }, ["openai"], false], + [{ state: "none", reason: "not_oauth" }, "auto", false], + [{ state: "none", reason: "not_oauth" }, ["opencode"], true], + ])("reports availability for %j with enabledProviders %j -> %j", async (resolution, enabledProviders, expected) => { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce(resolution); + await expect(opencodeZenProvider.isAvailable(context({ enabledProviders }))).resolves.toBe( + expected, + ); }); it.each([ @@ -186,46 +204,100 @@ describe("opencode Zen provider", () => { expect(opencodeZenProvider.matchesCurrentModel?.(model)).toBe(expected); }); - it("returns attempted:false when configuration is absent", async () => { - mocks.resolveOpenCodeZenConfigCached.mockResolvedValueOnce({ state: "none" }); - expectNotAttempted(await opencodeZenProvider.fetch(context())); + it.each([ + { state: "none" }, + { state: "none", reason: "not_oauth" }, + ])("returns attempted:false in auto mode without a Console sign-in (%j)", async (resolution) => { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce(resolution); + + const result = await opencodeZenProvider.fetch(context({ enabledProviders: "auto" })); + + expectNotAttempted(result); + expect(result.statusDetails).toEqual([{ key: "console_auth_state", value: "none" }]); expect(mocks.queryOpenCodeZenQuota).not.toHaveBeenCalled(); }); - it.each([ - [ - { state: "incomplete", source: "env(OPENCODE_*)", missing: "OPENCODE_AUTH_COOKIE" }, - "Missing OPENCODE_AUTH_COOKIE", - ], - [{ state: "invalid", source: "/tmp/opencode.json", error: "bad JSON" }, "Invalid config"], - [ + it("shows the sign-in hint when Zen is explicitly enabled without a Console sign-in", async () => { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ state: "none", reason: "not_oauth" }); + + const result = await opencodeZenProvider.fetch(context({ enabledProviders: ["opencode"] })); + + expectAttemptedWithErrorLabel(result, "OpenCode"); + expect(result.errors).toEqual([ + { label: "OpenCode", message: `No OpenCode Console sign-in found. ${LOGIN_HINT}` }, + ]); + expect(result.statusDetails).toEqual([{ key: "console_auth_state", value: "none" }]); + expect(mocks.queryOpenCodeZenQuota).not.toHaveBeenCalled(); + }); + + it("shows a failed Console sign-in as an attempted error with the scrubbed reason", async () => { + const leakedToken = "a".repeat(40); + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "invalid", + error: `refresh_failed: token ${leakedToken} was rejected`, + }); + + const result = await opencodeZenProvider.fetch(context()); + + expectAttemptedWithErrorLabel(result, "OpenCode"); + expect(result.errors).toEqual([ { - state: "invalid", - source: "/tmp/opencode.json", - error: - "authCookie no longer works after the OpenCode Console redesign; paste the __Host-console_session cookie as consoleSessionCookie", + label: "OpenCode", + message: `OpenCode Console sign-in failed: refresh_failed: token [redacted] was rejected. ${LOGIN_HINT}`, }, - "paste the __Host-console_session cookie as consoleSessionCookie", - ], - ])("projects config state as an attempted error", async (configState, message) => { - mocks.resolveOpenCodeZenConfigCached.mockResolvedValueOnce(configState); + ]); + expect(result.statusDetails).toEqual([{ key: "console_auth_state", value: "invalid" }]); + expect(JSON.stringify(result)).not.toContain(leakedToken); + expect(mocks.queryOpenCodeZenQuota).not.toHaveBeenCalled(); + }); + + it("shows an expired Console sign-in as an attempted error", async () => { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "expired", + credential: consoleCredential, + }); + const result = await opencodeZenProvider.fetch(context()); expectAttemptedWithErrorLabel(result, "OpenCode"); - expect(result.errors[0]?.message).toContain(message); + expect(result.errors).toEqual([ + { + label: "OpenCode", + message: `OpenCode Console sign-in failed: the sign-in expired. ${LOGIN_HINT}`, + }, + ]); + expect(result.statusDetails).toEqual([{ key: "console_auth_state", value: "expired" }]); + expect(JSON.stringify(result)).not.toContain("st_secret-token"); expect(mocks.queryOpenCodeZenQuota).not.toHaveBeenCalled(); }); it("projects Console failures as attempted errors", async () => { - const sessionError = - "OpenCode Console session expired or invalid — paste a fresh __Host-console_session cookie as consoleSessionCookie"; configured(); - mocks.queryOpenCodeZenQuota.mockResolvedValueOnce({ success: false, error: sessionError }); + mocks.queryOpenCodeZenQuota.mockResolvedValueOnce({ + success: false, + error: + "OpenCode Console session expired or invalid. Run `opencode auth login opencode` to sign in again.", + }); const result = await opencodeZenProvider.fetch(context()); expectAttemptedWithErrorLabel(result, "OpenCode"); - expect(result.errors[0]?.message).toBe(sessionError); + expect(result.errors[0]?.message).toContain("opencode auth login opencode"); + }); + + it("reports the Console server and org but never the token", async () => { + configured({ accessToken: "st_secret-token", server: "https://console.self-hosted.example" }); + success(); + + const result = await opencodeZenProvider.fetch(context()); + + expectAttemptedWithNoErrors(result); + expect(result.statusDetails.slice(0, 3)).toEqual([ + { key: "console_auth_state", value: "configured" }, + { key: "console_server", value: "https://console.self-hosted.example" }, + { key: "console_org", value: "(none)" }, + ]); + expect(JSON.stringify(result)).not.toContain("st_secret-token"); }); it("makes structured balance primary when no monthly budget is available", async () => { @@ -239,6 +311,34 @@ describe("opencode Zen provider", () => { expect(result.presentation).toBeUndefined(); }); + it("shows current-month spend next to the balance when there is no monthly limit", async () => { + configured(); + success({ monthlyLimit: null, monthlyUsage: 1_182_020_000 }); + + const result = await opencodeZenProvider.fetch(context()); + + expectAttemptedWithNoErrors(result); + expect(result.entries).toEqual([ + { + accounting: { ...balanceAccounting, resultType: "spend" }, + kind: "quantity", + name: "zen-monthly-spend", + group: "OpenCode Zen", + semantic: { + metric: { kind: "window", window: "month" }, + prominence: "primary", + }, + quantity: { decimal: "11.8202", unit: { kind: "currency", code: "USD" } }, + }, + balanceEntry("primary"), + autoReloadEntry(), + ]); + expect(result.statusDetails).toContainEqual({ + key: "monthly_usage_usd", + value: "USD 11.8202", + }); + }); + it("calculates monthly-limit remaining from monthly usage (default display)", async () => { configured(); success({ monthlyLimit: 100, monthlyUsage: 575_000_000 }); @@ -252,18 +352,30 @@ describe("opencode Zen provider", () => { autoReloadEntry(), ]); expect(result.statusDetails).toEqual([ - { key: "config_state", value: "configured" }, - { key: "config_source", value: "test" }, - { key: "config_checked_paths", value: "(none)" }, + { key: "console_auth_state", value: "configured" }, + { key: "console_server", value: "https://opencode.ai/console" }, + { key: "console_org", value: "Acme" }, { key: "balance_usd", value: "USD 42.5" }, { key: "monthly_limit_usd", value: "USD 100" }, - { key: "last_payment_usd", value: "(none)" }, + { key: "monthly_usage_usd", value: "USD 5.75" }, + { key: "budget_source", value: "credit_limit" }, { key: "auto_reload", value: "false" }, { key: "auto_reload_amount_raw", value: "(none)" }, { key: "auto_reload_trigger_raw", value: "(none)" }, ]); }); + it("does not project a last payment status detail", async () => { + configured(); + success({ lastPayment: 50 }); + + const result = await opencodeZenProvider.fetch(context()); + + expectAttemptedWithNoErrors(result); + expect(result.statusDetails.some((d) => d.key === "last_payment_usd")).toBe(false); + expect(JSON.stringify(result)).not.toContain("last_payment_usd"); + }); + it("emits only the contract-backed reload boolean and keeps ambiguous values diagnostic", async () => { configured(); success({ @@ -295,6 +407,82 @@ describe("opencode Zen provider", () => { expect(result.presentation).toBeUndefined(); }); + it("attaches the org budget reset date to the monthly budget entry", async () => { + configured(); + success({ + monthlyLimit: 60, + monthlyUsage: 617_355_570, + budgetResetIso: "2026-10-01T00:00:00.000Z", + budgetSource: "org_budget", + }); + + const result = await opencodeZenProvider.fetch(context()); + + expectAttemptedWithNoErrors(result); + expect(result.entries).toEqual([ + budgetEntry({ + percentRemaining: Math.min(100, ((60 - 6.1735557) / 60) * 100), + used: "6.1735557", + limit: "60", + remaining: "53.8264443", + resetTimeIso: "2026-10-01T00:00:00.000Z", + }), + balanceEntry("supplementary"), + autoReloadEntry(), + ]); + expect(result.statusDetails).toContainEqual({ key: "budget_source", value: "org_budget" }); + }); + + it("does not reject the whole result for a parseable non-ISO org-budget reset", async () => { + configured(); + // Run the real query pipeline against a fake HTTP layer where budgets/org + // returns a parseable-but-non-ISO reset ("0"); the resolver must normalize + // it to canonical ISO so shared result validation never drops the result. + mocks.fetchResponse.mockImplementation((url: string) => { + const route = url.slice("https://opencode.ai/console/api/".length); + const payloads: Record = { + "billing/status": { balanceMicroCents: "4250000000" }, + "billing/account": { orgId: "wrk_123", creditLimitMicroCents: null }, + "billing/auto-recharge": { + enabled: false, + thresholdDollars: 5, + rechargeAmountDollars: 20, + pending: false, + failureReason: null, + }, + "budgets/org": { + limitMicroCents: "6000000000", + spentMicroCents: "617355570", + exceeded: false, + resetsAt: "2026-10-01T00:00:00", + }, + "usage/cost-by-day": [], + }; + const payload = payloads[route]; + if (payload === undefined) throw new Error(`unexpected url ${url}`); + return new Response(JSON.stringify(payload), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }); + if (!mocks.realQueryOpenCodeZenQuota) throw new Error("real query not captured"); + const realQuery = mocks.realQueryOpenCodeZenQuota; + mocks.queryOpenCodeZenQuota.mockImplementation( + (credential: typeof consoleCredential, opts?: { requestTimeoutMs?: number }) => + realQuery(credential, opts), + ); + + const result = await opencodeZenProvider.fetch(context()); + + expectAttemptedWithNoErrors(result); + expect(result.entries[0]).toMatchObject({ + percentRemaining: Math.min(100, ((60 - 6.1735557) / 60) * 100), + resetTimeIso: new Date("2026-10-01T00:00:00").toISOString(), + }); + // Canonical ISO only: the parseable non-ISO input must never surface. + expect(result.entries[0].resetTimeIso?.endsWith("Z")).toBe(true); + }); + it("prefers the positive plugin monthly-limit override", async () => { configured(); success({ monthlyLimit: 100, monthlyUsage: 575_000_000 }); @@ -409,7 +597,7 @@ describe("opencode Zen provider", () => { await opencodeZenProvider.fetch( context({ requestTimeoutMs: 7_654, requestTimeoutMsConfigured: true }), ); - expect(mocks.queryOpenCodeZenQuota).toHaveBeenLastCalledWith("wrk_123", "cookie-abc", { + expect(mocks.queryOpenCodeZenQuota).toHaveBeenLastCalledWith(consoleCredential, { requestTimeoutMs: 7_654, }); @@ -418,7 +606,7 @@ describe("opencode Zen provider", () => { await opencodeZenProvider.fetch( context({ requestTimeoutMs: 5_000, requestTimeoutMsConfigured: false }), ); - expect(mocks.queryOpenCodeZenQuota).toHaveBeenLastCalledWith("wrk_123", "cookie-abc", { + expect(mocks.queryOpenCodeZenQuota).toHaveBeenLastCalledWith(consoleCredential, { requestTimeoutMs: undefined, }); }); diff --git a/tests/quota-render-data.test.ts b/tests/quota-render-data.test.ts index 4622fa2c..0f909222 100644 --- a/tests/quota-render-data.test.ts +++ b/tests/quota-render-data.test.ts @@ -18,6 +18,15 @@ vi.mock("../src/providers/registry.js", () => ({ getProviders: () => mockProviders, })); +const consoleAuthMocks = vi.hoisted(() => ({ + resolveOpenCodeConsoleAuth: vi.fn(), +})); + +vi.mock("../src/lib/opencode-console-auth.js", async (importOriginal) => ({ + ...(await importOriginal()), + resolveOpenCodeConsoleAuth: consoleAuthMocks.resolveOpenCodeConsoleAuth, +})); + vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ getOpencodeRuntimeDirs: () => ({ dataDir: `${TEST_RUNTIME_ROOT}/data`, @@ -34,6 +43,7 @@ import { } from "../src/lib/quota-render-data.js"; import { __resetQuotaStateForTests } from "../src/lib/quota-state.js"; import { DEFAULT_CONFIG, type QuotaToastConfig } from "../src/lib/types.js"; +import { opencodeZenProvider } from "../src/providers/opencode-zen.js"; function renderConfig(overrides: Partial = {}): QuotaToastConfig { return { ...DEFAULT_CONFIG, showSessionTokens: false, ...overrides }; @@ -112,6 +122,56 @@ describe("collectQuotaRenderData shared quota state", () => { ]); }); + it("surfaces a failed OpenCode Zen sign-in but keeps auto mode quiet without one", async () => { + // A failed Console sign-in passes the auto-mode availability gate so + // fetch() can show its recovery hint. + consoleAuthMocks.resolveOpenCodeConsoleAuth.mockResolvedValue({ + state: "invalid", + error: "refresh_failed: boom", + }); + + const failed = await collectQuotaRenderData({ + client: TEST_CLIENT, + config: renderConfig(), + surfaceExplicitProviderIssues: true, + formatStyle: "allWindows", + providers: [opencodeZenProvider], + }); + expect(failed.active).toEqual([opencodeZenProvider]); + expect(failed.data?.errors).toContainEqual({ + label: "OpenCode", + message: + "OpenCode Console sign-in failed: refresh_failed: boom. Run `opencode auth login opencode`.", + }); + + // Without a Console sign-in, auto mode stays silent, but an explicit + // opencode entry keeps Zen actionable. + consoleAuthMocks.resolveOpenCodeConsoleAuth.mockResolvedValue({ state: "none" }); + + const autoNone = await collectQuotaRenderData({ + client: TEST_CLIENT, + config: renderConfig(), + surfaceExplicitProviderIssues: true, + formatStyle: "allWindows", + providers: [opencodeZenProvider], + }); + expect(autoNone.active).toEqual([]); + expect(autoNone.data?.errors ?? []).toEqual([]); + + const explicitNone = await collectQuotaRenderData({ + client: TEST_CLIENT, + config: renderConfig({ enabledProviders: ["opencode"] }), + surfaceExplicitProviderIssues: true, + formatStyle: "allWindows", + providers: [opencodeZenProvider], + }); + expect(explicitNone.active).toEqual([opencodeZenProvider]); + expect(explicitNone.data?.errors).toContainEqual({ + label: "OpenCode", + message: "No OpenCode Console sign-in found. Run `opencode auth login opencode`.", + }); + }); + it("returns allWindowsData when includeAllWindowsData is true and style is singleWindow", async () => { const provider = testProvider("test-provider", { entries: [ From 940a720f5be24dd908b85e33cb49a77f49b276f5 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:06:53 +0200 Subject: [PATCH 72/97] feat: find the Claude CLI in usual install folders and report the login source - Claude: when anthropicBinaryPath is not set and claude is not on the service PATH, try ~/.claude/local/claude, ~/.local/bin/claude, /opt/homebrew/bin/claude and /usr/local/bin/claude in order (not on Windows). The CLI that ran is reported as binary_path. - /quota_status: new credential_source section (source, list_error, failures); the opencode.db row says it is for session and token history; the opencode_go section now shows go_source, console_error, console_auth_state and console_server. - OpenCode Go: when the Console sign-in or request fails and there is no API key, show one Go error row instead of nothing. - Docs: logins come through OpenCode 2's plugin API, the service environment note (PATH, env, home folder), Claude install folders, sign-in refresh failures, Alibaba bl outside the home folder, CONTRIBUTING guardrails and the provider template. Co-authored-by: Ran Sagy --- CONTRIBUTING.md | 8 +- README.md | 5 +- contributing/provider-template/README.md | 4 +- docs/readme/configuration.md | 18 ++-- docs/readme/external-integration.md | 2 +- docs/readme/providers.md | 22 ++-- docs/readme/troubleshooting.md | 29 ++++-- docs/readme/updating.md | 9 +- src/lib/anthropic.ts | 70 +++++++++++-- src/lib/quota-status.ts | 29 +++++- src/providers/anthropic.ts | 1 + src/providers/opencode-go.ts | 46 +++++++-- tests/lib.anthropic.test.ts | 106 +++++++++++++++++++- tests/lib.quota-status.test.ts | 97 +++++++++++++++++- tests/provider-surfaces.opencode-go.test.ts | 42 ++++++++ tests/providers.anthropic.test.ts | 9 +- tests/providers.opencode-go.test.ts | 78 ++++++++++++++ 17 files changed, 510 insertions(+), 65 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2813dd3..3c4e03ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,10 +79,10 @@ Recommended settings for `main`: - Never invoke an LLM/model API to compute toast/report output. Everything must remain local and deterministic. - Rich accounting currency quantities must preserve the provider's uppercase ISO code and render through the shared formatter (for example, `USD 12.50`), never a provider-formatted currency string, bare symbol, decorative glyph, conversion, or cross-currency sum. - OpenCode Quota supports only OpenCode 2. Do not add OpenCode 1 code paths, plugin APIs, or database tables. -- The TUI plugin (`src/tui-v2.tsx`) owns every visible surface: slash commands as local TUI dialogs, toasts, the Sidebar panel, the compact line and prompt bar below the prompt, and the Home footer line. Commands never write to the session transcript or call a model. -- The server plugin (`src/plugin.ts`) only registers the `quota_status` tool, a diagnostics tool the assistant can call. OpenCode 2 gives plugins no Web or Desktop UI hooks, so there are no Web slash commands or toasts. -- Slash commands (`/quota`, `/quota_status`, `/quota_announcements`, `/pricing_refresh`, `/tokens_*`) and the `quota_status` tool must route through `buildQuotaDialogCommandOutput()`; do not duplicate command-output logic in `src/plugin.ts` or `src/tui-v2.tsx`. -- Read OpenCode credentials only from the `credential` table in OpenCode 2's `opencode.db`, never from `auth.json`. +- The server plugin (`src/plugin.ts`) registers the `quota_status` tool, the slash commands, and the `slkiser.opencode-quota` RPC, and computes the text of every surface. OpenCode 2 gives plugins no Web or Desktop UI hooks, so Web and Desktop get slash commands but no toasts or panels. Commands never call a model. +- The TUI plugin (`src/tui-v2.tsx`) only renders: toasts, the Sidebar panel, the compact line and prompt bar below the prompt, the Home footer line, and report popups. It gets every text through the RPC and imports no provider or credential module (`tests/tui-dist-import-graph.test.ts`, `tests/plugin.command-handled-boundary.test.ts`). +- Slash commands (`/quota`, `/quota_status`, `/quota_announcements`, `/pricing_refresh`, `/tokens_*`), the `quota_status` tool, and the RPC `command` method must route through `buildQuotaDialogCommandOutput()`; do not duplicate command-output logic in `src/plugin.ts` or `src/tui-v2.tsx`. +- Read OpenCode credentials only through `ctx.integration`, inside `src/lib/opencode-auth.ts`, never from `auth.json` or the `credential` table. Every reader call names its integration ids. - Keep `tests/plugin.command-handled-boundary.test.ts` aligned with these invariants. Additional boundary tests to keep healthy when touching plugin/provider logic: diff --git a/README.md b/README.md index 998ea73b..3da5c69e 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,8 @@ The updater always shows a preview first. `--yes` applies only the safe edits it > [!WARNING] > - OpenCode 1 is no longer supported. `init` and `update` detect OpenCode 1 and keep you on 4.x (`@slkiser/opencode-quota@4`). -> - Logins come only from OpenCode 2's `opencode.db`, never `auth.json`. If a provider is missing, log in again. +> - Logins are read through OpenCode 2's plugin API (OpenCode keeps them in `opencode.db`), never from `auth.json`. If a provider is missing, log in again. +> - Quota is computed in OpenCode's background service, so `PATH` and environment variables come from the service, not your terminal. See [Service environment](docs/readme/troubleshooting.md#service-environment). > - Web and Desktop get the slash commands (report posts in the chat) but no toasts or panels. > - One `"plugin"` entry in `opencode.json` loads the server and the TUI; no `tui.json` entry is needed. > - TUI slash reports now open in a popup. Set `tuiCommandDisplay: "inline"` to keep them in the chat. @@ -248,7 +249,7 @@ The guided setup previews the change before saving. See the [custom-provider gui If quota or token data looks wrong: 1. Run `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal for the same diagnostics (OpenCode must be running). Use `opencode-quota show` for a quick quota glance. -2. Confirm the expected provider appears in the detected provider list. If it is missing, log in to it again in OpenCode 2. +2. Confirm the expected provider appears in the detected provider list. If it is missing, log in to it again in OpenCode 2. `credential_source` lists logins OpenCode could not return. 3. Confirm companion auth plugins are before `@slkiser/opencode-quota` in `opencode.json`. 4. If token reports are empty, start OpenCode once so it creates `opencode.db`, then run a session with model usage. 5. Check [Troubleshooting](docs/readme/troubleshooting.md) for common symptoms and provider-specific fixes. diff --git a/contributing/provider-template/README.md b/contributing/provider-template/README.md index 4ab04521..91dced50 100644 --- a/contributing/provider-template/README.md +++ b/contributing/provider-template/README.md @@ -12,11 +12,11 @@ This template is intentionally outside `src/` and is not published to npm. `pack Use this template for API-key/token providers that support all three trusted sources: -1. existing OpenCode API-key/token auth (`opencode.db`) +1. existing OpenCode API-key/token login, read through the plugin API 2. trusted user/global OpenCode config 3. environment variables -Do not use it for OAuth-only providers such as OpenAI. +Do not use it for OAuth-only providers such as OpenAI. `readAuth` must pass the provider's integration ids, as `config.ts` does: `readAuthFile({ integrationIds: PROVIDER_KEYS })`. ## Copy targets diff --git a/docs/readme/configuration.md b/docs/readme/configuration.md index 804a1af4..076af0fe 100644 --- a/docs/readme/configuration.md +++ b/docs/readme/configuration.md @@ -237,7 +237,7 @@ The command writes the `experimental.quotaToast.quotaProviders` section. Configu - Local estimates support 1–16 UTC-day or rolling request windows. - Automatic models.dev matching runs first. `pricingModelMap` cannot override a successful automatic match. - If any request cannot be priced, request counts stay visible and the budget percentage is reported unavailable. -- Credentials resolve from `apiKeyEnv`, trusted global `provider..options.apiKey`, then API-key entries in OpenCode `opencode.db`. +- Credentials resolve from `apiKeyEnv`, trusted global `provider..options.apiKey`, then API-key logins saved in OpenCode 2 (`opencode.db`). - Definitions run automatically with `enabledProviders: "auto"`. A manual list must include `quota-providers` and every built-in provider you still want. - To tune maintained estimates, use the reserved `alibaba-coding-plan` ID and its maintained window shape. Do not add a duplicate normal provider block. Alibaba Personal Token Plan uses the reserved `alibaba-token-plan` ID and is not a local-estimate tuning target. - Project secrets, scripts, custom headers, executable mappings, regular expressions, and JSONPath are not accepted. @@ -449,7 +449,7 @@ Existing `experimental.quotaToast` settings remain supported. | Option | Default | Meaning | | ---------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `anthropicBinaryPath` | `"claude"` | Command/path used for local Claude CLI probing. | +| `anthropicBinaryPath` | `"claude"` | Command/path used for local Claude CLI probing. With `"claude"`, OpenCode's `PATH` is tried first, then the usual install folders (not on Windows). | | `opencodeGoWindows` | `["rolling", "weekly", "monthly"]` | Choose which validated OpenCode Go API results are available across surfaces and in the expanded sidebar: Five-hour, Weekly, and Monthly. | | `opencodeMonthlyLimit` | unset | Override the OpenCode Zen monthly budget in USD. | | `cursorPlan` | `"none"` | Cursor included API budget preset: `none`, `pro`, `pro-plus`, `ultra`. | @@ -460,16 +460,16 @@ Every `provider..options.apiKey` below can also be written in OpenCode 2's n Kimi has no `quota-toast.json` credential setting. Each regional plan resolves its own key: -- **Kimi Code (Global):** `KIMI_GLOBAL_API_KEY` → trusted user/global `provider.kimi-code-plan-global.options.apiKey` → strict `kimi-code-plan-global` API-key entry in OpenCode `opencode.db`. -- **Kimi Code (CN):** `KIMI_CN_API_KEY` → `KIMI_API_KEY` → `KIMI_CODE_API_KEY` → trusted user/global config under `kimi-code-plan-cn`, `kimi-for-coding`, `kimi-code`, or `kimi` → strict API-key entry in `opencode.db` under those same ids. +- **Kimi Code (Global):** `KIMI_GLOBAL_API_KEY` → trusted user/global `provider.kimi-code-plan-global.options.apiKey` → `kimi-code-plan-global` API-key login saved in OpenCode 2. +- **Kimi Code (CN):** `KIMI_CN_API_KEY` → `KIMI_API_KEY` → `KIMI_CODE_API_KEY` → trusted user/global config under `kimi-code-plan-cn`, `kimi-for-coding`, `kimi-code`, or `kimi` → API-key login saved in OpenCode 2 under those same ids. Project-local `opencode.json` and `opencode.jsonc` are never read for these secrets. Global credentials are sent only to `api.kimi.ai`; CN and legacy credentials are sent only to `api.kimi.com`. A failed request is not retried against the other host. -Kilo Gateway has no `quota-toast.json` credential setting. Use `KILO_API_KEY`, trusted user/global `provider.kilo.options.apiKey`, or a strict `kilo` API-key entry in OpenCode `opencode.db`; project-local OpenCode config is not read for this secret. See [Kilo Gateway setup](providers.md#kilo-gateway). +Kilo Gateway has no `quota-toast.json` credential setting. Use `KILO_API_KEY`, trusted user/global `provider.kilo.options.apiKey`, or a `kilo` API-key login saved in OpenCode 2; project-local OpenCode config is not read for this secret. See [Kilo Gateway setup](providers.md#kilo-gateway). -Ollama Cloud has no `quota-toast.json` credential setting. Use `OLLAMA_API_KEY`, trusted user/global `provider.ollama-cloud.options.apiKey`, or a strict `ollama-cloud` API-key entry in OpenCode `opencode.db`; project-local OpenCode config is not read for this secret. See [Ollama Cloud setup](providers.md#ollama-cloud). +Ollama Cloud has no `quota-toast.json` credential setting. Use `OLLAMA_API_KEY`, trusted user/global `provider.ollama-cloud.options.apiKey`, or an `ollama-cloud` API-key login saved in OpenCode 2; project-local OpenCode config is not read for this secret. See [Ollama Cloud setup](providers.md#ollama-cloud). -OpenCode Go has no `quota-toast.json` workspace ID, cookie, endpoint, credential, or token setting. It automatically uses `OPENCODE_API_KEY`, trusted user/global `provider.opencode-go.options.apiKey`, trusted user/global fallback `provider.opencode.options.apiKey`, a strict `opencode-go` API-key entry in OpenCode `opencode.db`, or a strict legacy `opencode` auth entry as the final fallback. `opencodeGoWindows` filters the Five-hour, Weekly, and Monthly rows returned by the official usage API, including the expanded sidebar. Use `tuiSidebarPanel.opencodeGoPreferredWindow` to prefer one available row only while the sidebar is collapsed. See [OpenCode Go setup](providers.md#opencode-go). +OpenCode Go has no `quota-toast.json` workspace ID, cookie, endpoint, credential, or token setting. It automatically uses `OPENCODE_API_KEY`, trusted user/global `provider.opencode-go.options.apiKey`, trusted user/global fallback `provider.opencode.options.apiKey`, an `opencode-go` API-key login saved in OpenCode 2, or a legacy `opencode` API-key login as the final fallback. A Console sign-in (`opencode auth login opencode`) is tried before any key. `opencodeGoWindows` filters the Five-hour, Weekly, and Monthly rows returned by the official usage API, including the expanded sidebar. Use `tuiSidebarPanel.opencodeGoPreferredWindow` to prefer one available row only while the sidebar is collapsed. See [OpenCode Go setup](providers.md#opencode-go). Xiaomi MiMo has no `quota-toast.json` credential or endpoint setting. Use `MIMO_USAGE_COOKIE` or trusted user/global `opencode-quota/mimo.json`; see [Xiaomi MiMo setup](providers.md#xiaomi-mimo). @@ -480,13 +480,13 @@ Xiaomi MiMo has no `quota-toast.json` credential or endpoint setting. Use `MIMO_ | Option | Default | Meaning | | ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- | | `export.enabled` | `false` | Write a JSON export file each time the TUI Home screen quota footer refreshes. | -| `export.path` | `""` | Export file path. Empty string uses the XDG default: `$XDG_CACHE_HOME/opencode/quota-export.json`. Supports `~/` expansion. | +| `export.path` | `""` | Export file path. Empty string uses the XDG default: `$XDG_CACHE_HOME/opencode/quota-export.json`. Supports `~/` expansion. A relative path is relative to your home folder, where OpenCode's service runs. | ### Telemetry settings | Option | Default | Meaning | | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- | -| `telemetry.enabled` | `false` | Publish quota consumption and cache-age gauges through the host's global OpenTelemetry `MeterProvider`; adds no provider calls. | +| `telemetry.enabled` | `false` | Publish quota consumption and cache-age gauges through the global OpenTelemetry `MeterProvider` of OpenCode's server process; adds no provider calls. | See [External integration](external-integration.md#3-send-opentelemetry-metrics) for metric names, attributes, setup, and privacy behavior. diff --git a/docs/readme/external-integration.md b/docs/readme/external-integration.md index bfc1114a..3e878ca4 100644 --- a/docs/readme/external-integration.md +++ b/docs/readme/external-integration.md @@ -106,7 +106,7 @@ interval = 60 ## 3. Send OpenTelemetry metrics -Use this only when your OpenCode host already has an OpenTelemetry metrics provider and exporter. OpenCode Quota does not create or configure them. +Use this only when OpenCode's server process (normally the background service) already has an OpenTelemetry metrics provider and exporter; the metrics come from that process. OpenCode Quota does not create or configure them. Add this to `opencode-quota/quota-toast.json`: diff --git a/docs/readme/providers.md b/docs/readme/providers.md index 120a1bf2..dbb329ed 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -12,7 +12,7 @@ ## Pre-configured providers -Most providers work automatically. `Automatic` means OpenCode Quota reuses the credential saved through OpenCode's `/connect`, read from OpenCode 2's `opencode.db`. If you have several logins for one provider, each one gets its own rows and `*` marks the active one. If a provider has a “Needs setup” link, open that setup note only if you use that provider. Providers can appear in both audience sections when the vendor supports both. +Most providers work automatically. `Automatic` means OpenCode Quota reuses the credential saved through OpenCode's `/connect`, read through OpenCode 2's plugin API. If you have several logins for one provider, each one gets its own rows and `*` marks the active one. If a provider has a “Needs setup” link, open that setup note only if you use that provider. Providers can appear in both audience sections when the vendor supports both. ### American providers @@ -273,7 +273,7 @@ Credentials are checked in this order: 1. The environment variable named by `apiKeyEnv`. 2. Trusted global `provider..options.apiKey`. -3. An API-key entry in OpenCode `opencode.db`. +3. An API-key login saved in OpenCode 2 (`opencode.db`) for that provider id. Project secrets are never read. Custom definitions cannot add scripts, methods, headers, templates, executable mappings, regular expressions, JSONPath, or automatic endpoint discovery. @@ -391,16 +391,16 @@ Official references: [AI Credit billing reports](https://docs.github.com/en/rest ### Anthropic (Claude) -OpenCode's existing Anthropic OAuth credential is sufficient; a separate Claude Code installation is not required. To use Claude Code as the local quota source and credential fallback, install it, authenticate it, and make sure `claude` is on your `PATH`: +OpenCode's existing Anthropic OAuth credential is sufficient; a separate Claude Code installation is not required. To use Claude Code as the local quota source and credential fallback, install it and authenticate it. OpenCode Quota runs `claude` from OpenCode's `PATH`, or else from the first of `~/.claude/local/claude`, `~/.local/bin/claude`, `/opt/homebrew/bin/claude`, and `/usr/local/bin/claude` that works (not on Windows): ```bash claude auth login claude auth status ``` -If Claude lives at a custom path, set `anthropicBinaryPath` in `opencode-quota/quota-toast.json`. +If Claude lives elsewhere, or on Windows when `claude` is not on OpenCode's `PATH`, set `anthropicBinaryPath` in `opencode-quota/quota-toast.json`. `/quota_status` shows the one it ran as `binary_path`. -When Claude Code does not expose quota windows itself, quota is read from Anthropic's OAuth usage endpoint using the first usable access token: OpenCode's own `anthropic` OAuth credential from `opencode.db`, then Claude Code's credentials. `/quota_status` reports which store answered as `oauth_credential_source`. +When Claude Code does not expose quota windows itself, quota is read from Anthropic's OAuth usage endpoint using the first usable access token: OpenCode's own `anthropic` OAuth login, then Claude Code's credentials. `/quota_status` reports which store answered as `oauth_credential_source`. When that OAuth response includes enabled Usage Credits with numeric utilization, quota displays show a separate monthly **Claude Usage Credits** group; missing or invalid credit data leaves the regular 5-hour and weekly rows unchanged. @@ -435,7 +435,7 @@ npm install -g bailian-cli bl auth login --console ``` -OpenCode Quota runs only `bl usage token-plan --output json`. It does not install `bl`, open a login flow, read console cookies, or accept a custom command. macOS and Linux resolve `bl` from absolute PATH directories outside the workspace. On Windows, use WSL. Native Windows `bl.exe` and `.cmd` shims are not supported in this release. +OpenCode Quota runs only `bl usage token-plan --output json`. It does not install `bl`, open a login flow, read console cookies, or accept a custom command. macOS and Linux resolve `bl` from absolute directories on OpenCode's `PATH` that are outside its working folder. OpenCode's background service works in your home folder, so `bl` must live outside it (for example `/opt/homebrew/bin`, not `~/.local/bin` or an nvm folder). On Windows, use WSL. Native Windows `bl.exe` and `.cmd` shims are not supported in this release. Team plans, China-only `alibaba-token-plan-cn` runtimes, and cookie-based console scraping are out of scope. After you change the CLI's active console account, restart OpenCode or wait for the next live probe. `/quota_status` has an `alibaba_token_plan` live probe that stays separate from Alibaba Coding Plan diagnostics. @@ -543,7 +543,7 @@ Credentials resolve in this order: 1. `KILO_API_KEY` 2. Trusted user/global OpenCode config: `provider.kilo.options.apiKey` -3. A strict `kilo` API-key entry in OpenCode `opencode.db`: `{ "type": "api", "key": "..." }` +3. A `kilo` API-key login saved in OpenCode 2 (`opencode.db`) Project-local `opencode.json` and `opencode.jsonc` files are not read for this secret. The canonical OpenCode provider ID is `kilo`; if you use manual provider selection, include `kilo` in `enabledProviders`. @@ -598,7 +598,7 @@ Credentials resolve in this order: 1. `OLLAMA_API_KEY` 2. Trusted user/global OpenCode config: `provider.ollama-cloud.options.apiKey` -3. A strict `ollama-cloud` API-key entry in OpenCode `opencode.db`: `{ "type": "api", "key": "..." }` +3. An `ollama-cloud` API-key login saved in OpenCode 2 (`opencode.db`) Project-local `opencode.json` and `opencode.jsonc` files are not read for this secret. The old `OLLAMA_USAGE_COOKIE`, `ollama-cloud.json`, and `ollama-usage/config.yaml` cookie setup is no longer supported. @@ -606,13 +606,13 @@ Project-local `opencode.json` and `opencode.jsonc` files are not read for this s ### OpenCode Go -If you are signed in to the OpenCode Console in OpenCode 2, OpenCode Go first reads the Console's `/api/go/status`; if you are not signed in, the sign-in expired, or that call fails, it reads the official `https://opencode.ai/zen/go/v1/usage` API with your API key. If the Console reports no Go subscription, no Go rows appear. OpenCode Quota automatically resolves the API key in this order: +If you are signed in to the OpenCode Console in OpenCode 2, OpenCode Go first reads the Console's `/api/go/status`; if you are not signed in, the sign-in expired or cannot be read, or that call fails, it reads the official `https://opencode.ai/zen/go/v1/usage` API with your API key. Without an API key, a failed sign-in or Console call shows as an OpenCode Go error. If the Console reports no Go subscription, no Go rows appear. OpenCode Quota automatically resolves the API key in this order: 1. `OPENCODE_API_KEY` 2. Trusted user/global OpenCode config: `provider.opencode-go.options.apiKey` 3. Trusted user/global fallback: `provider.opencode.options.apiKey` -4. A strict `opencode-go` API-key entry in OpenCode `opencode.db`: `{ "type": "api", "key": "..." }`. This is the key the OpenCode CLI writes via `opencode auth login opencode-go`. -5. A strict legacy `opencode` API-key entry in `opencode.db` as the final fallback. +4. An `opencode-go` API-key login saved in OpenCode 2 (`opencode.db`). `opencode auth login opencode-go` creates it. +5. A legacy `opencode` API-key login as the final fallback. Project-local `opencode.json` and `opencode.jsonc` files are not read for this secret. Use `opencodeGoWindows` to choose which validated API results appear across surfaces and in the expanded sidebar: **Five-hour**, **Weekly**, and/or **Monthly**. To keep those rows expanded but prefer one while the sidebar is collapsed, set `tuiSidebarPanel.opencodeGoPreferredWindow` to `rolling`, `weekly`, or `monthly`; an unset or unavailable preference keeps the lowest-remaining selection. These settings do not change authentication or the API request. diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 9becae0e..a5d484c4 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -13,7 +13,15 @@ Start with `/quota_status` inside OpenCode, or `opencode-quota status` from a te If every provider is missing, confirm OpenCode Quota is listed in `opencode.jsonc` or `.json`. That one entry also loads the TUI; no `tui.json` entry is needed. -OpenCode Quota reads logins only from OpenCode 2's `opencode.db`, never `auth.json`. OpenCode 2 copies `auth.json` once, the first time it starts. If a provider is missing, log in to it again in OpenCode 2. `/quota_status` shows the `opencode.db` path in use; `OPENCODE_DB` and `XDG_DATA_HOME` change it. Custom or source builds of OpenCode may use `opencode-.db` instead; set `OPENCODE_DB` to that file's path. +OpenCode Quota asks OpenCode 2 for logins through its plugin API (OpenCode keeps them in `opencode.db`) and never reads `auth.json`. OpenCode 2 copies `auth.json` once, the first time it starts. If a provider is missing, log in to it again in OpenCode 2. In `/quota_status`, the `credential_source` section shows the login `source`, a `list_error` when OpenCode could not list logins, and `failures`: each login OpenCode could not return (for example a failed token refresh) as `provider:label:reason:detail`. Such a login also shows as an error row; log in to that provider again. `/quota_status` also shows the `opencode.db` path used for session and token history; `OPENCODE_DB` and `XDG_DATA_HOME` change it. Custom or source builds of OpenCode may use `opencode-.db` instead; set `OPENCODE_DB` to that file's path. + +## Service environment + +Every surface (TUI, Web, Desktop, and the terminal command) gets its numbers from OpenCode's background service. The service takes `PATH` and environment variables from whatever started it first, not from the terminal you use now, and it runs in your home folder. + +- **`claude` or `bl` not found, or an API-key variable ignored:** in a terminal where they work, run `opencode service restart`. To keep that `PATH` for every later start, run `opencode service set env PATH "$PATH"` (it stops the service; open OpenCode again). For Claude you can instead set `anthropicBinaryPath`. +- A relative `export.path` is relative to your home folder. +- Cursor's plugin-entry check reads `opencode.json` in your global config folder and your home folder, not in the project folder. ## Common problems @@ -70,7 +78,7 @@ Run `/quota_status` and inspect `quota_providers`. Each definition shows its sta | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Config is rejected | Run `opencode-quota provider add` and keep `quotaProviders` in global OpenCode JSONC/JSON. Remove `customSources`, unknown fields, duplicate IDs/request identities, or overlapping model coverage. | | Definition is `unavailable` | Confirm OpenCode reports the exact configured `providerId`. With `onlyCurrentModel`, confirm the model id without provider prefix matches `modelIds`, or omit `modelIds` for provider-wide coverage. | -| `missing_credential` | Set the explicit `apiKeyEnv`, or configure trusted global `provider..options.apiKey`, or a strict `{ "type": "api", "key": "..." }` auth entry. | +| `missing_credential` | Set the explicit `apiKeyEnv`, or configure trusted global `provider..options.apiKey`, or an API-key login saved in OpenCode 2 for that provider id. | | `http_error`, `timeout`, or response error | Check the endpoint service and response format. `/quota_status` intentionally hides URLs, request/response contents, raw errors, and secret material. | | One definition fails but others render | This is expected partial-aggregate behavior. Successful definitions remain visible and the failed definition stays an error/status row. | | Single-window output shows fewer rows | Each source keeps only its lowest remaining percentage, or first value row. Use `"formatStyle": "allWindows"` for every row. | @@ -85,7 +93,7 @@ Run `/quota_status` and check the Anthropic section. | Symptom | Fix | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -| `claude` not found | Install Claude Code and make sure `claude` is on your `PATH`. | +| `claude` not found | Install Claude Code. OpenCode Quota tries `claude` on the [service `PATH`](#service-environment), then `~/.claude/local/claude`, `~/.local/bin/claude`, `/opt/homebrew/bin/claude`, and `/usr/local/bin/claude` (not on Windows). `binary_path` shows which one ran. | | Claude is installed at a custom path | Set `anthropicBinaryPath` in `opencode-quota/quota-toast.json`. | | Not authenticated | Run `claude auth login`, then confirm `claude auth status` works. | | Auth works but no quota rows appear | Check `quota_source` and `message` in `/quota_status`; re-authenticate Claude if the OAuth credential fallback is missing or stale. | @@ -119,8 +127,8 @@ Run `/quota_status` and check the OpenAI auth source and token status. | Symptom | Fix | | --------------------- | ------------------------------------------------------------------------------------------ | -| OpenAI quota missing | Confirm OpenCode native OpenAI OAuth is present in `opencode.db`. | -| Token expired | Re-run OpenCode's OpenAI auth flow. | +| OpenAI quota missing | Sign in to OpenAI in OpenCode 2: `opencode auth login openai`. | +| `OpenAI sign-in could not be refreshed` | OpenCode could not refresh the token. Run `opencode auth login openai`. For xAI, the row says `xAI sign-in could not be refreshed`; run `opencode auth login xai`. | | Provider not detected | Confirm your OpenCode config uses the `openai` provider or a compatible OpenAI auth entry. |
@@ -161,7 +169,7 @@ Run `/quota_status` and check the `alibaba_token_plan` live probe. This source i | Symptom | Fix | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| CLI not detected | Install official `bailian-cli` so `bl` is on an absolute PATH directory outside the workspace. On Windows, run this from WSL. Native `bl.exe` and `.cmd` shims are not supported. | +| CLI not detected | Install official `bailian-cli` so `bl` is in an absolute directory on the [service `PATH`](#service-environment) outside the service's working folder, your home folder (for example `/opt/homebrew/bin`, not `~/.local/bin` or an nvm folder). On Windows, run this from WSL. Native `bl.exe` and `.cmd` shims are not supported. | | Console session expired | Run `bl auth login --console`. A Coding Plan API key cannot authenticate this provider. | | Weekly row only | The official CLI may omit the five-hour window. OpenCode Quota does not invent a missing window. | | JSON export empty | `show --json` is cache-only. This provider is uncached, so a separate CLI process reports it unavailable instead of running `bl`. | @@ -189,7 +197,7 @@ These providers use trusted env vars, trusted user/global OpenCode config, or na If Synthetic is authenticated and the quota endpoint returns HTTP 200 `{}`, `/quota` and `/quota_status` report `Synthetic returned no quota data for this account.` That is not an invalid API key or a Clerk/browser requirement. The plugin does not invent 5h or Weekly rows. `/quota_status` shows it on `live_error_*`. -For security, repo-local `opencode.json` / `opencode.jsonc` is ignored for provider secrets in these integrations. Put secrets in environment variables or trusted user/global config. OpenCode auth fallbacks for API-key providers require `{ "type": "api", "key": "..." }` entries. +For security, repo-local `opencode.json` / `opencode.jsonc` is ignored for provider secrets in these integrations. Put secrets in environment variables or trusted user/global config. OpenCode auth fallbacks for API-key providers must be API-key logins, not OAuth sign-ins. @@ -244,12 +252,13 @@ Run `/quota_status` and check the `xiaomi` section. Diagnostics show state, sour
OpenCode Go -Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*` diagnostics, the selected display windows, normalized API usage, and `live_fetch_error` without exposing the API key. When you are signed in to the OpenCode Console, `go_source` shows whether the Console (`console`) or the API key (`legacy_key`) answered, and `console_error` shows why the Console failed. `opencode_go_state: not_subscribed` means the Console reports no Go subscription. +Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*` diagnostics, the selected display windows, normalized API usage, and `live_fetch_error` without exposing the API key. When you are signed in to the OpenCode Console, `console_auth_state` and `console_server` describe that sign-in, `go_source` shows whether the Console (`console`) or the API key (`legacy_key`) answered, and `console_error` shows why the Console failed. `opencode_go_state: not_subscribed` means the Console reports no Go subscription. | Symptom | Fix | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Provider not detected | Set `OPENCODE_API_KEY`, trusted global `provider.opencode-go.options.apiKey`, fallback `provider.opencode.options.apiKey`, a strict `opencode-go` API-key entry in OpenCode `opencode.db`, or a strict legacy `opencode` auth entry as the final fallback. Then check `auth_state`, `auth_source`, and `auth_checked_paths`. | -| `auth_state` is `invalid` | Fix the primary `opencode-go` record in `opencode.db` so it is `{ "type": "api", "key": "..." }`. A malformed primary record blocks the legacy `opencode` fallback and is reported in `auth_error`. | +| Provider not detected | Sign in with `opencode auth login opencode`, or set `OPENCODE_API_KEY`, trusted global `provider.opencode-go.options.apiKey`, fallback `provider.opencode.options.apiKey`, an `opencode-go` API-key login (`opencode auth login opencode-go`), or a legacy `opencode` API-key login as the final fallback. Then check `auth_state`, `auth_source`, and `auth_checked_paths`. | +| `auth_state` is `invalid` | Log in again with `opencode auth login opencode-go`. A broken `opencode-go` login blocks the legacy `opencode` fallback and is reported in `auth_error`. | +| `OpenCode Console sign-in failed` or `OpenCode Console request failed` | Shown only when no API key is set. Run `opencode auth login opencode`, or set an API key. `console_error` has the reason. | | API returns 401 or 403 | The usage API rejected the key. Update the winning source shown by `auth_source`, wait briefly for credential caching to expire, and rerun `/quota_status`. | | Invalid API response | Check `live_fetch_error`. OpenCode Quota requires valid 5h, Weekly, and Monthly results, so one missing or malformed API window rejects the full response instead of showing partial quota. | | API request times out or fails | Check `live_fetch_error`, confirm `https://opencode.ai/zen/go/v1/usage` is reachable, and retry. Increase `requestTimeoutMs` only when the error is a timeout. | diff --git a/docs/readme/updating.md b/docs/readme/updating.md index 11a924bd..fc768547 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -49,7 +49,8 @@ The updater builds one plan, prints it in full, and then either stops or applies What changed in `5.0.0`: - **OpenCode 1 is no longer supported.** `init` and `update` detect OpenCode 1 and keep you on 4.x. See [OpenCode 1](#opencode-1). -- **Logins come only from `opencode.db`.** OpenCode Quota reads logins only from OpenCode 2's database, never from `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. +- **Logins come only from OpenCode 2.** OpenCode Quota asks OpenCode 2 for logins through its plugin API (OpenCode keeps them in `opencode.db`) and never reads `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. +- **Quota is computed in OpenCode's background service.** `PATH`, environment variables, and the working folder (your home folder) come from the service, not from your terminal. See [Service environment](troubleshooting.md#service-environment). - **Web and Desktop have slash commands but no toasts or panels,** because OpenCode 2 gives plugins no Web UI hooks. On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. See [Web and Desktop notes](manual-install.md#web-and-desktop-notes). - **One plugin entry.** One `"plugin"` entry in `opencode.json` loads both the server and the TUI; no `tui.json` entry is needed. - **TUI reports open in a popup.** TUI slash commands and the command palette open the report in a popup and leave no chat message. `tuiCommandDisplay` now defaults to `"dialog"`; set `"inline"` to keep TUI slash reports in the chat instead. @@ -106,10 +107,10 @@ OpenCode Go now uses an official API key. Configure one supported source in this 1. `OPENCODE_API_KEY` 2. Trusted user/global OpenCode config: `provider.opencode-go.options.apiKey` 3. Trusted user/global fallback: `provider.opencode.options.apiKey` -4. A strict `opencode-go` API-key entry in OpenCode `opencode.db` -5. A strict legacy `opencode` API-key entry in `opencode.db` as the final fallback +4. An `opencode-go` API-key login saved in OpenCode 2 (`opencode.db`) +5. A legacy `opencode` API-key login as the final fallback -You can create the canonical `opencode.db` entry with: +You can create the `opencode-go` API-key login with: ```bash opencode auth login opencode-go diff --git a/src/lib/anthropic.ts b/src/lib/anthropic.ts index 1b67198c..dc88aed6 100644 --- a/src/lib/anthropic.ts +++ b/src/lib/anthropic.ts @@ -4,12 +4,13 @@ * Uses the local Claude CLI/runtime to detect install/auth state first. When * Claude auth is confirmed but local quota windows are missing, it falls back * to Anthropic's OAuth usage endpoint using the first usable OAuth access - * token: OpenCode's own opencode.db, then Claude OAuth credentials (macOS + * token: OpenCode's own Anthropic login, then Claude OAuth credentials (macOS * Keychain first, then the local credentials file). */ import { execFile } from "child_process"; import { createHash } from "crypto"; +import { existsSync } from "fs"; import { readFile } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; @@ -24,6 +25,8 @@ import { } from "./resolved-auth-identity.js"; const DEFAULT_CLAUDE_BINARY = "claude"; +/** How `binaryPath` reads when the default `claude` ran from PATH. */ +const CLAUDE_ON_PATH_DISPLAY = "claude (PATH)"; const CLAUDE_COMMAND_TIMEOUT_MS = 3_000; const ANTHROPIC_DIAGNOSTICS_TTL_MS = 5_000; const ANTHROPIC_OAUTH_BACKOFF_BASE_MS = 30_000; @@ -96,6 +99,11 @@ export interface AnthropicDiagnostics { quotaSource: AnthropicQuotaSource; /** Credential store that supplied the OAuth token for the usage probe. */ oauthCredentialSource?: AnthropicCredentialSource; + /** + * The Claude CLI the probe ran: the configured path, `claude (PATH)`, or an + * install-folder path. Null when no Claude CLI was found. + */ + binaryPath: string | null; checkedCommands: string[]; message?: string; quota?: AnthropicQuotaResult; @@ -131,6 +139,7 @@ type AnthropicLocalDiagnostics = { installed: boolean; version: string | null; authStatus: AnthropicAuthStatus; + binaryPath: string | null; checkedCommands: string[]; message?: string; localQuota?: AnthropicQuotaResult; @@ -203,6 +212,21 @@ export function resolveAnthropicBinaryPath(binaryPath?: string): string { return trimmed ? trimmed : DEFAULT_CLAUDE_BINARY; } +/** + * Usual Claude CLI install locations, tried in order when `anthropicBinaryPath` + * is not set and `claude` is not on the PATH of the OpenCode service. Windows + * is skipped: set `anthropicBinaryPath` there. + */ +function claudeInstallFolderCandidates(): string[] { + if (process.platform === "win32") return []; + return [ + join(homedir(), ".claude", "local", "claude"), + join(homedir(), ".local", "bin", "claude"), + "/opt/homebrew/bin/claude", + "/usr/local/bin/claude", + ]; +} + function formatCommandDisplayArg(value: string): string { const sanitized = sanitizeDisplayText(value); return /[\s"]/u.test(sanitized) ? JSON.stringify(sanitized) : sanitized; @@ -1146,6 +1170,7 @@ function mapLocalDiagnosticsToAnthropicDiagnostics( authStatus: localDiagnostics.authStatus, quotaSupported: true, quotaSource: "claude-auth-status-json", + binaryPath: localDiagnostics.binaryPath, checkedCommands: localDiagnostics.checkedCommands, quota: localDiagnostics.localQuota, }; @@ -1157,6 +1182,7 @@ function mapLocalDiagnosticsToAnthropicDiagnostics( authStatus: localDiagnostics.authStatus, quotaSupported: false, quotaSource: "none", + binaryPath: localDiagnostics.binaryPath, checkedCommands: localDiagnostics.checkedCommands, }; @@ -1167,25 +1193,51 @@ function mapLocalDiagnosticsToAnthropicDiagnostics( return diagnostics; } +async function runClaudeVersionCommand( + binaryPath: string, + checkedCommands: string[], +): Promise { + const versionCommand = buildClaudeCommandInvocation(binaryPath, ["--version"]); + checkedCommands.push(versionCommand.display); + return await runClaudeCommand(versionCommand); +} + async function probeAnthropicLocalDiagnostics( options: AnthropicProbeOptions = {}, ): Promise { - const binaryPath = resolveAnthropicBinaryPath(options.binaryPath); + const configuredBinaryPath = resolveAnthropicBinaryPath(options.binaryPath); const checkedCommands: string[] = []; - const versionCommand = buildClaudeCommandInvocation(binaryPath, ["--version"]); - checkedCommands.push(versionCommand.display); - const versionResult = await runClaudeCommand(versionCommand); + let binaryPath = configuredBinaryPath; + let versionResult = await runClaudeVersionCommand(binaryPath, checkedCommands); + // No configured path and no `claude` on PATH: the first install folder whose CLI runs wins. + const installFolderCandidates = + configuredBinaryPath === DEFAULT_CLAUDE_BINARY ? claudeInstallFolderCandidates() : []; + if (isCommandMissing(versionResult)) { + for (const candidate of installFolderCandidates) { + if (!existsSync(candidate)) continue; + binaryPath = candidate; + versionResult = await runClaudeVersionCommand(candidate, checkedCommands); + if (!isCommandMissing(versionResult)) break; + } + } if (isCommandMissing(versionResult)) { + const alsoChecked = + installFolderCandidates.length > 0 + ? ` Also checked: ${installFolderCandidates.map((candidate) => sanitizeDisplayText(candidate)).join(", ")}.` + : ""; return { installed: false, version: null, authStatus: "unknown", + binaryPath: null, checkedCommands, - message: `Claude CLI (\`${sanitizeDisplayText(binaryPath)}\`) is not installed or not on PATH.`, + message: `Claude CLI (\`${sanitizeDisplayText(configuredBinaryPath)}\`) is not installed or not on PATH.${alsoChecked}`, }; } + const binaryPathDisplay = + binaryPath === DEFAULT_CLAUDE_BINARY ? CLAUDE_ON_PATH_DISPLAY : binaryPath; const version = parseVersion(`${versionResult.stdout}\n${versionResult.stderr}`); const authStatusJsonCommand = buildClaudeCommandInvocation(binaryPath, [ @@ -1208,6 +1260,7 @@ async function probeAnthropicLocalDiagnostics( installed: true, version, authStatus: parsedAuth.authStatus, + binaryPath: binaryPathDisplay, checkedCommands, message: parsedAuth.message, }; @@ -1219,6 +1272,7 @@ async function probeAnthropicLocalDiagnostics( installed: true, version, authStatus: "authenticated", + binaryPath: binaryPathDisplay, checkedCommands, localQuota: quota, }; @@ -1228,6 +1282,7 @@ async function probeAnthropicLocalDiagnostics( installed: true, version, authStatus: "authenticated", + binaryPath: binaryPathDisplay, checkedCommands, message: CLAUDE_NO_LOCAL_QUOTA_MESSAGE, }; @@ -1331,6 +1386,7 @@ export async function getAnthropicDiagnostics( authStatus: localDiagnostics.authStatus, quotaSupported: false, quotaSource: "none", + binaryPath: localDiagnostics.binaryPath, checkedCommands: localDiagnostics.checkedCommands, message: buildAnthropicNoQuotaDiagnosticsMessage(credentials.detail), }; @@ -1364,6 +1420,7 @@ export async function getAnthropicDiagnostics( quotaSupported: false, quotaSource: "none", oauthCredentialSource: credentials.source, + binaryPath: localDiagnostics.binaryPath, checkedCommands: localDiagnostics.checkedCommands, message: buildAnthropicNoQuotaDiagnosticsMessage(fallbackQuota.detail), }; @@ -1380,6 +1437,7 @@ export async function getAnthropicDiagnostics( ? "opencode-auth-oauth-api" : "claude-credentials-oauth-api", oauthCredentialSource: credentials.source, + binaryPath: localDiagnostics.binaryPath, checkedCommands: localDiagnostics.checkedCommands, quota: fallbackQuota.quota, }; diff --git a/src/lib/quota-status.ts b/src/lib/quota-status.ts index 6225eb20..592fe142 100644 --- a/src/lib/quota-status.ts +++ b/src/lib/quota-status.ts @@ -31,6 +31,7 @@ import { readPricingRefreshState, hasProvider as snapshotHasProvider, } from "./modelsdev-pricing.js"; +import { getCredentialSourceDiagnostics } from "./opencode-auth.js"; import { getOpenCodeDbPath } from "./opencode-db-path.js"; import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js"; import { getOpenCodeDbStats } from "./opencode-storage.js"; @@ -90,6 +91,10 @@ const OPENCODE_GO_STATUS_DETAIL_KEYS = new Set([ "monthly_usage", "live_fetch_error", "opencode_go_state", + "go_source", + "console_error", + "console_auth_state", + "console_server", ]); type ProviderLiveProbe = { providerId: string; @@ -774,7 +779,7 @@ export async function buildQuotaStatusReport(params: { const dbPath = getOpenCodeDbPath(); pathsRows.push({ key: "opencode.db", - value: `path=${dbPath} present=${(await pathExists(dbPath)) ? "true" : "false"}`, + value: `path=${dbPath} present=${(await pathExists(dbPath)) ? "true" : "false"} (session and token history)`, }); appendProviderStatusDetailRows( @@ -792,6 +797,28 @@ export async function buildQuotaStatusReport(params: { ); sections.push(createKvSection("paths", "paths:", pathsRows)); + // === credential_source === + const credentialSource = getCredentialSourceDiagnostics(); + sections.push( + createKvSection("credential_source", "credential_source:", [ + { + key: "source", + value: credentialSource.state === "bound" ? credentialSource.kind : "unbound", + }, + { key: "list_error", value: credentialSource.lastListError?.detail ?? "(none)" }, + { + key: "failures", + value: joinOrNone( + credentialSource.failures.map((failure) => + [failure.integrationId, failure.label, failure.category, failure.detail] + .map((part) => sanitizeSingleLineDisplayText(part)) + .join(":"), + ), + ), + }, + ]), + ); + for (const [id, providerId] of [ ["openai", "openai"], ["anthropic", "anthropic"], diff --git a/src/providers/anthropic.ts b/src/providers/anthropic.ts index 24eb8bfd..40358371 100644 --- a/src/providers/anthropic.ts +++ b/src/providers/anthropic.ts @@ -83,6 +83,7 @@ export const anthropicProvider: QuotaProvider = { statusDetails = statusDetailsFromRecord({ cli_installed: diagnostics.installed ? "true" : "false", cli_version: diagnostics.version ?? "(none)", + binary_path: diagnostics.binaryPath ?? "(none)", auth_status: diagnostics.authStatus, quota_supported: diagnostics.quotaSupported ? "true" : "false", quota_source: diagnostics.quotaSource === "none" ? "(none)" : diagnostics.quotaSource, diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index d9444e50..4f87415e 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -35,6 +35,7 @@ import { } from "./result-helpers.js"; const OPENCODE_GO_PROVIDER_LABEL = "OpenCode Go"; +const CONSOLE_LOGIN_HINT = "Run `opencode auth login opencode`."; const OPENCODE_GO_WINDOW_ORDER: OpenCodeGoWindowKey[] = ["rolling", "weekly", "monthly"]; const OPENCODE_GO_WINDOW_LABELS: Record = { rolling: { name: `${OPENCODE_GO_PROVIDER_LABEL} 5h`, label: "5h:" }, @@ -106,7 +107,12 @@ export const opencodeGoProvider: QuotaProvider = { async isAvailable(_ctx: QuotaProviderContext): Promise { const consoleAuth = await resolveOpenCodeConsoleAuth(); - if (consoleAuth.state === "configured" || consoleAuth.state === "expired") { + // A failed sign-in stays available so fetch() can show its error when there is no API key. + if ( + consoleAuth.state === "configured" || + consoleAuth.state === "expired" || + consoleAuth.state === "invalid" + ) { return true; } const auth = await resolveOpenCodeGoAuthCached({ @@ -172,7 +178,10 @@ export const opencodeGoProvider: QuotaProvider = { { key: "go_source", value: "legacy_key" }, { key: "selected_windows", value: windows.join(",") }, ]; - return await fetchOpenCodeGoLegacy(ctx, diagnostics, legacyStatusDetails); + return await fetchOpenCodeGoLegacy(ctx, diagnostics, legacyStatusDetails, { + message: `OpenCode Console request failed: ${scrubCredentialErrorText(consoleResult.error)}`, + retryable: consoleResult.retryable, + }); } if (consoleAuth.state === "expired") { @@ -194,13 +203,19 @@ export const opencodeGoProvider: QuotaProvider = { const diagnostics = await getOpenCodeGoAuthDiagnostics({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, }); - return await fetchOpenCodeGoLegacy(ctx, diagnostics, [ - ...authStatusDetails(diagnostics), - { key: "console_auth_state", value: "invalid" }, - { key: "console_error", value: scrubCredentialErrorText(consoleAuth.error) }, - { key: "go_source", value: "legacy_key" }, - { key: "selected_windows", value: windows.join(",") }, - ]); + const consoleError = scrubCredentialErrorText(consoleAuth.error); + return await fetchOpenCodeGoLegacy( + ctx, + diagnostics, + [ + ...authStatusDetails(diagnostics), + { key: "console_auth_state", value: "invalid" }, + { key: "console_error", value: consoleError }, + { key: "go_source", value: "legacy_key" }, + { key: "selected_windows", value: windows.join(",") }, + ], + { message: `OpenCode Console sign-in failed: ${consoleError}. ${CONSOLE_LOGIN_HINT}` }, + ); } const diagnostics = await getOpenCodeGoAuthDiagnostics({ @@ -213,10 +228,15 @@ export const opencodeGoProvider: QuotaProvider = { }, }; +/** + * The API-key path. `consoleFailure` is set when the Console sign-in or request + * failed first; without an API key it becomes Go's one error row. + */ async function fetchOpenCodeGoLegacy( ctx: QuotaProviderContext, diagnostics: OpenCodeGoAuthDiagnostics, statusDetails?: QuotaProviderResult["statusDetails"], + consoleFailure?: { message: string; retryable?: boolean }, ): Promise { const windows = ctx.config.opencodeGoWindows ?? OPENCODE_GO_WINDOW_ORDER; const baseStatusDetails = statusDetails ?? []; @@ -226,6 +246,14 @@ async function fetchOpenCodeGoLegacy( if (auth.state === "none") { notSubscribedCredentialFingerprints.clear(); + if (consoleFailure) { + return withStatusDetails( + attemptedErrorResult(OPENCODE_GO_PROVIDER_LABEL, consoleFailure.message, { + retryable: consoleFailure.retryable, + }), + baseStatusDetails, + ); + } return withStatusDetails(notAttemptedResult(), baseStatusDetails); } diff --git a/tests/lib.anthropic.test.ts b/tests/lib.anthropic.test.ts index 1ea5c862..2b0502e8 100644 --- a/tests/lib.anthropic.test.ts +++ b/tests/lib.anthropic.test.ts @@ -8,6 +8,10 @@ const authMocks = vi.hoisted(() => ({ readAuthFileCached: vi.fn(), })); +const fsMocks = vi.hoisted(() => ({ + existsSync: vi.fn((_path: string) => false), +})); + const identityMocks = vi.hoisted(() => ({ deriveResolvedAuthIdentity: vi.fn( async (params: { providerId: string }) => `identity:${params.providerId}`, @@ -18,6 +22,7 @@ const identityMocks = vi.hoisted(() => ({ ), })); +import { homedir } from "node:os"; import { join } from "node:path"; import { execFile } from "child_process"; import { readFile } from "fs/promises"; @@ -42,6 +47,11 @@ vi.mock("fs/promises", () => ({ readFile: vi.fn(), })); +vi.mock("fs", async (importOriginal) => ({ + ...(await importOriginal()), + existsSync: fsMocks.existsSync, +})); + vi.mock("../src/lib/opencode-auth.js", () => ({ readAuthFileCached: authMocks.readAuthFileCached, })); @@ -71,6 +81,12 @@ type ExecSequenceStep = { }; const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage"; +const CLAUDE_INSTALL_FOLDER_CANDIDATES = [ + join(homedir(), ".claude", "local", "claude"), + join(homedir(), ".local", "bin", "claude"), + "/opt/homebrew/bin/claude", + "/usr/local/bin/claude", +]; const execFileMock = vi.mocked(execFile); const readFileMock = vi.mocked(readFile); @@ -148,6 +164,8 @@ beforeEach(() => { afterEach(() => { vi.useRealTimers(); execFileMock.mockReset(); + fsMocks.existsSync.mockReset(); + fsMocks.existsSync.mockReturnValue(false); readFileMock.mockReset(); readAuthFileCachedMock.mockReset(); fetchWithTimeoutMock.mockClear(); @@ -517,6 +535,7 @@ describe("Claude CLI diagnostics", () => { expect(diagnostics.version).toBe("2.1.123"); expect(diagnostics.authStatus).toBe("authenticated"); expect(diagnostics.quotaSupported).toBe(true); + expect(diagnostics.binaryPath).toBe("C:/Users/alice/.local/bin/claude.exe"); expect(diagnostics.checkedCommands).toEqual([ "C:/Users/alice/.local/bin/claude.exe --version", "C:/Users/alice/.local/bin/claude.exe auth status --json", @@ -538,6 +557,7 @@ describe("Claude CLI diagnostics", () => { }); it("reports missing Claude CLI as unavailable without quota data", async () => { + setProcessPlatform("linux"); mockExecSequence([ { code: "ENOENT", @@ -547,19 +567,97 @@ describe("Claude CLI diagnostics", () => { const diagnostics = await getAnthropicDiagnostics(); + expect(fsMocks.existsSync.mock.calls.map(([path]) => path)).toEqual( + CLAUDE_INSTALL_FOLDER_CANDIDATES, + ); expect(diagnostics).toEqual({ installed: false, version: null, authStatus: "unknown", quotaSupported: false, quotaSource: "none", + binaryPath: null, checkedCommands: ["claude --version"], - message: "Claude CLI (`claude`) is not installed or not on PATH.", + message: `Claude CLI (\`claude\`) is not installed or not on PATH. Also checked: ${CLAUDE_INSTALL_FOLDER_CANDIDATES.join(", ")}.`, }); await expect(hasAnthropicCredentialsConfigured()).resolves.toBe(false); await expect(queryAnthropicQuota()).resolves.toBeNull(); }); + it("runs claude from PATH without looking in install folders", async () => { + mockExecSequence([ + { stdout: "claude 1.2.3\n" }, + { + stdout: JSON.stringify({ + authenticated: true, + quota: { + five_hour: { used_percentage: 10 }, + seven_day: { used_percentage: 20 }, + }, + }), + }, + ]); + + const diagnostics = await getAnthropicDiagnostics(); + + expect(fsMocks.existsSync).not.toHaveBeenCalled(); + expect(diagnostics.binaryPath).toBe("claude (PATH)"); + expect(diagnostics.checkedCommands).toEqual(["claude --version", "claude auth status --json"]); + }); + + it("uses the first install folder that has claude when it is not on PATH", async () => { + setProcessPlatform("linux"); + const [claudeLocal, localBin] = CLAUDE_INSTALL_FOLDER_CANDIDATES; + fsMocks.existsSync.mockImplementation( + (path) => path === localBin || path === "/usr/local/bin/claude", + ); + mockExecSequence([ + { code: "ENOENT", errorMessage: "spawn claude ENOENT" }, + { stdout: "2.1.0 (Claude Code)\n" }, + { + stdout: JSON.stringify({ + authenticated: true, + quota: { + five_hour: { used_percentage: 10 }, + seven_day: { used_percentage: 20 }, + }, + }), + }, + ]); + + const diagnostics = await getAnthropicDiagnostics(); + + expect(fsMocks.existsSync.mock.calls.map(([path]) => path)).toEqual([claudeLocal, localBin]); + expect(diagnostics.installed).toBe(true); + expect(diagnostics.version).toBe("2.1.0"); + expect(diagnostics.quotaSupported).toBe(true); + expect(diagnostics.binaryPath).toBe(localBin); + expect(diagnostics.checkedCommands).toEqual([ + "claude --version", + buildClaudeCommandInvocation(localBin!, ["--version"]).display, + buildClaudeCommandInvocation(localBin!, ["auth", "status", "--json"]).display, + ]); + expect(execFileMock).toHaveBeenNthCalledWith( + 3, + localBin, + ["auth", "status", "--json"], + expect.any(Object), + expect.any(Function), + ); + }); + + it("does not look in install folders on Windows", async () => { + setProcessPlatform("win32"); + mockExecSequence([{ code: "ENOENT", errorMessage: "spawn cmd.exe ENOENT" }]); + + const diagnostics = await getAnthropicDiagnostics(); + + expect(fsMocks.existsSync).not.toHaveBeenCalled(); + expect(diagnostics.installed).toBe(false); + expect(diagnostics.binaryPath).toBeNull(); + expect(diagnostics.message).toBe("Claude CLI (`claude`) is not installed or not on PATH."); + }); + it("uses OpenCode Anthropic OAuth when Claude CLI is unavailable", async () => { mockExecSequence([ { @@ -635,7 +733,11 @@ describe("Claude CLI diagnostics", () => { expect(diagnostics.checkedCommands).toEqual([ '"/Applications/Claude Code.app/Contents/MacOS/claude" --version', ]); - expect(diagnostics.message).toContain("/Applications/Claude Code.app/Contents/MacOS/claude"); + expect(fsMocks.existsSync).not.toHaveBeenCalled(); + expect(diagnostics.binaryPath).toBeNull(); + expect(diagnostics.message).toBe( + "Claude CLI (`/Applications/Claude Code.app/Contents/MacOS/claude`) is not installed or not on PATH.", + ); }); it("reports unauthenticated Claude CLI status", async () => { diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index af0d7737..6e2327f4 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -55,8 +55,17 @@ vi.mock("fs/promises", () => ({ stat: fsPromiseMocks.stat, })); +const credentialSourceMocks = vi.hoisted(() => ({ + getCredentialSourceDiagnostics: vi.fn(() => ({ + state: "bound", + kind: "opencode-integration-api", + failures: [], + })), +})); + vi.mock("../src/lib/opencode-auth.js", () => ({ getCredentialDatabasePaths: () => ["/tmp/opencode.db"], + getCredentialSourceDiagnostics: credentialSourceMocks.getCredentialSourceDiagnostics, readAuthFileCached: vi.fn(async () => ({})), })); @@ -490,7 +499,9 @@ describe("buildQuotaStatusReport", () => { expect(report).not.toContain("tui:"); expect(report).toContain("- workspace_root: /tmp/workspace"); expect(report).toContain("- config_root: /tmp/project"); - expect(report).toContain("- opencode.db: path=/tmp/opencode.db present=false"); + expect(report).toContain( + "- opencode.db: path=/tmp/opencode.db present=false (session and token history)", + ); expect(report).toContain( "- pricing: source=test active_source=bundled generated_at=2026-01-01T00:00:00.000Z units=usd_per_1m_tokens", ); @@ -1144,6 +1155,30 @@ describe("buildQuotaStatusReport", () => { ); }); + it("reports which source answered OpenCode Go and why the Console failed", async () => { + const report = await buildOpenCodeGoStatusReport({ + providerAvailability: [makeProviderAvailability("opencode-go")], + providerLiveProbes: [ + makeProviderProbe("opencode-go", { + statusDetails: makeStatusDetails({ + auth_state: "none", + console_auth_state: "configured", + console_server: "https://opencode.ai", + console_error: "OpenCode Console API error 500 (/api/go/status)", + go_source: "legacy_key", + selected_windows: "rolling,weekly,monthly", + }), + }), + ], + }); + + const section = getReportSection(report, "opencode_go:"); + expect(section).toContain("- console_auth_state: configured"); + expect(section).toContain("- console_server: https://opencode.ai"); + expect(section).toContain("- console_error: OpenCode Console API error 500 (/api/go/status)"); + expect(section).toContain("- go_source: legacy_key"); + }); + it("reports safe OpenCode Go invalid-auth details without legacy config fields", async () => { const report = await buildOpenCodeGoStatusReport({ providerLiveProbes: [ @@ -1418,6 +1453,54 @@ describe("buildQuotaStatusReport", () => { expect(section).not.toContain("live_error"); }); + it("reports the login source, its last list error, and each login OpenCode could not return", async () => { + credentialSourceMocks.getCredentialSourceDiagnostics.mockReturnValueOnce({ + state: "bound", + kind: "opencode-integration-api", + lastListError: { at: 1, detail: "Invalid credential value" }, + failures: [ + { + integrationId: "openai", + connectionId: "cred_1", + label: "Work", + category: "refresh_failed", + detail: "HTTP 401 [redacted]", + at: 2, + }, + { + integrationId: "xai", + connectionId: "xai", + label: "", + category: "active_failed", + detail: "database is locked", + at: 3, + }, + ], + } as never); + + const report = await buildQuotaStatusReportForTest(); + + expect(getReportSection(report, "credential_source:")).toMatchInlineSnapshot(` + "credential_source: + - source: opencode-integration-api + - list_error: Invalid credential value + - failures: openai:Work:refresh_failed:HTTP 401 [redacted] | xai::active_failed:database is locked + " + `); + }); + + it("reports an unbound login source", async () => { + credentialSourceMocks.getCredentialSourceDiagnostics.mockReturnValueOnce({ + state: "unbound", + kind: "opencode-integration-api", + failures: [], + } as never); + + const report = await buildQuotaStatusReportForTest(); + + expect(getReportSection(report, "credential_source:")).toContain("- source: unbound"); + }); + it("locks the early /quota_status section layout after the shared report-document migration", async () => { const report = await buildProviderStatusReport("copilot", { configSource: "defaults", @@ -1440,6 +1523,7 @@ describe("buildQuotaStatusReport", () => { makeProviderSuccessProbe("anthropic", { cli_installed: "true", cli_version: "1.2.3", + binary_path: "claude (PATH)", auth_status: "authenticated", quota_supported: "false", quota_source: "(none)", @@ -1461,7 +1545,7 @@ describe("buildQuotaStatusReport", () => { ); expect(blank).toBe(""); - const excerpt = body.slice(0, 47).join("\n"); + const excerpt = body.slice(0, 53).join("\n"); expect(excerpt).toMatchInlineSnapshot(` "toast: - configSource: defaults @@ -1478,13 +1562,18 @@ describe("buildQuotaStatusReport", () => { paths: - opencode_dirs: data=/tmp/data config=/tmp/config cache=/tmp/cache state=/tmp/state - - opencode.db: path=/tmp/opencode.db present=false + - opencode.db: path=/tmp/opencode.db present=false (session and token history) - alibaba auth configured: false - alibaba_api_key_source: (none) - alibaba_api_key_checked_paths: (none) - alibaba_api_key_credential_database_paths: /tmp/opencode.db - alibaba_coding_plan: (none) + credential_source: + - source: opencode-integration-api + - list_error: (none) + - failures: (none) + openai: - auth_configured: false - auth_source: (none) @@ -1496,6 +1585,7 @@ describe("buildQuotaStatusReport", () => { anthropic: - cli_installed: true - cli_version: 1.2.3 + - binary_path: claude (PATH) - auth_status: authenticated - quota_supported: false - quota_source: (none) @@ -1519,6 +1609,7 @@ describe("buildQuotaStatusReport", () => { expect(titles).toMatchInlineSnapshot(` "toast: paths: +credential_source: openai: anthropic: cursor: diff --git a/tests/provider-surfaces.opencode-go.test.ts b/tests/provider-surfaces.opencode-go.test.ts index 83392feb..de129053 100644 --- a/tests/provider-surfaces.opencode-go.test.ts +++ b/tests/provider-surfaces.opencode-go.test.ts @@ -262,6 +262,48 @@ describe("OpenCode Go shared projections", () => { ); // The failed sign-in is resolved once, then not again within the minute. expect(integration.connection.resolve).toHaveBeenCalledOnce(); + expect(status).toContain("- console_error: refresh_failed: HTTP 401"); + expect(status).toContain("- go_source: legacy_key"); + }); + + it("shows the failed console sign-in on every surface when there is no Go key", async () => { + mocks.resolveOpenCodeGoAuthCached.mockResolvedValue({ state: "none" }); + mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValue({ + state: "none", + source: null, + checkedPaths: ["env:OPENCODE_API_KEY"], + credentialDatabasePaths: ["/tmp/opencode.db"], + }); + const integration = createFakeIntegration([ + { + integrationId: "opencode", + id: "cred_console", + label: "default", + registered: true, + method: "oauth", + value: { + type: "oauth", + methodID: "device", + access: "distinctive-console-access", + refresh: "distinctive-console-refresh", + expires: 0, + }, + resolveError: "HTTP 401", + }, + ]); + + const status = await runQuotaStatus("opencode-go-console-invalid-no-key", integration); + const output = await collectQuotaProjection(); + const message = + "OpenCode Console sign-in failed: refresh_failed: HTTP 401. Run `opencode auth login opencode`."; + + expect(status).toContain(`- live_error_1: ${message}`); + expect(output).toContain(message); + expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); + for (const secret of ["distinctive-console-access", "distinctive-console-refresh"]) { + expect(status).not.toContain(secret); + expect(output).not.toContain(secret); + } }); it("hides a not-subscribed result on every display and keeps it visible in safe diagnostics", async () => { diff --git a/tests/providers.anthropic.test.ts b/tests/providers.anthropic.test.ts index 7f66e3bc..4c729c12 100644 --- a/tests/providers.anthropic.test.ts +++ b/tests/providers.anthropic.test.ts @@ -136,7 +136,8 @@ describe("anthropic provider", () => { quotaSupported: true, quotaSource: "opencode-auth-oauth-api", oauthCredentialSource: "opencode-auth", - checkedCommands: ["claude --version"], + binaryPath: "/opt/homebrew/bin/claude", + checkedCommands: ["claude --version", "/opt/homebrew/bin/claude --version"], quota: { success: true, five_hour: { percentRemaining: 80 }, @@ -154,6 +155,10 @@ describe("anthropic provider", () => { key: "oauth_credential_source", value: "opencode-auth", }); + expect(out.statusDetails).toContainEqual({ + key: "binary_path", + value: "/opt/homebrew/bin/claude", + }); expect(out.statusDetails).toContainEqual({ key: "quota_source", value: "opencode-auth-oauth-api", @@ -174,6 +179,7 @@ describe("anthropic provider", () => { authStatus: "authenticated", quotaSupported: false, quotaSource: "none", + binaryPath: null, checkedCommands: ["claude --version"], }); (queryAnthropicQuota as any).mockResolvedValueOnce(null); @@ -183,6 +189,7 @@ describe("anthropic provider", () => { key: "oauth_credential_source", value: "(none)", }); + expect(out.statusDetails).toContainEqual({ key: "binary_path", value: "(none)" }); }); it("adds the Fable weekly row and diagnostic when the OAuth response reports it", async () => { diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index 500bd53b..88300468 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -243,6 +243,72 @@ describe("opencode-go provider", () => { expect(JSON.stringify(out)).not.toContain(jwt); }); + it("shows the failed Console sign-in as Go's error when there is no API key", async () => { + const jwt = `eyJ${"a".repeat(24)}.${"b".repeat(24)}.${"c".repeat(24)}`; + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "invalid", + error: `refresh_failed: HTTP 401 ${jwt}`, + }); + mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValueOnce(diagnostics("none")); + mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ state: "none" }); + + const out = await runFetch(); + + expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); + expect(out.attempted).toBe(true); + expect(out.entries).toEqual([]); + expect(out.errors).toEqual([ + { + label: "OpenCode Go", + message: + "OpenCode Console sign-in failed: refresh_failed: HTTP 401 [redacted]. Run `opencode auth login opencode`.", + }, + ]); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "console_auth_state", value: "invalid" }, + { key: "go_source", value: "legacy_key" }, + ]), + ); + expect(JSON.stringify(out)).not.toContain(jwt); + }); + + it("shows the failed Console request as Go's error when there is no API key", async () => { + consoleConfigured(); + mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ + success: false, + error: "OpenCode Console API error 500 (/api/go/status)", + retryable: true, + }); + mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValueOnce(diagnostics("none")); + mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ state: "none" }); + + const out = await runFetch(); + + expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); + expect(out.errors).toEqual([ + { + label: "OpenCode Go", + message: "OpenCode Console request failed: OpenCode Console API error 500 (/api/go/status)", + retryable: true, + }, + ]); + expect(out.statusDetails).toContainEqual({ + key: "console_error", + value: "OpenCode Console API error 500 (/api/go/status)", + }); + }); + + it("stays quiet without a Console sign-in or an API key", async () => { + mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValueOnce(diagnostics("none")); + mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ state: "none" }); + + const out = await runFetch(); + + expectNotAttempted(out); + expect(out.errors).toEqual([]); + }); + it("keeps the plain key path when the opencode login is an API key", async () => { mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ state: "none", reason: "not_oauth" }); @@ -843,6 +909,18 @@ describe("opencode-go availability and model matching", () => { expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); }); + it("stays available for a failed Console sign-in without an API key", async () => { + mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ + state: "invalid", + error: "refresh_failed: HTTP 401", + }); + mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ state: "none" }); + + await expect(opencodeGoProvider.isAvailable(createProviderAvailabilityContext())).resolves.toBe( + true, + ); + }); + it.each([ ["opencode-go/some-model", true], ["opencode-go-subscription/any", true], From 25c2773a58c91a05f12a47140af664253f9c2110 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 19:48:47 +0200 Subject: [PATCH 73/97] fix: address v5 review findings - Alibaba: bl's trusted PATH is checked against the project folder quota is computed for (the location's git root, passed to providers as workspaceRoot) instead of the service's working folder. The home folder never counts as a project, so ~/.local/bin and nvm installs work. - OpenCode Go: a Console 403 from /api/go/status means no Go subscription. Without a Go API key, only a broken Console sign-in shows an error row; a failed Console request stays quiet with console_error in the status. - Auth cache: a read in flight when logins change no longer refills the cache with the old login (generation counter). - Server log: a failed credential event subscription and failed surface, footer and writeExport RPCs now log a sanitized reason. Co-authored-by: Ran Sagy --- docs/readme/providers.md | 4 +- docs/readme/troubleshooting.md | 7 +- src/lib/alibaba-token-plan.ts | 59 +++++++++++++---- src/lib/cli-reports.ts | 1 + src/lib/entries.ts | 5 ++ src/lib/opencode-auth.ts | 10 ++- src/lib/opencode-go.ts | 10 ++- src/lib/quota-dialog-commands.ts | 12 +++- src/lib/quota-export.ts | 1 + src/lib/quota-render-data.ts | 5 ++ src/lib/quota-runtime-context.ts | 2 + src/lib/quota-surface-data.ts | 7 +- src/plugin.ts | 46 ++++++++++--- src/providers/alibaba-token-plan.ts | 1 + src/providers/opencode-go.ts | 22 +++---- tests/lib.alibaba-token-plan.test.ts | 77 ++++++++++++++++++++++ tests/lib.opencode-auth.test.ts | 25 +++++++ tests/lib.opencode-go.test.ts | 23 +++++++ tests/lib.quota-surface-data.test.ts | 3 + tests/plugin-rpc.test.ts | 55 +++++++++++++++- tests/plugin-v2.test.ts | 39 +++++++++++ tests/providers.alibaba-token-plan.test.ts | 18 +++++ tests/providers.opencode-go.test.ts | 33 +++++----- 23 files changed, 397 insertions(+), 68 deletions(-) diff --git a/docs/readme/providers.md b/docs/readme/providers.md index dbb329ed..50c5884a 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -435,7 +435,7 @@ npm install -g bailian-cli bl auth login --console ``` -OpenCode Quota runs only `bl usage token-plan --output json`. It does not install `bl`, open a login flow, read console cookies, or accept a custom command. macOS and Linux resolve `bl` from absolute directories on OpenCode's `PATH` that are outside its working folder. OpenCode's background service works in your home folder, so `bl` must live outside it (for example `/opt/homebrew/bin`, not `~/.local/bin` or an nvm folder). On Windows, use WSL. Native Windows `bl.exe` and `.cmd` shims are not supported in this release. +OpenCode Quota runs only `bl usage token-plan --output json`. It does not install `bl`, open a login flow, read console cookies, or accept a custom command. macOS and Linux resolve `bl` from absolute directories on OpenCode's `PATH`. Relative entries are ignored, and so is anything inside the project folder quota is shown for, including a `bl` that links into it. Your home folder does not count as a project, so `~/.local/bin` and nvm folders work. On Windows, use WSL. Native Windows `bl.exe` and `.cmd` shims are not supported in this release. Team plans, China-only `alibaba-token-plan-cn` runtimes, and cookie-based console scraping are out of scope. After you change the CLI's active console account, restart OpenCode or wait for the next live probe. `/quota_status` has an `alibaba_token_plan` live probe that stays separate from Alibaba Coding Plan diagnostics. @@ -606,7 +606,7 @@ Project-local `opencode.json` and `opencode.jsonc` files are not read for this s ### OpenCode Go -If you are signed in to the OpenCode Console in OpenCode 2, OpenCode Go first reads the Console's `/api/go/status`; if you are not signed in, the sign-in expired or cannot be read, or that call fails, it reads the official `https://opencode.ai/zen/go/v1/usage` API with your API key. Without an API key, a failed sign-in or Console call shows as an OpenCode Go error. If the Console reports no Go subscription, no Go rows appear. OpenCode Quota automatically resolves the API key in this order: +If you are signed in to the OpenCode Console in OpenCode 2, OpenCode Go first reads the Console's `/api/go/status`; if you are not signed in, the sign-in expired or cannot be read, or that call fails, it reads the official `https://opencode.ai/zen/go/v1/usage` API with your API key. Without an API key, a sign-in OpenCode cannot return shows as an OpenCode Go error, and a failed Console call shows no Go quota (`/quota_status` lists `console_error`). If the Console reports no Go subscription (including HTTP 403 or 404), no Go rows appear. OpenCode Quota automatically resolves the API key in this order: 1. `OPENCODE_API_KEY` 2. Trusted user/global OpenCode config: `provider.opencode-go.options.apiKey` diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index a5d484c4..5f59af94 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -169,7 +169,7 @@ Run `/quota_status` and check the `alibaba_token_plan` live probe. This source i | Symptom | Fix | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| CLI not detected | Install official `bailian-cli` so `bl` is in an absolute directory on the [service `PATH`](#service-environment) outside the service's working folder, your home folder (for example `/opt/homebrew/bin`, not `~/.local/bin` or an nvm folder). On Windows, run this from WSL. Native `bl.exe` and `.cmd` shims are not supported. | +| CLI not detected | Install official `bailian-cli` so `bl` is in an absolute directory on the [service `PATH`](#service-environment) that is not inside the project folder (for example `/opt/homebrew/bin`, `~/.local/bin`, or an nvm folder). Relative `PATH` entries are ignored. On Windows, run this from WSL. Native `bl.exe` and `.cmd` shims are not supported. | | Console session expired | Run `bl auth login --console`. A Coding Plan API key cannot authenticate this provider. | | Weekly row only | The official CLI may omit the five-hour window. OpenCode Quota does not invent a missing window. | | JSON export empty | `show --json` is cache-only. This provider is uncached, so a separate CLI process reports it unavailable instead of running `bl`. | @@ -252,13 +252,14 @@ Run `/quota_status` and check the `xiaomi` section. Diagnostics show state, sour
OpenCode Go -Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*` diagnostics, the selected display windows, normalized API usage, and `live_fetch_error` without exposing the API key. When you are signed in to the OpenCode Console, `console_auth_state` and `console_server` describe that sign-in, `go_source` shows whether the Console (`console`) or the API key (`legacy_key`) answered, and `console_error` shows why the Console failed. `opencode_go_state: not_subscribed` means the Console reports no Go subscription. +Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*` diagnostics, the selected display windows, normalized API usage, and `live_fetch_error` without exposing the API key. When you are signed in to the OpenCode Console, `console_auth_state` and `console_server` describe that sign-in, `go_source` shows whether the Console (`console`) or the API key (`legacy_key`) answered, and `console_error` shows why the Console failed. `opencode_go_state: not_subscribed` means the Console reports no Go subscription; an HTTP 403 or 404 from `/api/go/status` counts as that. | Symptom | Fix | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Provider not detected | Sign in with `opencode auth login opencode`, or set `OPENCODE_API_KEY`, trusted global `provider.opencode-go.options.apiKey`, fallback `provider.opencode.options.apiKey`, an `opencode-go` API-key login (`opencode auth login opencode-go`), or a legacy `opencode` API-key login as the final fallback. Then check `auth_state`, `auth_source`, and `auth_checked_paths`. | | `auth_state` is `invalid` | Log in again with `opencode auth login opencode-go`. A broken `opencode-go` login blocks the legacy `opencode` fallback and is reported in `auth_error`. | -| `OpenCode Console sign-in failed` or `OpenCode Console request failed` | Shown only when no API key is set. Run `opencode auth login opencode`, or set an API key. `console_error` has the reason. | +| `OpenCode Console sign-in failed` | Shown only when no API key is set. Run `opencode auth login opencode`, or set an API key. `console_error` has the reason. | +| No Go quota, `console_error` set | The Console request failed and no API key is set, so Go shows no quota (`Not configured` in manual mode). Check `console_error` and retry, or set an API key. | | API returns 401 or 403 | The usage API rejected the key. Update the winning source shown by `auth_source`, wait briefly for credential caching to expire, and rerun `/quota_status`. | | Invalid API response | Check `live_fetch_error`. OpenCode Quota requires valid 5h, Weekly, and Monthly results, so one missing or malformed API window rejects the full response instead of showing partial quota. | | API request times out or fails | Check `live_fetch_error`, confirm `https://opencode.ai/zen/go/v1/usage` is reachable, and retry. Increase `requestTimeoutMs` only when the error is a timeout. | diff --git a/src/lib/alibaba-token-plan.ts b/src/lib/alibaba-token-plan.ts index 087dc08f..c9cd5375 100644 --- a/src/lib/alibaba-token-plan.ts +++ b/src/lib/alibaba-token-plan.ts @@ -122,7 +122,8 @@ export type AlibabaTokenPlanExecutableResolution = export type AlibabaTokenPlanRuntime = { platform?: NodeJS.Platform; pathEnv?: string; - cwd?: string; + /** The project folder quota is computed for. */ + cwd: string; tmpdir?: string; homedir?: string; env?: NodeJS.ProcessEnv; @@ -202,14 +203,30 @@ function stripPathQuotes(value: string): string { return trimmed; } +/** + * The folder whose PATH entries and executables are not trusted: the project folder quota is + * computed for. For OpenCode opened at the home folder, and for the terminal command, that is + * the home folder. The home folder is not a project, so it gives none. + */ +function untrustedProjectFolder(params: { + cwd: string; + homedir?: string; + platform: NodeJS.Platform; +}): string | null { + const paths = runtimePath(params.platform); + const cwd = paths.resolve(params.cwd); + return cwd === paths.resolve(params.homedir ?? homedir()) ? null : cwd; +} + export function listTrustedPathDirectories(params: { pathEnv: string | undefined; cwd: string; + homedir?: string; platform: NodeJS.Platform; }): string[] { const paths = runtimePath(params.platform); const delimiter = paths.delimiter; - const cwd = paths.resolve(params.cwd); + const projectFolder = untrustedProjectFolder(params); const trusted: string[] = []; const seen = new Set(); @@ -219,7 +236,7 @@ export function listTrustedPathDirectories(params: { if (!paths.isAbsolute(entry)) continue; const resolved = paths.resolve(entry); - if (isPathInside(cwd, resolved, paths)) continue; + if (projectFolder !== null && isPathInside(projectFolder, resolved, paths)) continue; if (seen.has(resolved)) continue; seen.add(resolved); trusted.push(resolved); @@ -254,7 +271,8 @@ async function fileLooksLikeExecutable(file: string): Promise { export async function resolveAlibabaTokenPlanExecutable(params: { pathEnv?: string; - cwd?: string; + cwd: string; + homedir?: string; platform?: NodeJS.Platform; }): Promise { const platform = params.platform ?? process.platform; @@ -263,10 +281,15 @@ export async function resolveAlibabaTokenPlanExecutable(params: { } const paths = runtimePath(platform); - const cwd = paths.resolve(params.cwd ?? process.cwd()); + const projectFolder = untrustedProjectFolder({ + cwd: params.cwd, + homedir: params.homedir, + platform, + }); const trusted = listTrustedPathDirectories({ pathEnv: params.pathEnv ?? process.env.PATH, - cwd, + cwd: params.cwd, + homedir: params.homedir, platform, }); if (trusted.length === 0) { @@ -293,7 +316,7 @@ export async function resolveAlibabaTokenPlanExecutable(params: { sawShellLauncher = true; continue; } - if (isPathInside(cwd, resolved, paths)) { + if (projectFolder !== null && isPathInside(projectFolder, resolved, paths)) { return executableResolutionError("workspace_path_rejected"); } return { ok: true, file: resolved }; @@ -590,31 +613,39 @@ function mapSpawnFailure(result: AlibabaTokenPlanSpawnResult): AlibabaTokenPlanC return parseAlibabaTokenPlanUsageJson(result.stdout.toString("utf8")); } -export async function queryAlibabaTokenPlanQuota( - options: { requestTimeoutMs?: number; runtime?: AlibabaTokenPlanRuntime } = {}, -): Promise { - const runtime = options.runtime ?? {}; +export async function queryAlibabaTokenPlanQuota(options: { + requestTimeoutMs?: number; + runtime: AlibabaTokenPlanRuntime; +}): Promise { + const runtime = options.runtime; const platform = runtime.platform ?? process.platform; if (!isAlibabaTokenPlanSupportedPlatform(platform)) { return fail("unsupported_platform"); } - const cwd = runtimePath(platform).resolve(runtime.cwd ?? process.cwd()); + const cwd = runtimePath(platform).resolve(runtime.cwd); + const home = runtime.homedir ?? homedir(); const pathEnv = runtime.pathEnv ?? runtime.env?.PATH ?? process.env.PATH; const resolved = await resolveAlibabaTokenPlanExecutable({ pathEnv, cwd, + homedir: home, platform, }); if (!resolved.ok) { return { ok: false, error: resolved.error }; } - const trustedDirectories = listTrustedPathDirectories({ pathEnv, cwd, platform }); + const trustedDirectories = listTrustedPathDirectories({ + pathEnv, + cwd, + homedir: home, + platform, + }); const processCwd = resolveNonWorkspaceCwd({ cwd, tmpdir: runtime.tmpdir ?? tmpdir(), - homedir: runtime.homedir ?? homedir(), + homedir: home, platform, }); const env: NodeJS.ProcessEnv = { diff --git a/src/lib/cli-reports.ts b/src/lib/cli-reports.ts index 1c0d41de..65d8d9cd 100644 --- a/src/lib/cli-reports.ts +++ b/src/lib/cli-reports.ts @@ -63,6 +63,7 @@ export async function buildCliShowText(params: { config, configMeta: runtime.configMeta, request: createQuotaRuntimeRequestContext(runtime), + workspaceRoot: runtime.roots.workspaceRoot, surfaceExplicitProviderIssues: true, formatStyle: resolveQuotaFormatStyle(config.formatStyle), providers: runtime.providers, diff --git a/src/lib/entries.ts b/src/lib/entries.ts index 1f2993f3..fd164ef0 100644 --- a/src/lib/entries.ts +++ b/src/lib/entries.ts @@ -365,6 +365,11 @@ export interface QuotaProviderContext { }; }; resolveRuntimeProviderIds: RuntimeProviderIdResolver; + /** + * The project folder quota is computed for: the git root of the OpenCode location, or the + * location itself. The terminal command computes quota for the home folder. + */ + workspaceRoot: string; config: { anthropicBinaryPath?: string; cursorPlan: CursorQuotaPlan; diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index ef9f0c6c..078079b8 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -142,6 +142,8 @@ export function formatCredentialDisplayNames( /** Cached auth maps keyed by the sorted integration id list and the method filter. */ const authCache = new Map(); +/** Changes when the cache is dropped, so a read started before that does not refill it. */ +let authCacheGeneration = 0; /** * The database OpenCode keeps its logins in, for diagnostics only (logins are @@ -170,6 +172,7 @@ export function bindCredentialSource(source: CredentialSource): () => void { export function notifyCredentialsChanged(): void { credentialFailures.clear(); authCache.clear(); + authCacheGeneration += 1; lastListError = undefined; } @@ -511,12 +514,16 @@ export async function readAuthFileCached(params: { return cached.inFlight; } + const generation = authCacheGeneration; const inFlight = (async () => { const value = await readAuthFile({ integrationIds: params.integrationIds, methods: params.methods, }); - authCache.set(cacheKey, { timestamp: Date.now(), value }); + // A login change during the read may make this value stale: return it, but do not cache it. + if (generation === authCacheGeneration) { + authCache.set(cacheKey, { timestamp: Date.now(), value }); + } return value; })(); @@ -539,4 +546,5 @@ export async function readAuthFileCached(params: { /** Test helper to clear cached auth state between test cases. */ export function clearReadAuthFileCacheForTests(): void { authCache.clear(); + authCacheGeneration += 1; } diff --git a/src/lib/opencode-go.ts b/src/lib/opencode-go.ts index 453d9555..ebec5861 100644 --- a/src/lib/opencode-go.ts +++ b/src/lib/opencode-go.ts @@ -263,14 +263,12 @@ export async function queryOpenCodeGoConsoleStatus( timeoutMs: options.requestTimeoutMs, consume: async (response) => { if (!response.ok) { - // 404 on this member-scoped route is read as "no Go subscription". - // 403 is kept ambiguous (it could be an access/permission issue on - // the console side rather than a subscription state), so callers - // fall back instead of showing a not-subscribed state. - if (response.status === 404) { + // 404 and 403 on this member-scoped route are read as "no Go subscription", + // as the usage API's 403 EntitlementError is. + if (response.status === 404 || response.status === 403) { return { success: false, - error: "OpenCode Go subscription not found for this console account (404)", + error: `OpenCode Go subscription not found for this console account (${response.status})`, notSubscribed: true, }; } diff --git a/src/lib/quota-dialog-commands.ts b/src/lib/quota-dialog-commands.ts index ff81123e..d162d91b 100644 --- a/src/lib/quota-dialog-commands.ts +++ b/src/lib/quota-dialog-commands.ts @@ -138,6 +138,7 @@ async function fetchQuotaCommandData(params: { config: runtime.config, configMeta: runtime.configMeta, request, + workspaceRoot: runtime.roots.workspaceRoot, surfaceExplicitProviderIssues: false, formatStyle: ALL_WINDOWS_FORMAT_STYLE, providers: runtime.providers, @@ -307,7 +308,10 @@ export async function buildStatusReportData(params: { const providers = params.providerFilterId ? params.runtime.providers.filter((provider) => provider.id === params.providerFilterId) : params.runtime.providers; - const providerContext = createQuotaProviderRuntimeContext(params.runtime); + const providerContext = createQuotaProviderRuntimeContext({ + ...params.runtime, + workspaceRoot: params.runtime.roots.workspaceRoot, + }); const availability = await Promise.all( providers.map(async (p) => { let ok = false; @@ -353,6 +357,7 @@ export async function buildStatusReportData(params: { config: runtimeConfig, configMeta: params.runtime.configMeta, request: createQuotaRuntimeRequestContext(params.runtime), + workspaceRoot: params.runtime.roots.workspaceRoot, providers: liveProbeProviders, }); } catch (error) { @@ -528,7 +533,10 @@ async function buildQuotaAnnouncementsCommandOutput(runtime: QuotaRuntimeContext if (runtime.config.enabled && runtime.config.maintainerAnnouncements.enabled) { const providerIds = await collectConcreteEnabledProviderIds({ providers: runtime.providers, - ctx: createQuotaProviderRuntimeContext(runtime), + ctx: createQuotaProviderRuntimeContext({ + ...runtime, + workspaceRoot: runtime.roots.workspaceRoot, + }), enabledProviders: runtime.config.enabledProviders, }); const summary = getMaintainerAnnouncementsSummary({ diff --git a/src/lib/quota-export.ts b/src/lib/quota-export.ts index 568b4742..f9b76a71 100644 --- a/src/lib/quota-export.ts +++ b/src/lib/quota-export.ts @@ -54,6 +54,7 @@ const EXPORT_WINDOW_LABELS: Readonly> = { export function createExportProviderContext(runtime: QuotaRuntimeContext): QuotaProviderContext { return createQuotaProviderRuntimeContext({ ...runtime, + workspaceRoot: runtime.roots.workspaceRoot, config: { ...runtime.config, onlyCurrentModel: false, diff --git a/src/lib/quota-render-data.ts b/src/lib/quota-render-data.ts index c619eeda..be97b048 100644 --- a/src/lib/quota-render-data.ts +++ b/src/lib/quota-render-data.ts @@ -179,6 +179,7 @@ export async function resolveQuotaRenderSelection(params: { client: QuotaProviderContext["client"]; config: QuotaToastConfig; request?: QuotaRequestContext; + workspaceRoot: string; configMeta?: Pick; providers?: QuotaProvider[]; resolveRuntimeProviderIds?: RuntimeProviderIdResolver; @@ -197,6 +198,7 @@ export async function resolveQuotaRenderSelection(params: { configMeta: params.configMeta, resolveRuntimeProviderIds: params.resolveRuntimeProviderIds ?? createRuntimeProviderIdResolver(client), + workspaceRoot: params.workspaceRoot, session: { sessionMeta: { modelID: currentModel, @@ -304,6 +306,7 @@ export async function collectQuotaStatusLiveProbes(params: { client: QuotaProviderContext["client"]; config: QuotaToastConfig; request?: QuotaRequestContext; + workspaceRoot: string; configMeta?: Pick; providers: QuotaProvider[]; resolveRuntimeProviderIds?: RuntimeProviderIdResolver; @@ -325,6 +328,7 @@ export async function collectQuotaStatusLiveProbes(params: { configMeta: params.configMeta, resolveRuntimeProviderIds: params.resolveRuntimeProviderIds ?? createRuntimeProviderIdResolver(params.client), + workspaceRoot: params.workspaceRoot, session: { sessionMeta: { modelID: currentModel, @@ -459,6 +463,7 @@ export async function collectQuotaRenderData(params: { client: QuotaProviderContext["client"]; config: QuotaToastConfig; request?: QuotaRequestContext; + workspaceRoot: string; surfaceExplicitProviderIssues: boolean; formatStyle?: QuotaFormatStyle; configMeta?: Pick; diff --git a/src/lib/quota-runtime-context.ts b/src/lib/quota-runtime-context.ts index 0b07e62d..3b1e37e6 100644 --- a/src/lib/quota-runtime-context.ts +++ b/src/lib/quota-runtime-context.ts @@ -117,6 +117,7 @@ export function createQuotaProviderRuntimeContext(runtime: { config: QuotaToastConfig; session: QuotaRuntimeContext["session"]; resolveRuntimeProviderIds: RuntimeProviderIdResolver; + workspaceRoot: string; configMeta?: Pick; configureTelemetry?: boolean; }): QuotaProviderContext { @@ -126,6 +127,7 @@ export function createQuotaProviderRuntimeContext(runtime: { return { client: runtime.client, resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds, + workspaceRoot: runtime.workspaceRoot, config: { anthropicBinaryPath: runtime.config.anthropicBinaryPath, cursorPlan: runtime.config.cursorPlan, diff --git a/src/lib/quota-surface-data.ts b/src/lib/quota-surface-data.ts index 3ad16f1d..72110a6b 100644 --- a/src/lib/quota-surface-data.ts +++ b/src/lib/quota-surface-data.ts @@ -114,6 +114,7 @@ export async function getQuotaMessage( config, configMeta: runtime.configMeta, request: createQuotaRuntimeRequestContext(runtime), + workspaceRoot: runtime.roots.workspaceRoot, surfaceExplicitProviderIssues: true, formatStyle, providers: runtime.providers, @@ -247,7 +248,10 @@ async function getHomeAnnouncementText(runtime: QuotaRuntimeContext): Promise(messages: M[]): M[] { }); } +/** An error as one line that is safe to log: tokens redacted, at most 120 characters. */ +function logReason(error: unknown): string { + return scrubCredentialErrorText(error instanceof Error ? error.message : String(error)); +} + +/** + * Runs an RPC handler and logs its failure in the OpenCode service before rethrowing it. + * The client gets only OpenCode's `rpc.internal` error, so the reason shows only here. + */ +async function logRpcFailure(method: string, handler: () => Promise): Promise { + try { + return await handler(); + } catch (error) { + console.warn(`[opencode-quota] ${method} RPC failed: ${logReason(error)}`); + throw error; + } +} + export const QuotaToastPlugin = Plugin.define({ id: "@slkiser/opencode-quota.server", async setup(ctx) { @@ -132,13 +151,18 @@ export const QuotaToastPlugin = Plugin.define({ // Awaited so the plugin is active only once the RPC is registered: OpenCode waits for // activation before it routes an RPC call, so the first TUI call cannot miss it. await ctx.rpc.register(QuotaRpc, { - surface: async (input) => ({ - quota: (await getQuotaMessage(surfaceHost, input.sessionID, input.surface)) ?? null, - }), - footer: async (input) => ({ - lines: await getQuotaFooter(surfaceHost, input.sessionID, input.surface), - }), - writeExport: async () => ({ written: await writeQuotaExportIfEnabled(surfaceHost) }), + surface: (input) => + logRpcFailure("surface", async () => ({ + quota: (await getQuotaMessage(surfaceHost, input.sessionID, input.surface)) ?? null, + })), + footer: (input) => + logRpcFailure("footer", async () => ({ + lines: await getQuotaFooter(surfaceHost, input.sessionID, input.surface), + })), + writeExport: () => + logRpcFailure("writeExport", async () => ({ + written: await writeQuotaExportIfEnabled(surfaceHost), + })), command: async (input): Promise => { try { return await buildOutput(input.command, input.sessionID, input.arguments, { @@ -273,7 +297,8 @@ export const QuotaToastPlugin = Plugin.define({ createIntegrationCredentialSource(ctx.integration), ); // A login added, changed or switched in OpenCode drops cached logins and failed-login - // entries, so the next read asks OpenCode again. + // entries, so the next read asks OpenCode again. If the subscription fails, cached logins + // stay until they expire; the log says why. The cleanup's abort ends it silently. const credentialEvents = new AbortController(); void (async () => { for await (const event of ctx.event.subscribe({ signal: credentialEvents.signal })) { @@ -281,7 +306,10 @@ export const QuotaToastPlugin = Plugin.define({ notifyCredentialsChanged(); } } - })().catch(() => {}); + })().catch((error) => { + if (credentialEvents.signal.aborted) return; + console.warn(`[opencode-quota] credential event subscription stopped: ${logReason(error)}`); + }); return () => { credentialEvents.abort(); diff --git a/src/providers/alibaba-token-plan.ts b/src/providers/alibaba-token-plan.ts index 9797879b..1ef47b89 100644 --- a/src/providers/alibaba-token-plan.ts +++ b/src/providers/alibaba-token-plan.ts @@ -110,6 +110,7 @@ export const alibabaTokenPlanProvider: QuotaProvider = { async fetch(ctx: QuotaProviderContext): Promise { const result = await queryAlibabaTokenPlanQuota({ requestTimeoutMs: ctx.config?.requestTimeoutMs ?? REQUEST_TIMEOUT_MS, + runtime: { cwd: ctx.workspaceRoot }, }); return withStatusDetails( mapClosedResult(result), diff --git a/src/providers/opencode-go.ts b/src/providers/opencode-go.ts index 4f87415e..22ba1639 100644 --- a/src/providers/opencode-go.ts +++ b/src/providers/opencode-go.ts @@ -167,7 +167,8 @@ export const opencodeGoProvider: QuotaProvider = { ]); } // Console request failed; fall back to the legacy API-key path below - // and surface the console error in diagnostics. + // and surface the console error in diagnostics. Without an API key Go + // stays quiet: the sign-in works, only this request failed. const diagnostics = await getOpenCodeGoAuthDiagnostics({ maxAgeMs: DEFAULT_OPENCODE_GO_AUTH_CACHE_MAX_AGE_MS, }); @@ -178,10 +179,7 @@ export const opencodeGoProvider: QuotaProvider = { { key: "go_source", value: "legacy_key" }, { key: "selected_windows", value: windows.join(",") }, ]; - return await fetchOpenCodeGoLegacy(ctx, diagnostics, legacyStatusDetails, { - message: `OpenCode Console request failed: ${scrubCredentialErrorText(consoleResult.error)}`, - retryable: consoleResult.retryable, - }); + return await fetchOpenCodeGoLegacy(ctx, diagnostics, legacyStatusDetails); } if (consoleAuth.state === "expired") { @@ -214,7 +212,7 @@ export const opencodeGoProvider: QuotaProvider = { { key: "go_source", value: "legacy_key" }, { key: "selected_windows", value: windows.join(",") }, ], - { message: `OpenCode Console sign-in failed: ${consoleError}. ${CONSOLE_LOGIN_HINT}` }, + `OpenCode Console sign-in failed: ${consoleError}. ${CONSOLE_LOGIN_HINT}`, ); } @@ -229,14 +227,14 @@ export const opencodeGoProvider: QuotaProvider = { }; /** - * The API-key path. `consoleFailure` is set when the Console sign-in or request - * failed first; without an API key it becomes Go's one error row. + * The API-key path. `consoleSignInFailure` is set when OpenCode could not return + * the Console sign-in; without an API key it becomes Go's one error row. */ async function fetchOpenCodeGoLegacy( ctx: QuotaProviderContext, diagnostics: OpenCodeGoAuthDiagnostics, statusDetails?: QuotaProviderResult["statusDetails"], - consoleFailure?: { message: string; retryable?: boolean }, + consoleSignInFailure?: string, ): Promise { const windows = ctx.config.opencodeGoWindows ?? OPENCODE_GO_WINDOW_ORDER; const baseStatusDetails = statusDetails ?? []; @@ -246,11 +244,9 @@ async function fetchOpenCodeGoLegacy( if (auth.state === "none") { notSubscribedCredentialFingerprints.clear(); - if (consoleFailure) { + if (consoleSignInFailure) { return withStatusDetails( - attemptedErrorResult(OPENCODE_GO_PROVIDER_LABEL, consoleFailure.message, { - retryable: consoleFailure.retryable, - }), + attemptedErrorResult(OPENCODE_GO_PROVIDER_LABEL, consoleSignInFailure), baseStatusDetails, ); } diff --git a/tests/lib.alibaba-token-plan.test.ts b/tests/lib.alibaba-token-plan.test.ts index 43992e46..8109ce3e 100644 --- a/tests/lib.alibaba-token-plan.test.ts +++ b/tests/lib.alibaba-token-plan.test.ts @@ -331,6 +331,62 @@ describe("alibaba token plan PATH trust", () => { expect(dirs).toEqual([trusted]); }); + it("trusts PATH directories under the home folder when quota is computed for home", () => { + const home = "/home/user"; + const pathEnv = [ + "/home/user/.local/bin", + "/home/user/.nvm/versions/node/v22.0.0/bin", + "relative", + ".", + "..", + "/usr/bin", + ].join(":"); + + expect( + listTrustedPathDirectories({ platform: "linux", cwd: home, homedir: home, pathEnv }), + ).toEqual(["/home/user/.local/bin", "/home/user/.nvm/versions/node/v22.0.0/bin", "/usr/bin"]); + }); + + it("still ignores PATH directories inside a project folder under home", () => { + const pathEnv = "/home/user/project/node_modules/.bin:/home/user/.local/bin:/usr/bin"; + + expect( + listTrustedPathDirectories({ + platform: "linux", + cwd: "/home/user/project", + homedir: "/home/user", + pathEnv, + }), + ).toEqual(["/home/user/.local/bin", "/usr/bin"]); + }); + + it("accepts a bl that resolves under home when quota is computed for home", async () => { + const target = "/home/user/.nvm/versions/node/v22.0.0/lib/node_modules/bailian-cli/bin/bl"; + await mockExecutable("/opt/homebrew/bin/bl", target); + + expect( + await resolveAlibabaTokenPlanExecutable({ + platform: "darwin", + cwd: "/home/user", + homedir: "/home/user", + pathEnv: "/opt/homebrew/bin", + }), + ).toEqual({ ok: true, file: target }); + }); + + it("rejects a bl that resolves into a project folder under home", async () => { + await mockExecutable("/opt/homebrew/bin/bl", "/home/user/project/node_modules/.bin/bl"); + + expect( + await resolveAlibabaTokenPlanExecutable({ + platform: "darwin", + cwd: "/home/user/project", + homedir: "/home/user", + pathEnv: "/opt/homebrew/bin", + }), + ).toMatchObject({ ok: false, error: { kind: "workspace_path_rejected" } }); + }); + it.each([ "bin", "..bin", @@ -460,6 +516,27 @@ describe("alibaba token plan process boundary", () => { expect(requests[0]?.killGraceMs).toBe(ALIBABA_TOKEN_PLAN_KILL_GRACE_MS); }); + it("runs a bl from the home folder's PATH when quota is computed for home", async () => { + const bin = "/home/user/.local/bin"; + await mockExecutable(`${bin}/bl`); + const requests: AlibabaTokenPlanSpawnRequest[] = []; + const result = await queryAlibabaTokenPlanQuota({ + runtime: { + platform: "linux", + cwd: "/home/user", + homedir: "/home/user", + pathEnv: `${bin}:.`, + tmpdir: "/safe-tmp", + env: {}, + spawn: capturedSpawn(requests), + }, + }); + expect(result.ok).toBe(true); + expect(requests[0]?.file).toBe(`${bin}/${ALIBABA_TOKEN_PLAN_COMMAND}`); + expect(requests[0]?.env.PATH).toBe(bin); + expect(requests[0]?.cwd).toBe("/safe-tmp"); + }); + it("uses the injected environment PATH when pathEnv is omitted", async () => { await mockExecutable(); const requests: AlibabaTokenPlanSpawnRequest[] = []; diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 54df5796..91ed9c52 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -655,6 +655,31 @@ describe("readAuthFile and readAuthFileCached", () => { ).resolves.toEqual({ openai: { type: "oauth", access: "switched-access" } }); }); + it("does not cache a read that was in flight when the logins changed", async () => { + const openaiRow = (access: string) => ({ + id: "cred_openai", + integrationId: "openai", + label: "default", + active: true, + value: { type: "oauth", access }, + }); + let releaseOldRead!: (rows: ReturnType[]) => void; + let rows = new Promise[]>((resolve) => { + releaseOldRead = resolve; + }); + unbinds.push(bindCredentialSource({ readRows: () => rows })); + + const oldRead = readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai"] }); + notifyCredentialsChanged(); + releaseOldRead([openaiRow("old-access")]); + await expect(oldRead).resolves.toEqual({ openai: { type: "oauth", access: "old-access" } }); + rows = Promise.resolve([openaiRow("new-access")]); + + await expect( + readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai"] }), + ).resolves.toEqual({ openai: { type: "oauth", access: "new-access" } }); + }); + it("shares one in-flight read per id list", async () => { bindFakeIntegration([ keyCredential("deepseek", "cred_deepseek", { value: { type: "key", key: "deepseek-key" } }), diff --git a/tests/lib.opencode-go.test.ts b/tests/lib.opencode-go.test.ts index c67d3ce3..378abb47 100644 --- a/tests/lib.opencode-go.test.ts +++ b/tests/lib.opencode-go.test.ts @@ -510,4 +510,27 @@ describe("queryOpenCodeGoConsoleStatus", () => { }, ); }); + + it.each([404, 403])("reads HTTP %s as no Go subscription", async (status) => { + mocks.fetchResponse.mockResolvedValueOnce({ ok: false, status }); + + await expect(queryOpenCodeGoConsoleStatus({ accessToken: "console-access" })).resolves.toEqual({ + success: false, + error: `OpenCode Go subscription not found for this console account (${status})`, + notSubscribed: true, + }); + }); + + it.each([ + [401, false], + [500, true], + ])("keeps HTTP %s as a failed Console request", async (status, retryable) => { + mocks.fetchResponse.mockResolvedValueOnce({ ok: false, status }); + + await expect(queryOpenCodeGoConsoleStatus({ accessToken: "console-access" })).resolves.toEqual({ + success: false, + error: `OpenCode Console API error ${status} (/api/go/status)`, + retryable, + }); + }); }); diff --git a/tests/lib.quota-surface-data.test.ts b/tests/lib.quota-surface-data.test.ts index 542ac415..3154f18b 100644 --- a/tests/lib.quota-surface-data.test.ts +++ b/tests/lib.quota-surface-data.test.ts @@ -133,6 +133,7 @@ describe("quota surface data", () => { configMeta: {}, providers: [], resolveRuntimeProviderIds: vi.fn(), + roots: { workspaceRoot: "/project", configRoot: "/project" }, session: { sessionID: "session-1" }, } as never); const collect = vi.spyOn(renderDataModule, "collectQuotaRenderData").mockImplementation( @@ -205,6 +206,7 @@ describe("quota surface data", () => { configMeta: {}, providers: [], resolveRuntimeProviderIds: vi.fn(), + roots: { workspaceRoot: "/project", configRoot: "/project" }, session: { sessionID: "ses_1", sessionMeta: { modelID: "m", providerID: "p" } }, } as never); const collect = vi.spyOn(renderDataModule, "collectQuotaRenderData").mockResolvedValue({ @@ -218,6 +220,7 @@ describe("quota surface data", () => { const params = collect.mock.calls[0][0]; expect(params.config).toMatchObject({ onlyCurrentModel: false, showSessionTokens: false }); expect(params.request).toEqual({ sessionID: undefined, sessionMeta: undefined }); + expect(params.workspaceRoot).toBe("/project"); }); it("computes the count-only Home announcement from the enabled providers", async () => { diff --git a/tests/plugin-rpc.test.ts b/tests/plugin-rpc.test.ts index e36d0a1d..e3b71f53 100644 --- a/tests/plugin-rpc.test.ts +++ b/tests/plugin-rpc.test.ts @@ -80,12 +80,12 @@ function useConfig(overrides: Partial): void { ); } -async function setupServer(sessionGet = vi.fn().mockResolvedValue({})) { +async function setupServer(sessionGet = vi.fn().mockResolvedValue({}), directory = process.cwd()) { const { default: server } = await import("../src/plugin.js"); const register = vi.fn(async () => ({ dispose: async () => {}, events: { emit: vi.fn() } })); const commands: RegisteredCommand[] = []; const ctx = { - location: { directory: process.cwd() }, + location: { directory }, provider: { list: vi.fn().mockResolvedValue({ data: [{ id: "copilot" }] }) }, session: { get: sessionGet, wait: vi.fn(), prompt: vi.fn(), hook: vi.fn() }, tool: { transform: vi.fn() }, @@ -376,4 +376,55 @@ describe("server quota RPC", () => { await expect(missing.call("surface", input)).resolves.toEqual(expected); expect(sessionGet).toHaveBeenCalledWith({ sessionID: "missing" }); }); + + it("computes quota for the location's project folder, not the service's working folder", async () => { + useConfig({ + tuiCompactStatus: { enabled: true, homeBottom: true, sessionPrompt: false, maxWidth: 96 }, + }); + const workspaceRoots: string[] = []; + mocks.getProviders.mockReturnValue([ + { + id: "copilot", + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn(async (ctx: { workspaceRoot: string }) => { + workspaceRoots.push(ctx.workspaceRoot); + return { + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 81 }], + errors: [], + }; + }), + }, + ]); + const project = `${TEST_RUNTIME_ROOT}/project`; + const { call } = await setupServer(undefined, project); + + await call("surface", { surface: "sidebar", sessionID: "session-1" }); + await call("footer", { surface: "home" }); + await call("command", { command: "quota", sessionID: "session-1" }); + await call("cli", { command: "show" }); + + expect(workspaceRoots).toHaveLength(4); + expect(new Set(workspaceRoots)).toEqual(new Set([project])); + expect(project).not.toBe(process.cwd()); + }); + + it("logs why a surface, footer or export RPC failed, without tokens, and still fails it", async () => { + const token = `eyJ${"a".repeat(24)}.${"b".repeat(24)}.${"c".repeat(24)}`; + mocks.loadConfig.mockRejectedValue(new Error(`config broke ${token}`)); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { handlers } = await setupServer(); + + for (const [method, input] of [ + ["surface", { surface: "sidebar", sessionID: "session-1" }], + ["footer", { surface: "home" }], + ["writeExport", {}], + ] as const) { + await expect(handlers[method](input, callContext)).rejects.toThrow("config broke"); + expect(warn).toHaveBeenLastCalledWith( + `[opencode-quota] ${method} RPC failed: config broke [redacted]`, + ); + } + expect(JSON.stringify(warn.mock.calls)).not.toContain(token); + }); }); diff --git a/tests/plugin-v2.test.ts b/tests/plugin-v2.test.ts index f1b8d678..e476346e 100644 --- a/tests/plugin-v2.test.ts +++ b/tests/plugin-v2.test.ts @@ -360,4 +360,43 @@ describe("V2 server plugin", () => { ); await cleanup?.(); }); + + it("logs why the credential event subscription stopped, without tokens", async () => { + const { ctx } = createContext(); + const token = `eyJ${"a".repeat(24)}.${"b".repeat(24)}.${"c".repeat(24)}`; + const subscribe = vi.fn(() => ({ + // biome-ignore lint/correctness/useYield: the stream fails before its first event + async *[Symbol.asyncIterator]() { + throw new Error(`event stream closed ${token}`); + }, + })); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const cleanup = await plugin.setup({ ...ctx, event: { subscribe } } as never); + await vi.waitFor(() => + expect(warn).toHaveBeenCalledWith( + "[opencode-quota] credential event subscription stopped: event stream closed [redacted]", + ), + ); + await cleanup?.(); + warn.mockRestore(); + }); + + it("stops the credential event subscription silently on cleanup", async () => { + const { ctx } = createContext(); + const subscribe = vi.fn(({ signal }: { signal: AbortSignal }) => ({ + async *[Symbol.asyncIterator]() { + await new Promise((resolve) => signal.addEventListener("abort", resolve)); + throw new DOMException("This operation was aborted", "AbortError"); + }, + })); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const cleanup = await plugin.setup({ ...ctx, event: { subscribe } } as never); + await cleanup?.(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(warn.mock.calls.flat().join("\n")).not.toContain("credential event subscription"); + warn.mockRestore(); + }); }); diff --git a/tests/providers.alibaba-token-plan.test.ts b/tests/providers.alibaba-token-plan.test.ts index 30d0073b..4754d30e 100644 --- a/tests/providers.alibaba-token-plan.test.ts +++ b/tests/providers.alibaba-token-plan.test.ts @@ -94,6 +94,24 @@ describe("alibaba-token-plan provider", () => { ); }); + it("probes bl for the project folder quota is computed for", async () => { + const { queryAlibabaTokenPlanQuota } = await import("../src/lib/alibaba-token-plan.js"); + vi.mocked(queryAlibabaTokenPlanQuota).mockResolvedValue({ + ok: true, + weekly: { percentRemaining: 60 }, + }); + + await alibabaTokenPlanProvider.fetch({ + workspaceRoot: "/home/user/project", + config: { requestTimeoutMs: 5000 }, + } as never); + + expect(queryAlibabaTokenPlanQuota).toHaveBeenLastCalledWith({ + requestTimeoutMs: 5000, + runtime: { cwd: "/home/user/project" }, + }); + }); + it("returns a fixed attempted setup failure when the CLI is absent", async () => { const { queryAlibabaTokenPlanQuota } = await import("../src/lib/alibaba-token-plan.js"); vi.mocked(queryAlibabaTokenPlanQuota).mockResolvedValue({ diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index 88300468..6be9303e 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -166,11 +166,13 @@ describe("opencode-go provider", () => { ]); }); - it("reports a console not-subscribed state without errors", async () => { + it.each([ + 404, 403, + ])("reports a console not-subscribed state (%s) without errors", async (status) => { consoleConfigured(); mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ success: false, - error: "OpenCode Go subscription not found for this console account (404)", + error: `OpenCode Go subscription not found for this console account (${status})`, notSubscribed: true, }); @@ -273,11 +275,15 @@ describe("opencode-go provider", () => { expect(JSON.stringify(out)).not.toContain(jwt); }); - it("shows the failed Console request as Go's error when there is no API key", async () => { + it.each([ + ["a 5xx answer", "OpenCode Console API error 500 (/api/go/status)"], + ["a network failure", "fetch failed"], + ["a timeout", "Request timeout after 5s"], + ])("stays quiet after %s from the Console when there is no API key", async (_case, error) => { consoleConfigured(); mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ success: false, - error: "OpenCode Console API error 500 (/api/go/status)", + error, retryable: true, }); mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValueOnce(diagnostics("none")); @@ -286,17 +292,14 @@ describe("opencode-go provider", () => { const out = await runFetch(); expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); - expect(out.errors).toEqual([ - { - label: "OpenCode Go", - message: "OpenCode Console request failed: OpenCode Console API error 500 (/api/go/status)", - retryable: true, - }, - ]); - expect(out.statusDetails).toContainEqual({ - key: "console_error", - value: "OpenCode Console API error 500 (/api/go/status)", - }); + expectNotAttempted(out); + expect(out.errors).toEqual([]); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "console_auth_state", value: "configured" }, + { key: "console_error", value: error }, + ]), + ); }); it("stays quiet without a Console sign-in or an API key", async () => { From 941ad418d0162ac06ac9d87c0a35a593b3d2ad52 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Sun, 27 Sep 2026 19:52:38 +0200 Subject: [PATCH 74/97] fix(go): keep a Console 403 falling back to the Go API key 25c2773a5 read a Console 403 from /api/go/status as no subscription, which stopped users with a Go API key from falling back to it; only 404 means no subscription again. Co-authored-by: Ran Sagy --- docs/readme/providers.md | 2 +- docs/readme/troubleshooting.md | 4 +-- src/lib/opencode-go.ts | 10 +++--- tests/lib.opencode-go.test.ts | 7 ++-- tests/providers.opencode-go.test.ts | 53 ++++++++++++++++++++++++++--- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/docs/readme/providers.md b/docs/readme/providers.md index 50c5884a..b9f8dde1 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -606,7 +606,7 @@ Project-local `opencode.json` and `opencode.jsonc` files are not read for this s ### OpenCode Go -If you are signed in to the OpenCode Console in OpenCode 2, OpenCode Go first reads the Console's `/api/go/status`; if you are not signed in, the sign-in expired or cannot be read, or that call fails, it reads the official `https://opencode.ai/zen/go/v1/usage` API with your API key. Without an API key, a sign-in OpenCode cannot return shows as an OpenCode Go error, and a failed Console call shows no Go quota (`/quota_status` lists `console_error`). If the Console reports no Go subscription (including HTTP 403 or 404), no Go rows appear. OpenCode Quota automatically resolves the API key in this order: +If you are signed in to the OpenCode Console in OpenCode 2, OpenCode Go first reads the Console's `/api/go/status`; if you are not signed in, the sign-in expired or cannot be read, or that call fails, it reads the official `https://opencode.ai/zen/go/v1/usage` API with your API key. Without an API key, a sign-in OpenCode cannot return shows as an OpenCode Go error, and a failed Console call shows no Go quota (`/quota_status` lists `console_error`). If the Console reports no Go subscription (for example HTTP 404), no Go rows appear. An HTTP 403 counts as a failed Console call. OpenCode Quota automatically resolves the API key in this order: 1. `OPENCODE_API_KEY` 2. Trusted user/global OpenCode config: `provider.opencode-go.options.apiKey` diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 5f59af94..030c11f5 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -252,14 +252,14 @@ Run `/quota_status` and check the `xiaomi` section. Diagnostics show state, sour
OpenCode Go -Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*` diagnostics, the selected display windows, normalized API usage, and `live_fetch_error` without exposing the API key. When you are signed in to the OpenCode Console, `console_auth_state` and `console_server` describe that sign-in, `go_source` shows whether the Console (`console`) or the API key (`legacy_key`) answered, and `console_error` shows why the Console failed. `opencode_go_state: not_subscribed` means the Console reports no Go subscription; an HTTP 403 or 404 from `/api/go/status` counts as that. +Run `/quota_status` and check the `opencode_go` section. It reports safe `auth_*` diagnostics, the selected display windows, normalized API usage, and `live_fetch_error` without exposing the API key. When you are signed in to the OpenCode Console, `console_auth_state` and `console_server` describe that sign-in, `go_source` shows whether the Console (`console`) or the API key (`legacy_key`) answered, and `console_error` shows why the Console failed. `opencode_go_state: not_subscribed` means the Console reports no Go subscription (for example HTTP 404 from `/api/go/status`). An HTTP 403 is a failed Console request: Go uses the API key if one is set, otherwise it stays quiet with `console_error` set. | Symptom | Fix | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Provider not detected | Sign in with `opencode auth login opencode`, or set `OPENCODE_API_KEY`, trusted global `provider.opencode-go.options.apiKey`, fallback `provider.opencode.options.apiKey`, an `opencode-go` API-key login (`opencode auth login opencode-go`), or a legacy `opencode` API-key login as the final fallback. Then check `auth_state`, `auth_source`, and `auth_checked_paths`. | | `auth_state` is `invalid` | Log in again with `opencode auth login opencode-go`. A broken `opencode-go` login blocks the legacy `opencode` fallback and is reported in `auth_error`. | | `OpenCode Console sign-in failed` | Shown only when no API key is set. Run `opencode auth login opencode`, or set an API key. `console_error` has the reason. | -| No Go quota, `console_error` set | The Console request failed and no API key is set, so Go shows no quota (`Not configured` in manual mode). Check `console_error` and retry, or set an API key. | +| No Go quota, `console_error` set | The Console request failed (for example HTTP 403) and no API key is set, so Go shows no quota (`Not configured` in manual mode). Check `console_error` and retry, or set an API key. | | API returns 401 or 403 | The usage API rejected the key. Update the winning source shown by `auth_source`, wait briefly for credential caching to expire, and rerun `/quota_status`. | | Invalid API response | Check `live_fetch_error`. OpenCode Quota requires valid 5h, Weekly, and Monthly results, so one missing or malformed API window rejects the full response instead of showing partial quota. | | API request times out or fails | Check `live_fetch_error`, confirm `https://opencode.ai/zen/go/v1/usage` is reachable, and retry. Increase `requestTimeoutMs` only when the error is a timeout. | diff --git a/src/lib/opencode-go.ts b/src/lib/opencode-go.ts index ebec5861..453d9555 100644 --- a/src/lib/opencode-go.ts +++ b/src/lib/opencode-go.ts @@ -263,12 +263,14 @@ export async function queryOpenCodeGoConsoleStatus( timeoutMs: options.requestTimeoutMs, consume: async (response) => { if (!response.ok) { - // 404 and 403 on this member-scoped route are read as "no Go subscription", - // as the usage API's 403 EntitlementError is. - if (response.status === 404 || response.status === 403) { + // 404 on this member-scoped route is read as "no Go subscription". + // 403 is kept ambiguous (it could be an access/permission issue on + // the console side rather than a subscription state), so callers + // fall back instead of showing a not-subscribed state. + if (response.status === 404) { return { success: false, - error: `OpenCode Go subscription not found for this console account (${response.status})`, + error: "OpenCode Go subscription not found for this console account (404)", notSubscribed: true, }; } diff --git a/tests/lib.opencode-go.test.ts b/tests/lib.opencode-go.test.ts index 378abb47..f96de9a3 100644 --- a/tests/lib.opencode-go.test.ts +++ b/tests/lib.opencode-go.test.ts @@ -511,18 +511,19 @@ describe("queryOpenCodeGoConsoleStatus", () => { ); }); - it.each([404, 403])("reads HTTP %s as no Go subscription", async (status) => { - mocks.fetchResponse.mockResolvedValueOnce({ ok: false, status }); + it("reads HTTP 404 as no Go subscription", async () => { + mocks.fetchResponse.mockResolvedValueOnce({ ok: false, status: 404 }); await expect(queryOpenCodeGoConsoleStatus({ accessToken: "console-access" })).resolves.toEqual({ success: false, - error: `OpenCode Go subscription not found for this console account (${status})`, + error: "OpenCode Go subscription not found for this console account (404)", notSubscribed: true, }); }); it.each([ [401, false], + [403, false], [500, true], ])("keeps HTTP %s as a failed Console request", async (status, retryable) => { mocks.fetchResponse.mockResolvedValueOnce({ ok: false, status }); diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index 6be9303e..ec03d17a 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -166,13 +166,11 @@ describe("opencode-go provider", () => { ]); }); - it.each([ - 404, 403, - ])("reports a console not-subscribed state (%s) without errors", async (status) => { + it("reports a console not-subscribed state without errors", async () => { consoleConfigured(); mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ success: false, - error: `OpenCode Go subscription not found for this console account (${status})`, + error: "OpenCode Go subscription not found for this console account (404)", notSubscribed: true, }); @@ -206,6 +204,53 @@ describe("opencode-go provider", () => { ); }); + it("falls back to the API key after a Console 403", async () => { + consoleConfigured(); + mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ + success: false, + error: "OpenCode Console API error 403 (/api/go/status)", + retryable: false, + }); + + const out = await runFetch(); + + expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); + expectAttemptedWithNoErrors(out); + expect(visibleEntries(out.entries, "opencode-go")).toHaveLength(3); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "go_source", value: "legacy_key" }, + { key: "console_error", value: "OpenCode Console API error 403 (/api/go/status)" }, + ]), + ); + expect(out.statusDetails).not.toContainEqual({ + key: "opencode_go_state", + value: "not_subscribed", + }); + }); + + it("stays quiet after a Console 403 when there is no API key", async () => { + consoleConfigured(); + mocks.queryOpenCodeGoConsoleStatus.mockResolvedValueOnce({ + success: false, + error: "OpenCode Console API error 403 (/api/go/status)", + retryable: false, + }); + mocks.getOpenCodeGoAuthDiagnostics.mockResolvedValueOnce(diagnostics("none")); + mocks.resolveOpenCodeGoAuthCached.mockResolvedValueOnce({ state: "none" }); + + const out = await runFetch(); + + expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); + expectNotAttempted(out); + expect(out.statusDetails).toEqual( + expect.arrayContaining([ + { key: "console_auth_state", value: "configured" }, + { key: "console_error", value: "OpenCode Console API error 403 (/api/go/status)" }, + ]), + ); + }); + it("reports the console server the sign-in belongs to", async () => { mocks.resolveOpenCodeConsoleAuth.mockResolvedValueOnce({ state: "configured", From 81ea90319550d2bdba37411ebe12a9f484a9f21c Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:11:45 +0200 Subject: [PATCH 75/97] feat(cli): run show and status without OpenCode, reading logins from its database `opencode-quota show` and `status` compute quota in the terminal process again, as before 713845c19, so they work while OpenCode is closed. For one report they bind a read-only reader of OpenCode's credential table as the login source (src/lib/opencode-auth-sqlite.ts, the SQL, order and filters from 23aa94fac). It never refreshes or writes a token: an expired sign-in shows as expired in the terminal until OpenCode refreshes it. /quota_status from the terminal shows `source: sqlite`. The server plugin still reads logins only through ctx.integration, and an import-graph test keeps the database reader out of the server and TUI entries. The command again uses the folder it runs in (git worktree root, so project quota settings apply), its own provider detection (configured providers plus Anthropic and Kimi logins), and the shell's PATH and environment. Removed: the RPC `cli` method, OpenCode service discovery, the RPC HTTP client and exit code 3; exit codes are 0/1/2 again. The packed-package smoke runs the packed CLI's `show --json` again. Docs: README, external integration, troubleshooting, updating, providers and CONTRIBUTING (the one exception to "logins only through ctx.integration"). Starship keeps reading the export file. --- CONTRIBUTING.md | 2 +- README.md | 12 +- docs/readme/external-integration.md | 15 +- docs/readme/providers.md | 4 +- docs/readme/troubleshooting.md | 11 +- docs/readme/updating.md | 4 +- scripts/fixtures/smoke-packed-real-otel.mjs | 27 +- scripts/smoke-packed-package.mjs | 29 +- src/bin/opencode-quota.ts | 6 +- src/lib/cli-reports.ts | 4 +- src/lib/cli-show.ts | 177 ++++-- src/lib/cli-status.ts | 15 +- src/lib/opencode-auth-sqlite.ts | 115 ++++ src/lib/opencode-auth.ts | 42 +- src/lib/opencode-service.ts | 70 --- src/lib/quota-rpc-client.ts | 87 --- src/plugin.ts | 44 +- src/rpc.ts | 25 - tests/helpers/credential-database.ts | 53 ++ tests/lib.cli-reports.test.ts | 3 +- tests/lib.cli-show.test.ts | 616 ++++++++++++++++---- tests/lib.cli-status.test.ts | 337 ++++++++--- tests/lib.opencode-auth-sqlite.test.ts | 370 ++++++++++++ tests/lib.opencode-auth.test.ts | 8 +- tests/lib.opencode-service.test.ts | 147 ----- tests/lib.quota-rpc-client.test.ts | 173 ------ tests/lib.quota-status.test.ts | 19 +- tests/plugin-rpc.test.ts | 91 +-- tests/plugin-v2.test.ts | 5 +- tests/rpc-definition.test.ts | 39 +- tests/tui-dist-import-graph.test.ts | 35 +- 31 files changed, 1537 insertions(+), 1048 deletions(-) create mode 100644 src/lib/opencode-auth-sqlite.ts delete mode 100644 src/lib/opencode-service.ts delete mode 100644 src/lib/quota-rpc-client.ts create mode 100644 tests/helpers/credential-database.ts create mode 100644 tests/lib.opencode-auth-sqlite.test.ts delete mode 100644 tests/lib.opencode-service.test.ts delete mode 100644 tests/lib.quota-rpc-client.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c4e03ab..cacd9acc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,7 +82,7 @@ Recommended settings for `main`: - The server plugin (`src/plugin.ts`) registers the `quota_status` tool, the slash commands, and the `slkiser.opencode-quota` RPC, and computes the text of every surface. OpenCode 2 gives plugins no Web or Desktop UI hooks, so Web and Desktop get slash commands but no toasts or panels. Commands never call a model. - The TUI plugin (`src/tui-v2.tsx`) only renders: toasts, the Sidebar panel, the compact line and prompt bar below the prompt, the Home footer line, and report popups. It gets every text through the RPC and imports no provider or credential module (`tests/tui-dist-import-graph.test.ts`, `tests/plugin.command-handled-boundary.test.ts`). - Slash commands (`/quota`, `/quota_status`, `/quota_announcements`, `/pricing_refresh`, `/tokens_*`), the `quota_status` tool, and the RPC `command` method must route through `buildQuotaDialogCommandOutput()`; do not duplicate command-output logic in `src/plugin.ts` or `src/tui-v2.tsx`. -- Read OpenCode credentials only through `ctx.integration`, inside `src/lib/opencode-auth.ts`, never from `auth.json` or the `credential` table. Every reader call names its integration ids. +- Read OpenCode credentials only through `ctx.integration`, inside `src/lib/opencode-auth.ts`, never from `auth.json` or the `credential` table. The one exception: the terminal command (`src/lib/cli-show.ts`) binds the read-only reader in `src/lib/opencode-auth-sqlite.ts`, the only code that queries the `credential` table; `src/plugin.ts` and `src/tui-v2.tsx` must never reach it (`tests/tui-dist-import-graph.test.ts`). Every reader call still names its integration ids. - Keep `tests/plugin.command-handled-boundary.test.ts` aligned with these invariants. Additional boundary tests to keep healthy when touching plugin/provider logic: diff --git a/README.md b/README.md index 3da5c69e..3879def8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Upgrading from 4.x? Read [what changed in 5.0](#breaking-changes-in-500). After installation: 1. Restart OpenCode. -2. Run `/quota` in the OpenCode TUI, or `opencode-quota show` in a terminal while OpenCode is running. +2. Run `/quota` in the OpenCode TUI, or `opencode-quota show` in a terminal. 3. If you enabled the sidebar, open the session sidebar and look for `Quota`. 4. If you enabled the compact status line, look at the bottom of Home or below the message input. @@ -66,8 +66,8 @@ The updater always shows a preview first. `--yes` applies only the safe edits it > [!WARNING] > - OpenCode 1 is no longer supported. `init` and `update` detect OpenCode 1 and keep you on 4.x (`@slkiser/opencode-quota@4`). -> - Logins are read through OpenCode 2's plugin API (OpenCode keeps them in `opencode.db`), never from `auth.json`. If a provider is missing, log in again. -> - Quota is computed in OpenCode's background service, so `PATH` and environment variables come from the service, not your terminal. See [Service environment](docs/readme/troubleshooting.md#service-environment). +> - Logins come from OpenCode 2 (it keeps them in `opencode.db`), never from `auth.json`. If a provider is missing, log in again. +> - Inside OpenCode, logins go through the plugin API and quota is computed in OpenCode's background service, so `PATH` and environment variables come from the service. See [Service environment](docs/readme/troubleshooting.md#service-environment). The terminal command reads `opencode.db` read-only and uses your terminal's environment. > - Web and Desktop get the slash commands (report posts in the chat) but no toasts or panels. > - One `"plugin"` entry in `opencode.json` loads the server and the TUI; no `tui.json` entry is needed. > - TUI slash reports now open in a popup. Set `tuiCommandDisplay: "inline"` to keep them in the chat. @@ -106,7 +106,7 @@ The updater always shows a preview first. `--yes` applies only the safe edits it More ways to use it: -- **Terminal:** run `npx @slkiser/opencode-quota show` while OpenCode is running; it asks OpenCode's background service and exits with code `3` if OpenCode is not running. In Web and Desktop, slash commands post the report in the chat, and the AI can call the `quota_status` tool. +- **Terminal:** run `npx @slkiser/opencode-quota show` (works with OpenCode closed). In Web and Desktop, slash commands post the report in the chat, and the AI can call the `quota_status` tool. - **Several logins:** each login for a provider gets its own rows; `*` marks the active one. - **Scripts and CI:** JSON output and optional OpenTelemetry metrics. See [External integration](docs/readme/external-integration.md). - **Display:** a quota bar under the prompt ([`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings)), OpenCode Go's collapsed-sidebar row ([`tuiSidebarPanel.opencodeGoPreferredWindow`](docs/readme/configuration.md#tui-settings)), reset countdown style ([`resetTimeSpaced`](docs/readme/configuration.md#common-changes), [`resetTimeDecimals`](docs/readme/configuration.md#common-changes)), bare `81%` labels ([`percentLabelStyle`](docs/readme/configuration.md#common-changes)), and extra accounting rows ([`accountingDetail`](docs/readme/configuration.md#show-accounting-detail)). @@ -149,7 +149,7 @@ Use the CLI for setup, updates, terminal checks, and custom providers. | `npx @slkiser/opencode-quota@latest status` | Check configuration and provider problems | | `npx @slkiser/opencode-quota@latest update` | Update an existing installation | -`show` and `status` need OpenCode running: they ask its background service, using your global quota settings. If OpenCode is not running, they exit with code `3`. Run `npx @slkiser/opencode-quota@latest --help` for command options. See [External integration](docs/readme/external-integration.md#1-get-json-from-a-command) for JSON, scripts, and CI examples. +`show` and `status` work with OpenCode closed: they read logins from `opencode.db` read-only, never refresh a token (a sign-in can show as expired, for example `Token expired`, until you open OpenCode), take `PATH` and API-key variables from your shell, and use the settings of the folder you run them in (a project's `opencode-quota/quota-toast.jsonc` applies). Run `npx @slkiser/opencode-quota@latest --help` for command options. See [External integration](docs/readme/external-integration.md#1-get-json-from-a-command) for JSON, scripts, and CI examples. ## Providers @@ -248,7 +248,7 @@ The guided setup previews the change before saving. See the [custom-provider gui If quota or token data looks wrong: -1. Run `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal for the same diagnostics (OpenCode must be running). Use `opencode-quota show` for a quick quota glance. +1. Run `/quota_status` in the OpenCode TUI, or `opencode-quota status` from a terminal for the same diagnostics. Use `opencode-quota show` for a quick quota glance. 2. Confirm the expected provider appears in the detected provider list. If it is missing, log in to it again in OpenCode 2. `credential_source` lists logins OpenCode could not return. 3. Confirm companion auth plugins are before `@slkiser/opencode-quota` in `opencode.json`. 4. If token reports are empty, start OpenCode once so it creates `opencode.db`, then run a session with model usage. diff --git a/docs/readme/external-integration.md b/docs/readme/external-integration.md index 3e878ca4..9d70bdfc 100644 --- a/docs/readme/external-integration.md +++ b/docs/readme/external-integration.md @@ -20,7 +20,13 @@ Use this for scripts and CI: opencode-quota show --json ``` -The command asks OpenCode's background service for the data, so OpenCode must be running. It never starts OpenCode. It always runs for your home folder, so it uses your global quota settings, not a project's. +The command runs in your terminal and works with OpenCode closed: + +- It reads logins from OpenCode's database (`opencode.db`) read-only. +- It never refreshes a token, so a sign-in can show as expired (for example `Token expired`) in the terminal while OpenCode shows data. Opening OpenCode refreshes it. +- It takes `PATH` and API-key variables from your shell. +- It uses the settings of the folder you run it in, so a project's `opencode-quota/quota-toast.jsonc` applies. +- `opencode-quota status` shows `source: sqlite` under `credential_source`. Useful variations: @@ -39,9 +45,6 @@ Threshold exit codes: | `0` | Quota is available and above the threshold | | `1` | At least one comparable cached percentage is below the threshold | | `2` | Results were incomplete or no comparable percentage was found | -| `3` | OpenCode is not running or the plugin could not be reached | - -Exit code `3` can come from any `show` or `status` command. The reason goes to stderr and stdout stays empty. ### CI example @@ -49,8 +52,6 @@ Exit code `3` can come from any `show` or `status` command. The reason goes to s npx @slkiser/opencode-quota show --json --threshold 5 ``` -This needs OpenCode running on the same machine and user account. Otherwise the command exits with code `3`. A tool that only needs recent numbers can read the [export file](#2-read-an-export-file) instead. - ### Read Copilot's percentage with `jq` Some Copilot results contain values instead of percentages. Select a percentage row instead of assuming the first row is one: @@ -269,7 +270,7 @@ Each summary is exactly `id`, effective `providerId`, coarse `status`, and `entr - All options use data collected during normal OpenCode Quota activity. - The command and export file read cached data instead of contacting providers. -- The command and the export file both come from OpenCode's background service. The command needs OpenCode running; the file keeps its last contents when OpenCode stops. +- The export file comes from OpenCode's background service and keeps its last contents when OpenCode stops. The command runs in your terminal and works with OpenCode closed. - The OpenTelemetry integration reads in-memory results and never starts its own refresh loop. - OpenTelemetry metric labels are limited to safe provider, result type, and quota-window values. - Display names, account IDs, source IDs, credentials, paths, URLs, errors, and raw responses are never metric labels. diff --git a/docs/readme/providers.md b/docs/readme/providers.md index b9f8dde1..6d5244c5 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -628,9 +628,9 @@ OpenCode Zen reads billing and usage from unofficial OpenCode Console routes (`/ 1. In a terminal, run `opencode auth login opencode`, sign in to the Console in your browser, and pick the organization to track. 2. To track another organization, sign in again and pick it. With several saved Console sign-ins, `opencode auth switch opencode` chooses the active one, and Zen follows it. -3. Check it with `/quota_status` in OpenCode, or `opencode-quota status` in a terminal while OpenCode runs. +3. Check it with `/quota_status` in OpenCode, or `opencode-quota status` in a terminal. -When Zen reads the sign-in, OpenCode refreshes its token if needed. If the sign-in fails or the Console rejects it, Zen asks you to run `opencode auth login opencode` again. Without a Console sign-in (an OpenCode API key alone is not one), auto mode skips Zen; include `opencode` in `enabledProviders` to see the sign-in hint instead. +When Zen reads the sign-in inside OpenCode, OpenCode refreshes its token if needed; the terminal command does not. If the sign-in fails or the Console rejects it, Zen asks you to run `opencode auth login opencode` again. Without a Console sign-in (an OpenCode API key alone is not one), auto mode skips Zen; include `opencode` in `enabledProviders` to see the sign-in hint instead. > The old `opencode-quota/opencode.json` file (`workspaceId` + `consoleSessionCookie`) and the `OPENCODE_WORKSPACE_ID` / `OPENCODE_AUTH_COOKIE` variables are no longer read. Remove them after Zen works; see [Updating safely](updating.md#opencode-zen-findings). diff --git a/docs/readme/troubleshooting.md b/docs/readme/troubleshooting.md index 030c11f5..e4a09bb6 100644 --- a/docs/readme/troubleshooting.md +++ b/docs/readme/troubleshooting.md @@ -2,22 +2,22 @@ # Troubleshooting -Start with `/quota_status` inside OpenCode, or `opencode-quota status` from a terminal while OpenCode is running. Both show which config, providers, authentication, and local files OpenCode Quota found. +Start with `/quota_status` inside OpenCode, or `opencode-quota status` from a terminal. Both show which config, providers, authentication, and local files OpenCode Quota found. ## First checks -1. Run `/quota_status` inside OpenCode. From a terminal, `opencode-quota status` shows the same report; it needs OpenCode running and uses your global quota settings. +1. Run `/quota_status` inside OpenCode. From a terminal, `opencode-quota status` shows the same report for the folder you run it in; it works with OpenCode closed. 2. Find the provider or feature that is failing. 3. Follow the matching fix below. 4. Restart OpenCode after changing config or authentication. If every provider is missing, confirm OpenCode Quota is listed in `opencode.jsonc` or `.json`. That one entry also loads the TUI; no `tui.json` entry is needed. -OpenCode Quota asks OpenCode 2 for logins through its plugin API (OpenCode keeps them in `opencode.db`) and never reads `auth.json`. OpenCode 2 copies `auth.json` once, the first time it starts. If a provider is missing, log in to it again in OpenCode 2. In `/quota_status`, the `credential_source` section shows the login `source`, a `list_error` when OpenCode could not list logins, and `failures`: each login OpenCode could not return (for example a failed token refresh) as `provider:label:reason:detail`. Such a login also shows as an error row; log in to that provider again. `/quota_status` also shows the `opencode.db` path used for session and token history; `OPENCODE_DB` and `XDG_DATA_HOME` change it. Custom or source builds of OpenCode may use `opencode-.db` instead; set `OPENCODE_DB` to that file's path. +Inside OpenCode, OpenCode Quota asks OpenCode 2 for logins through its plugin API (OpenCode keeps them in `opencode.db`); the terminal command reads `opencode.db` read-only. Neither reads `auth.json`. OpenCode 2 copies `auth.json` once, the first time it starts. If a provider is missing, log in to it again in OpenCode 2. In `/quota_status`, the `credential_source` section shows the login `source`, a `list_error` when OpenCode could not list logins, and `failures`: each login OpenCode could not return (for example a failed token refresh) as `provider:label:reason:detail`. Such a login also shows as an error row; log in to that provider again. From the terminal, `source` is `sqlite`, and `list_error` and `failures` stay empty. `/quota_status` also shows the `opencode.db` path used for session and token history; `OPENCODE_DB` and `XDG_DATA_HOME` change it. Custom or source builds of OpenCode may use `opencode-.db` instead; set `OPENCODE_DB` to that file's path. ## Service environment -Every surface (TUI, Web, Desktop, and the terminal command) gets its numbers from OpenCode's background service. The service takes `PATH` and environment variables from whatever started it first, not from the terminal you use now, and it runs in your home folder. +The TUI, Web, and Desktop get their numbers from OpenCode's background service. The service takes `PATH` and environment variables from whatever started it first, not from the terminal you use now, and it runs in your home folder. The terminal command uses your terminal's `PATH`, environment, and current folder instead. - **`claude` or `bl` not found, or an API-key variable ignored:** in a terminal where they work, run `opencode service restart`. To keep that `PATH` for every later start, run `opencode service set env PATH "$PATH"` (it stops the service; open OpenCode again). For Claude you can instead set `anthropicBinaryPath`. - A relative `export.path` is relative to your home folder. @@ -36,8 +36,7 @@ Every surface (TUI, Web, Desktop, and the terminal command) gets its numbers fro | Token reports are empty | Start OpenCode once, then use a model so `opencode.db` contains usage. | | Pricing looks old | Run `/pricing_refresh`. | | Web report columns do not line up | Expected: Web uses a proportional font. Use the TUI or run `npx @slkiser/opencode-quota show` in a terminal. | -| Terminal command says `OpenCode is not running` (exit 3) | Open OpenCode or run `opencode service start`, then try again. The command never starts OpenCode itself. | -| Terminal command says the server plugin is not loaded (exit 3) | Check the plugin entry above, then run `opencode service restart`. | +| Terminal shows a sign-in as expired but OpenCode shows data | The terminal command never refreshes tokens. Open OpenCode (or use that provider in it) once, then run the command again. | ## Update safely diff --git a/docs/readme/updating.md b/docs/readme/updating.md index fc768547..7fdd76e3 100644 --- a/docs/readme/updating.md +++ b/docs/readme/updating.md @@ -49,8 +49,8 @@ The updater builds one plan, prints it in full, and then either stops or applies What changed in `5.0.0`: - **OpenCode 1 is no longer supported.** `init` and `update` detect OpenCode 1 and keep you on 4.x. See [OpenCode 1](#opencode-1). -- **Logins come only from OpenCode 2.** OpenCode Quota asks OpenCode 2 for logins through its plugin API (OpenCode keeps them in `opencode.db`) and never reads `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. -- **Quota is computed in OpenCode's background service.** `PATH`, environment variables, and the working folder (your home folder) come from the service, not from your terminal. See [Service environment](troubleshooting.md#service-environment). +- **Logins come only from OpenCode 2.** Inside OpenCode, OpenCode Quota asks OpenCode 2 for logins through its plugin API; the terminal command reads OpenCode's database (`opencode.db`) read-only. Neither reads `auth.json`. OpenCode 2 copies your old logins once, the first time it starts. If a provider is missing, log in again in OpenCode 2. +- **Inside OpenCode, quota is computed in OpenCode's background service.** `PATH`, environment variables, and the working folder (your home folder) come from the service, not from your terminal; the terminal command uses your terminal's. See [Service environment](troubleshooting.md#service-environment). - **Web and Desktop have slash commands but no toasts or panels,** because OpenCode 2 gives plugins no Web UI hooks. On Web and Desktop, `/quota` posts the report in the chat as your message. The AI never answers it, and the plugin filters it out of every AI request. If you uninstall the plugin, old reports in past chats are no longer filtered. See [Web and Desktop notes](manual-install.md#web-and-desktop-notes). - **One plugin entry.** One `"plugin"` entry in `opencode.json` loads both the server and the TUI; no `tui.json` entry is needed. - **TUI reports open in a popup.** TUI slash commands and the command palette open the report in a popup and leave no chat message. `tuiCommandDisplay` now defaults to `"dialog"`; set `"inline"` to keep TUI slash reports in the chat instead. diff --git a/scripts/fixtures/smoke-packed-real-otel.mjs b/scripts/fixtures/smoke-packed-real-otel.mjs index 585bce6d..979fe2c4 100644 --- a/scripts/fixtures/smoke-packed-real-otel.mjs +++ b/scripts/fixtures/smoke-packed-real-otel.mjs @@ -434,21 +434,14 @@ async function runFailingInfrastructure() { } async function runSeedProductionOpenRouter() { - const { buildCliShowJson } = await packageImport("cli-reports.js"); - const { resolveOpenCodeLocationRoots } = await packageImport("config-file-utils.js"); + const { createCliQuotaClient, resolveCliRoots } = await packageImport("cli-show.js"); const { createExportProviderContext } = await packageImport("quota-export.js"); const { resolveQuotaRuntimeContext } = await packageImport("quota-runtime-context.js"); const secret = process.env.OPENROUTER_API_KEY; assert.ok(secret); - // The same configuration client and roots the server plugin builds for its location. - const roots = resolveOpenCodeLocationRoots(process.cwd()); - const client = { - config: { - get: async () => ({ data: {} }), - providers: async () => ({ data: { providers: [] } }), - }, - }; + const roots = resolveCliRoots(process.cwd()); + const client = createCliQuotaClient({ configRootDir: roots.configRoot }); const runtime = await resolveQuotaRuntimeContext({ client, roots, @@ -475,20 +468,6 @@ async function runSeedProductionOpenRouter() { assert.equal(exported.providers.openrouter.status, "ok"); assert.equal(exported.providers.openrouter.entries[0].percentRemaining, 67); assert.ok(!JSON.stringify(exported).includes(secret)); - - // `opencode-quota show --json` gets this report from the server plugin's `cli` RPC method. - const report = await buildCliShowJson({ runtime, providerId: "openrouter" }); - assert.equal(report.exitCode, 0); - assert.equal(report.stderr, ""); - const cachedOpenRouter = JSON.parse(report.stdout); - assert.equal(cachedOpenRouter.fromCache, true); - assert.equal(cachedOpenRouter.providers.openrouter.status, "ok"); - assert.equal( - cachedOpenRouter.providers.openrouter.entries[0].name, - "Packed production OpenRouter", - ); - assert.equal(cachedOpenRouter.providers.openrouter.entries[0].percentRemaining, 67); - process.stdout.write(report.stdout); } try { diff --git a/scripts/smoke-packed-package.mjs b/scripts/smoke-packed-package.mjs index 615645e8..39b52884 100644 --- a/scripts/smoke-packed-package.mjs +++ b/scripts/smoke-packed-package.mjs @@ -125,12 +125,9 @@ try { const openRouterSecret = "packed-openrouter-secret-canary"; const openRouterEnv = { ...isolatedRuntimeEnv, OPENROUTER_API_KEY: openRouterSecret }; - const cachedOpenRouterOutput = run( - process.execPath, - [realOtelFixture, "seed-production-openrouter"], - workdir, - { env: openRouterEnv }, - ); + run(process.execPath, [realOtelFixture, "seed-production-openrouter"], workdir, { + env: openRouterEnv, + }); const cliPath = path.join( workdir, @@ -153,17 +150,20 @@ try { } } - // The isolated state folder has no OpenCode service registration, so the packed CLI must - // report that OpenCode is not running: exit 3, a stderr message and an empty stdout. - const offlineShow = spawnSync( + const cachedOpenRouterOutput = run( process.execPath, [cliPath, "show", "--json", "--provider", "openrouter"], - { cwd: workdir, encoding: "utf8", env: { ...process.env, ...openRouterEnv } }, + workdir, + { env: openRouterEnv }, + ); + const cachedOpenRouter = JSON.parse(cachedOpenRouterOutput); + assert.equal(cachedOpenRouter.fromCache, true); + assert.equal(cachedOpenRouter.providers.openrouter.status, "ok"); + assert.equal( + cachedOpenRouter.providers.openrouter.entries[0].name, + "Packed production OpenRouter", ); - if (offlineShow.error) throw offlineShow.error; - assert.equal(offlineShow.status, 3); - assert.equal(offlineShow.stdout, ""); - assert.match(offlineShow.stderr, /OpenCode is not running\. Start OpenCode and try again\./); + assert.equal(cachedOpenRouter.providers.openrouter.entries[0].percentRemaining, 67); const readTree = async (directory) => { const chunks = []; @@ -176,7 +176,6 @@ try { }; const privateArtifacts = [ cachedOpenRouterOutput, - offlineShow.stderr, ...(await readTree(path.join(workdir, "runtime"))), ].join("\n"); assert.ok(!privateArtifacts.includes(openRouterSecret)); diff --git a/src/bin/opencode-quota.ts b/src/bin/opencode-quota.ts index b21acf80..d1c2e825 100644 --- a/src/bin/opencode-quota.ts +++ b/src/bin/opencode-quota.ts @@ -19,11 +19,11 @@ const USAGE = [ " init Run the interactive quota installer", " --dry-run Preview validated changes without writing files", " --sync-legacy-config also writes experimental.quotaToast", - " show Print a quick quota glance (needs OpenCode running)", + " show Print a quick quota glance", " --json Machine-readable JSON output (reads from cache)", " --threshold With --json, exit 1 if below %, 2 if incomplete/not comparable", " --provider Filter to one provider", - " status Print Quota Status diagnostics (same data as /quota_status; needs OpenCode running)", + " status Print Quota Status diagnostics (same data as /quota_status)", " --json Machine-readable JSON output", " --provider Filter to one provider", " update Preview safe setting/package-cache changes and report legacy credential findings", @@ -33,8 +33,6 @@ const USAGE = [ " --yes Apply only safe setting/cache work after printing the preview", " provider add Add or update one global quotaProviders definition", " --dry-run Preview the exact global OpenCode config without writing", - "", - "show and status exit with code 3 when OpenCode is not running or the plugin could not be reached.", ].join("\n"); function printUsage(): void { diff --git a/src/lib/cli-reports.ts b/src/lib/cli-reports.ts index 65d8d9cd..93b4d0c5 100644 --- a/src/lib/cli-reports.ts +++ b/src/lib/cli-reports.ts @@ -1,6 +1,6 @@ /** - * The reports behind `opencode-quota show` and `status`. The server plugin builds them for - * the `cli` RPC method; the terminal command only prints what they return. + * The reports behind `opencode-quota show` and `status`. The terminal command builds them + * in its own process (`cli-show.ts`) and prints them. */ import { sanitizeQuotaRenderData } from "./display-sanitize.js"; import { formatQuotaRows } from "./format.js"; diff --git a/src/lib/cli-show.ts b/src/lib/cli-show.ts index fe76247c..e04b7a06 100644 --- a/src/lib/cli-show.ts +++ b/src/lib/cli-show.ts @@ -1,11 +1,26 @@ -import { homedir } from "node:os"; -import type { QuotaRpcCliInput, QuotaRpcCliOutput } from "../rpc.js"; -import { discoverOpenCodeService } from "./opencode-service.js"; +import { resolve } from "path"; +import { hasAnthropicCredentialsConfigured } from "./anthropic.js"; +import { buildCliShowJson, buildCliShowText, type CliReport } from "./cli-reports.js"; +import { findGitWorktreeRoot } from "./config-file-utils.js"; +import { + DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS, + resolveKimiCnAuthCached, + resolveKimiGlobalAuthCached, +} from "./kimi-auth.js"; +import { bindCredentialSource } from "./opencode-auth.js"; +import { createSqliteCredentialSource } from "./opencode-auth-sqlite.js"; +import { + loadConfiguredOpenCodeConfig, + loadConfiguredProviderIds, +} from "./opencode-config-providers.js"; import { getQuotaProviderShape } from "./provider-metadata.js"; -import { callQuotaRpc } from "./quota-rpc-client.js"; +import type { QuotaRuntimeClient, QuotaRuntimeContext } from "./quota-runtime-context.js"; +import { resolveQuotaRuntimeContext } from "./quota-runtime-context.js"; +import type { QuotaToastConfig } from "./types.js"; export interface RunCliShowCommandOptions { argv?: string[]; + cwd?: string; stdout?: Pick; stderr?: Pick; } @@ -18,16 +33,12 @@ const SHOW_USAGE = [ "Usage:", " npx @slkiser/opencode-quota show [--provider ] [--json] [--threshold ]", "", - "Needs OpenCode running: the quota comes from its background service.", - "", "Options:", " --provider Show quota for one provider", " --json Machine-readable JSON output (reads from cache)", " --threshold With --json, exit 1 if any complete cached percentage is below", " % remaining (exit 2 if data is incomplete or not comparable)", " --help, -h Show help", - "", - "Exit code 3: OpenCode is not running or the plugin could not be reached.", ].join("\n"); function parseShowArgs(argv: string[]): ParsedShowArgs { @@ -108,57 +119,118 @@ function parseShowArgs(argv: string[]): ParsedShowArgs { return { ok: true, providerId, help: false, json, threshold }; } -function writeLine(stream: Pick, message: string): void { - stream.write(message.endsWith("\n") ? message : `${message}\n`); +export function resolveCliRoots(cwd: string): { + workspaceRoot: string; + configRoot: string; + fallbackDirectory: string; +} { + const fallbackDirectory = resolve(cwd); + const worktreeRoot = findGitWorktreeRoot(fallbackDirectory) ?? fallbackDirectory; + return { + workspaceRoot: worktreeRoot, + configRoot: worktreeRoot, + fallbackDirectory, + }; +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); } -function isCliOutput(value: unknown): value is QuotaRpcCliOutput { - if (typeof value !== "object" || value === null) return false; - const output = value as Record; - return ( - Number.isInteger(output.exitCode) && - typeof output.stdout === "string" && - typeof output.stderr === "string" - ); +async function loadCliAuthenticatedProviderIds(config: Record): Promise { + const experimental = isRecord(config.experimental) ? config.experimental : undefined; + const quotaToast = isRecord(experimental?.quotaToast) ? experimental.quotaToast : undefined; + const anthropicBinaryPath = + typeof quotaToast?.anthropicBinaryPath === "string" + ? quotaToast.anthropicBinaryPath + : undefined; + const [anthropicConfigured, kimiGlobalAuth, kimiCnAuth] = await Promise.all([ + hasAnthropicCredentialsConfigured({ binaryPath: anthropicBinaryPath }), + resolveKimiGlobalAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }), + resolveKimiCnAuthCached({ maxAgeMs: DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS }), + ]); + + return [ + ...(anthropicConfigured ? ["anthropic"] : []), + ...(kimiGlobalAuth.state === "configured" ? ["kimi-code-plan-global"] : []), + ...(kimiCnAuth.state === "configured" ? ["kimi-code-plan-cn"] : []), + ]; +} + +export function createCliQuotaClient(params: { configRootDir: string }): QuotaRuntimeClient { + let configPromise: Promise> | undefined; + let providerIdsPromise: Promise | undefined; + + return { + config: { + get: async () => { + configPromise ??= loadConfiguredOpenCodeConfig({ + configRootDir: params.configRootDir, + }); + return { + data: (await configPromise) as { + experimental?: { quotaToast?: Partial }; + model?: string; + }, + }; + }, + providers: async () => { + providerIdsPromise ??= (async () => { + configPromise ??= loadConfiguredOpenCodeConfig({ + configRootDir: params.configRootDir, + }); + const [configuredIds, authenticatedIds] = await Promise.all([ + loadConfiguredProviderIds({ configRootDir: params.configRootDir }), + configPromise.then(loadCliAuthenticatedProviderIds), + ]); + return [...new Set([...configuredIds, ...authenticatedIds])]; + })(); + const ids = await providerIdsPromise; + return { + data: { + providers: ids.map((id) => ({ id })), + }, + }; + }, + }, + }; +} + +function writeLine(stream: Pick, message: string): void { + stream.write(message.endsWith("\n") ? message : `${message}\n`); } /** - * Runs a `show` or `status` report inside the running OpenCode service, which computes it - * for the home folder, and prints what it returns. Exit code 3 means OpenCode or the plugin - * could not be reached; stdout then stays empty so JSON readers never parse an error. + * Builds one `show` or `status` report in this process, for the folder the command runs in, + * and prints it. It works with OpenCode closed: logins are read read-only from OpenCode's + * database, and only while the report runs. */ -export async function runCliReportInOpenCode(params: { - input: QuotaRpcCliInput; +export async function runCliReport(params: { + cwd: string; + failurePrefix: "Failed to show quota" | "Failed to generate quota status"; + build: (runtime: QuotaRuntimeContext) => Promise; stdout: Pick; stderr: Pick; }): Promise { - const endpoint = await discoverOpenCodeService(); - if (!endpoint) { - writeLine(params.stderr, "OpenCode is not running. Start OpenCode and try again."); - return 3; - } - - let output: unknown; + const unbind = bindCredentialSource(createSqliteCredentialSource()); try { - output = await callQuotaRpc(endpoint, "cli", params.input, { - directory: homedir(), - timeoutMs: 60_000, + const roots = resolveCliRoots(params.cwd); + const runtime = await resolveQuotaRuntimeContext({ + client: createCliQuotaClient({ configRootDir: roots.configRoot }), + roots, + includeSessionMeta: false, }); + const report = await params.build(runtime); + params.stdout.write(report.stdout); + params.stderr.write(report.stderr); + return report.exitCode; } catch (error) { - writeLine(params.stderr, error instanceof Error ? error.message : String(error)); - return 3; - } - if (!isCliOutput(output)) { - writeLine( - params.stderr, - "OpenCode Quota's server plugin returned an unexpected result. Update the plugin and restart OpenCode.", - ); - return 3; + const message = error instanceof Error ? error.message : String(error); + writeLine(params.stderr, `${params.failurePrefix}: ${message}`); + return 1; + } finally { + unbind(); } - - params.stdout.write(output.stdout); - params.stderr.write(output.stderr); - return output.exitCode; } export async function runCliShowCommand(options: RunCliShowCommandOptions = {}): Promise { @@ -184,12 +256,13 @@ export async function runCliShowCommand(options: RunCliShowCommandOptions = {}): return 1; } - return runCliReportInOpenCode({ - input: { - command: parsed.json ? "show-json" : "show", - providerId, - threshold: parsed.threshold, - }, + return runCliReport({ + cwd: options.cwd ?? process.cwd(), + failurePrefix: "Failed to show quota", + build: (runtime) => + parsed.json + ? buildCliShowJson({ runtime, providerId, threshold: parsed.threshold }) + : buildCliShowText({ runtime, providerId }), stdout, stderr, }); diff --git a/src/lib/cli-status.ts b/src/lib/cli-status.ts index 45ee65da..efa41eba 100644 --- a/src/lib/cli-status.ts +++ b/src/lib/cli-status.ts @@ -1,8 +1,10 @@ -import { runCliReportInOpenCode } from "./cli-show.js"; +import { buildCliStatus } from "./cli-reports.js"; +import { runCliReport } from "./cli-show.js"; import { getQuotaProviderShape } from "./provider-metadata.js"; export interface RunCliStatusCommandOptions { argv?: string[]; + cwd?: string; stdout?: Pick; stderr?: Pick; } @@ -15,8 +17,8 @@ const STATUS_USAGE = [ "Usage:", " npx @slkiser/opencode-quota status [--provider ] [--json]", "", - "Print the same Quota Status diagnostics as the /quota_status slash command.", - "Needs OpenCode running: the report comes from its background service.", + "Print the same Quota Status diagnostics as the /quota_status slash command,", + "without launching OpenCode.", "", "Options:", " --provider Restrict provider availability and live probes to one provider", @@ -28,7 +30,6 @@ const STATUS_USAGE = [ " 0 success", " 1 error or quota disabled (enabled: false)", " 2 no comparable provider data (with --json only)", - " 3 OpenCode is not running or the plugin could not be reached", ].join("\n"); const THRESHOLD_REDIRECT = @@ -118,8 +119,10 @@ export async function runCliStatusCommand( return 1; } - return runCliReportInOpenCode({ - input: { command: parsed.json ? "status-json" : "status", providerId }, + return runCliReport({ + cwd: options.cwd ?? process.cwd(), + failurePrefix: "Failed to generate quota status", + build: (runtime) => buildCliStatus({ runtime, providerId, json: parsed.json }), stdout, stderr, }); diff --git a/src/lib/opencode-auth-sqlite.ts b/src/lib/opencode-auth-sqlite.ts new file mode 100644 index 00000000..3acbcc9d --- /dev/null +++ b/src/lib/opencode-auth-sqlite.ts @@ -0,0 +1,115 @@ +/** + * The terminal command's read-only login reader + * + * `opencode-quota show` and `status` bind this source while one report runs, so they work + * with OpenCode closed. It is the only code that queries OpenCode's `credential` table. It + * never refreshes or writes a login: a sign-in whose token expired stays expired here until + * OpenCode refreshes it. The server plugin and the TUI must never import this file + * (`tests/tui-dist-import-graph.test.ts`). + */ + +import { existsSync } from "fs"; + +import { + type CredentialMethod, + type CredentialRow, + type CredentialSource, + getCredentialDatabasePaths, +} from "./opencode-auth.js"; +import { openOpenCodeSqliteReadOnly, type SqliteConn } from "./opencode-sqlite.js"; + +/** The OpenCode 2 method of a parsed credential value (the reader maps a stored `key` to `api`). */ +function credentialMethod(value: Record): CredentialMethod | undefined { + if (value.type === "oauth") return "oauth"; + if (value.type === "api" || value.type === "key") return "key"; + return undefined; +} + +async function readCredentialDatabase(path: string): Promise { + if (!existsSync(path)) return []; + + let database: SqliteConn | undefined; + try { + database = await openOpenCodeSqliteReadOnly(path); + const rows = database.all>( + "SELECT id, integration_id, label, active, value FROM credential WHERE integration_id IS NOT NULL ORDER BY active DESC, time_updated DESC, id DESC", + ); + const credentials: CredentialRow[] = []; + + for (const row of rows) { + if ( + typeof row.id !== "string" || + typeof row.integration_id !== "string" || + typeof row.label !== "string" || + typeof row.value !== "string" + ) + continue; + const value = parseCredentialValue(row.value); + if (value) { + credentials.push({ + id: row.id, + integrationId: row.integration_id, + label: row.label, + active: row.active === 1, + value, + }); + } + } + + return credentials; + } catch { + return []; + } finally { + database?.close(); + } +} + +function parseCredentialValue(value: string): Record | null { + try { + const parsed = JSON.parse(value); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; + const credential = parsed as Record; + const metadata = credential.metadata; + const authEntry = { + ...(metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {}), + ...credential, + }; + if (authEntry.type === "key" && typeof authEntry.key === "string") { + authEntry.type = "api"; + } + return authEntry; + } catch { + return null; + } +} + +/** + * Logins from OpenCode's database, in database order (active first, then most recently + * updated), not in request order. The database path is resolved on every read, so + * `OPENCODE_DB` and `XDG_DATA_HOME` apply. Any read error means no logins. + */ +export function createSqliteCredentialSource(): CredentialSource { + return { + kind: "sqlite", + async readRows(request) { + const [path] = getCredentialDatabasePaths(); + const rows = (path ? await readCredentialDatabase(path) : []).filter((row) => + request.integrationIds.includes(row.integrationId), + ); + const seenIntegrationIds = new Set(); + const firstRows = request.firstOnly + ? rows.filter((row) => { + if (seenIntegrationIds.has(row.integrationId)) return false; + seenIntegrationIds.add(row.integrationId); + return true; + }) + : rows; + const methods = request.methods; + if (!methods) return firstRows; + return firstRows.filter((row) => { + const method = credentialMethod(row.value); + return method !== undefined && methods.includes(method); + }); + }, + }; +} diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 078079b8..2343e093 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -1,9 +1,10 @@ /** * OpenCode login reader * - * Reads OpenCode 2 logins through the server plugin's integration API - * (`ctx.integration`). The server plugin binds that source in `setup`; the TUI - * and the terminal command never bind one, so a read there finds no login. + * Reads OpenCode 2 logins through the bound credential source. The server + * plugin binds its integration API (`ctx.integration`) in `setup`; the terminal + * command binds the read-only database reader in `opencode-auth-sqlite.ts` + * while one report runs; the TUI binds nothing, so a read there finds no login. * Every read names the integration ids it needs, so OpenCode resolves (and may * refresh) only those logins. */ @@ -50,8 +51,14 @@ export type CredentialReadRequest = ReadCredentialRowsOptions & { integrationIds: readonly string[]; }; -/** Where logins come from. The only production source is `ctx.integration`. */ +export type CredentialSourceKind = "opencode-integration-api" | "sqlite"; + +/** + * Where logins come from. The two production sources: `ctx.integration` in the + * server plugin, and the terminal command's read-only reader of OpenCode's database. + */ export type CredentialSource = { + kind: CredentialSourceKind; readRows(request: CredentialReadRequest): Promise; }; @@ -74,9 +81,10 @@ type CredentialFailure = { label: string; }; -export type CredentialSourceDiagnostics = { - state: "bound" | "unbound"; - kind: "opencode-integration-api"; +export type CredentialSourceDiagnostics = ( + | { state: "bound"; kind: CredentialSourceKind } + | { state: "unbound" } +) & { lastListError?: { at: number; detail: string }; failures: Array<{ integrationId: string; @@ -146,9 +154,10 @@ const authCache = new Map(); let authCacheGeneration = 0; /** - * The database OpenCode keeps its logins in, for diagnostics only (logins are - * read through OpenCode, never from this file): the one resolved path, or none - * when `OPENCODE_DB` is `:memory:`. + * The database OpenCode keeps its logins in: the one resolved path, or none + * when `OPENCODE_DB` is `:memory:`. Inside OpenCode it is for diagnostics only + * (logins are read through OpenCode); the terminal command reads its logins + * from this file. */ export function getCredentialDatabasePaths(): string[] { const path = getOpenCodeDbPath(); @@ -177,9 +186,9 @@ export function notifyCredentialsChanged(): void { } export function getCredentialSourceDiagnostics(): CredentialSourceDiagnostics { + const source = credentialSources.at(-1)?.source; return { - state: credentialSources.length > 0 ? "bound" : "unbound", - kind: "opencode-integration-api", + ...(source ? { state: "bound" as const, kind: source.kind } : { state: "unbound" as const }), ...(lastListError ? { lastListError: { ...lastListError } } : {}), failures: Array.from(credentialFailures, ([connectionId, failure]) => ({ integrationId: failure.integrationId, @@ -372,6 +381,7 @@ export function createIntegrationCredentialSource( integration: CredentialIntegration, ): CredentialSource { return { + kind: "opencode-integration-api", async readRows(request) { const slots: ConnectionSlot[] = []; if (request.firstOnly) { @@ -425,9 +435,11 @@ export async function readAuthFile(params: { } /** - * Credential rows of the requested integration ids, in request-id order, then - * OpenCode's connection order (active first, then newest). Without a bound - * source (the TUI and the terminal command) there are none. + * Credential rows of the requested integration ids from the last bound source. + * Row order depends on the source: the integration API returns request-id + * order, then OpenCode's connection order (active first, then newest); the + * database reader returns database order. With no bound source (the TUI) there + * are none. */ export async function readCredentialRows( integrationIds: readonly string[], diff --git a/src/lib/opencode-service.ts b/src/lib/opencode-service.ts deleted file mode 100644 index 31f27a91..00000000 --- a/src/lib/opencode-service.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Finds the running OpenCode background service the way OpenCode's own clients do - * (`@opencode/client` `Service.discover`): read its registration file, then check that the - * registered process answers `/api/info`. It never starts OpenCode. - */ -import { readFile } from "node:fs/promises"; -import { homedir } from "node:os"; -import { join } from "node:path"; - -export type OpenCodeServiceEndpoint = { url: string; headers: Record }; - -const INFO_TIMEOUT_MS = 2000; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function isProcessId(value: unknown): value is number { - return typeof value === "number" && Number.isInteger(value) && value >= 0; -} - -/** - * Returns the service URL and its auth headers, or undefined when no healthy service is - * registered: no or unreadable registration file, no answer within 2 seconds, a non-2xx - * answer, or an answer from a different process or version than the file names. - */ -export async function discoverOpenCodeService(): Promise { - const file = join( - process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), - "opencode", - "service.json", - ); - const text = await readFile(file, "utf8").catch(() => undefined); - if (text === undefined) return undefined; - - let registration: unknown; - try { - registration = JSON.parse(text); - } catch { - return undefined; - } - if (!isRecord(registration)) return undefined; - const { url, pid, password, version } = registration; - if (typeof url !== "string" || !isProcessId(pid)) return undefined; - if (password !== undefined && typeof password !== "string") return undefined; - - const headers: Record = - password === undefined - ? {} - : { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }; - - let info: unknown; - try { - const response = await fetch(new URL("/api/info", url), { - headers, - signal: AbortSignal.timeout(INFO_TIMEOUT_MS), - }); - if (!response.ok) return undefined; - info = await response.json(); - } catch { - return undefined; - } - if (!isRecord(info) || typeof info.version !== "string" || !isProcessId(info.pid)) { - return undefined; - } - if (info.pid !== pid) return undefined; - if (version !== undefined && info.version !== version) return undefined; - - return { url, headers }; -} diff --git a/src/lib/quota-rpc-client.ts b/src/lib/quota-rpc-client.ts deleted file mode 100644 index b3af5a84..00000000 --- a/src/lib/quota-rpc-client.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Calls OpenCode Quota's plugin RPC over the OpenCode service's HTTP API with plain `fetch`, - * so the terminal command loads no `@opencode/*` package. - */ -import { QUOTA_RPC_ID, type QuotaRpc } from "../rpc.js"; -import { sanitizeSingleLineDisplayText } from "./display-sanitize.js"; -import type { OpenCodeServiceEndpoint } from "./opencode-service.js"; - -/** OpenCode or the plugin could not be reached, or the call failed before a result came back. */ -export class QuotaRpcTransportError extends Error { - override name = "QuotaRpcTransportError"; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -/** - * POSTs `{ input }` to `/api/rpc//` in the given location and returns the - * `output` of the answer. Throws `QuotaRpcTransportError` with a message for the user when - * the call fails. - */ -export async function callQuotaRpc( - endpoint: OpenCodeServiceEndpoint, - method: keyof typeof QuotaRpc.methods, - input: unknown, - options: { directory: string; timeoutMs: number }, -): Promise { - let response: Response; - let text: string; - try { - response = await fetch( - new URL(`/api/rpc/${encodeURIComponent(QUOTA_RPC_ID)}/${method}`, endpoint.url), - { - method: "POST", - headers: { - ...endpoint.headers, - "content-type": "application/json", - "x-opencode-directory": encodeURIComponent(options.directory), - }, - body: JSON.stringify({ input }), - signal: AbortSignal.timeout(options.timeoutMs), - }, - ); - text = await response.text(); - } catch (error) { - if (error instanceof Error && error.name === "TimeoutError") { - throw new QuotaRpcTransportError( - `OpenCode did not answer within ${options.timeoutMs / 1000} s. Try again.`, - ); - } - // Node's fetch reports "fetch failed" and keeps the reason (for example ECONNREFUSED) in `cause`. - const cause = error instanceof Error && error.cause instanceof Error ? error.cause : error; - const message = cause instanceof Error ? cause.message : String(cause); - throw new QuotaRpcTransportError( - `Could not reach OpenCode: ${sanitizeSingleLineDisplayText(message)}`, - ); - } - let body: unknown; - try { - body = JSON.parse(text); - } catch { - body = undefined; - } - - if (response.status === 401) { - throw new QuotaRpcTransportError( - "OpenCode's service password changed. Restart OpenCode and try again.", - ); - } - if (!response.ok) { - if (isRecord(body) && body.type === "rpc.unavailable") { - throw new QuotaRpcTransportError( - "OpenCode Quota's server plugin is not loaded. Check the plugin entry in opencode.json and restart OpenCode.", - ); - } - const message = - isRecord(body) && typeof body.message === "string" - ? sanitizeSingleLineDisplayText(body.message) - : `HTTP ${response.status}`; - throw new QuotaRpcTransportError(`OpenCode could not run OpenCode Quota: ${message}`); - } - if (!isRecord(body) || !("output" in body)) { - throw new QuotaRpcTransportError("OpenCode returned an answer without an output."); - } - return body.output; -} diff --git a/src/plugin.ts b/src/plugin.ts index 6650f9af..5fb61a91 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,10 +1,9 @@ /** * OpenCode V2 server plugin: quota slash commands for every client, a diagnostics tool, and - * the quota RPC that computes the TUI surfaces and the terminal command's reports. It is the - * only place logins are read, through OpenCode's integration API. + * the quota RPC that computes the TUI surfaces. Inside OpenCode, logins are read only here, + * through OpenCode's integration API. */ import { Plugin } from "@opencode/plugin"; -import { buildCliShowJson, buildCliShowText, buildCliStatus } from "./lib/cli-reports.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { @@ -26,17 +25,14 @@ import { type QuotaReportMetadata, removeQuotaReports, } from "./lib/quota-report-message.js"; -import { - type QuotaSessionModelContext, - resolveQuotaRuntimeContext, -} from "./lib/quota-runtime-context.js"; +import type { QuotaSessionModelContext } from "./lib/quota-runtime-context.js"; import { getQuotaFooter, getQuotaMessage, type QuotaSurfaceHost, writeQuotaExportIfEnabled, } from "./lib/quota-surface-data.js"; -import { QuotaRpc, type QuotaRpcCliOutput, type QuotaRpcCommandOutput } from "./rpc.js"; +import { QuotaRpc, type QuotaRpcCommandOutput } from "./rpc.js"; type ModelMessage = { readonly role: string; @@ -180,38 +176,6 @@ export const QuotaToastPlugin = Plugin.define({ }; } }, - // `opencode-quota show` and `status` in a terminal. The command calls this at the home - // folder and prints the result, so a failure is returned as the command's stderr. - cli: async (input): Promise => { - try { - const runtime = await resolveQuotaRuntimeContext({ - client, - roots, - includeSessionMeta: false, - }); - switch (input.command) { - case "show": - return await buildCliShowText({ runtime, providerId: input.providerId }); - case "show-json": - return await buildCliShowJson({ - runtime, - providerId: input.providerId, - threshold: input.threshold, - }); - case "status": - return await buildCliStatus({ runtime, providerId: input.providerId, json: false }); - case "status-json": - return await buildCliStatus({ runtime, providerId: input.providerId, json: true }); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - const failure = - input.command === "status" || input.command === "status-json" - ? "Failed to generate quota status" - : "Failed to show quota"; - return { exitCode: 1, stdout: "", stderr: `${failure}: ${message}\n` }; - } - }, }); await ctx.tool.transform((editor) => { diff --git a/src/rpc.ts b/src/rpc.ts index 9e610df6..b75565d8 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -34,12 +34,6 @@ export type QuotaRpcCommandInput = { sessionID?: string; }; export type QuotaRpcCommandOutput = QuotaDialogCommandOutputResult; -export type QuotaRpcCliInput = { - command: "show" | "show-json" | "status" | "status-json"; - providerId?: string; - threshold?: number; -}; -export type QuotaRpcCliOutput = { exitCode: number; stdout: string; stderr: string }; type StandardValueSchema = Extract; type QuotaRpcSchema = StandardValueSchema & { "~standard": { types?: { input: T; output: T } } }; @@ -77,7 +71,6 @@ function checkOptionalString(input: Record, field: string): str const SURFACES = ["sidebar", "idle", "compacted", "question"]; const FOOTER_SURFACES = ["prompt", "home"]; -const CLI_COMMANDS = ["show", "show-json", "status", "status-json"]; export const QuotaRpc = { id: QUOTA_RPC_ID, @@ -120,23 +113,5 @@ export const QuotaRpc = { }), output: schema(checkOutput), }, - cli: { - input: schema((value) => { - if (!isObject(value)) return "input must be an object"; - if (typeof value.command !== "string" || !CLI_COMMANDS.includes(value.command)) { - return `command must be one of ${CLI_COMMANDS.join(", ")}`; - } - if ( - value.threshold !== undefined && - (typeof value.threshold !== "number" || - !Number.isFinite(value.threshold) || - value.threshold <= 0) - ) { - return "threshold must be a positive finite number"; - } - return checkOptionalString(value, "providerId"); - }), - output: schema(checkOutput), - }, }, } satisfies Rpc.PortableDefinition; diff --git a/tests/helpers/credential-database.ts b/tests/helpers/credential-database.ts new file mode 100644 index 00000000..2a8f6de6 --- /dev/null +++ b/tests/helpers/credential-database.ts @@ -0,0 +1,53 @@ +import { DatabaseSync } from "node:sqlite"; + +/** One row of OpenCode 2's `credential` table. */ +export type CredentialDatabaseRow = { + id: string; + integrationId: string; + /** Defaults to `default`, the label OpenCode gives an unnamed connection. */ + label?: string; + active: 0 | 1 | null; + /** `time_updated`; newer rows come first among rows with the same `active`. */ + updated: number; + /** Stored as JSON in the `value` column. */ + value: unknown; +}; + +/** + * Creates OpenCode 2's `credential` table in a new database file (use a temp dir) and + * inserts `rows`. + */ +export function writeCredentialDatabase( + databasePath: string, + rows: readonly CredentialDatabaseRow[], +): void { + const database = new DatabaseSync(databasePath); + try { + database.exec(`CREATE TABLE credential ( + id TEXT PRIMARY KEY, + integration_id TEXT, + label TEXT NOT NULL, + value TEXT NOT NULL, + connector_id TEXT, + method_id TEXT, + active INTEGER, + time_created INTEGER NOT NULL, + time_updated INTEGER NOT NULL + )`); + const insert = database.prepare( + "INSERT INTO credential VALUES (?, ?, ?, ?, NULL, NULL, ?, 1, ?)", + ); + for (const row of rows) { + insert.run( + row.id, + row.integrationId, + row.label ?? "default", + JSON.stringify(row.value), + row.active, + row.updated, + ); + } + } finally { + database.close(); + } +} diff --git a/tests/lib.cli-reports.test.ts b/tests/lib.cli-reports.test.ts index 4809a4bd..46f29bd3 100644 --- a/tests/lib.cli-reports.test.ts +++ b/tests/lib.cli-reports.test.ts @@ -100,7 +100,8 @@ describe("CLI reports", () => { let workspaceDir: string; let savedConfigDir: string | undefined; - // The server plugin's configuration client: OpenCode's provider list, no SDK config. + // A minimal configuration client: the given provider ids, no SDK config. Quota settings + // come from the files in the workspace folder. async function runtimeFor(providerIds: string[] = []) { return resolveQuotaRuntimeContext({ client: { diff --git a/tests/lib.cli-show.test.ts b/tests/lib.cli-show.test.ts index 4e2d1185..3c7432d2 100644 --- a/tests/lib.cli-show.test.ts +++ b/tests/lib.cli-show.test.ts @@ -1,20 +1,58 @@ -import { homedir } from "node:os"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const TEST_ACCOUNTING = { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", +} as const; + +const { authMocks, mockProviders, runtimeDirs } = vi.hoisted(() => ({ + authMocks: { + anthropicConfigured: false, + kimiGlobalState: "none" as "none" | "configured", + kimiCnState: "none" as "none" | "configured", + }, + mockProviders: [] as any[], + runtimeDirs: { + value: { + dataDir: "/tmp/opencode-quota-cli-show-data", + configDir: "/tmp/opencode-quota-cli-show-config", + cacheDir: "/tmp/opencode-quota-cli-show-cache", + stateDir: "/tmp/opencode-quota-cli-show-state", + }, + }, +})); -vi.mock("../src/lib/opencode-service.js", () => ({ - discoverOpenCodeService: vi.fn(), +vi.mock("../src/lib/anthropic.js", () => ({ + hasAnthropicCredentialsConfigured: vi.fn(async () => authMocks.anthropicConfigured), })); -vi.mock("../src/lib/quota-rpc-client.js", async (importOriginal) => ({ - ...(await importOriginal()), - callQuotaRpc: vi.fn(), +vi.mock("../src/lib/kimi-auth.js", () => ({ + DEFAULT_KIMI_AUTH_CACHE_MAX_AGE_MS: 30_000, + resolveKimiGlobalAuthCached: vi.fn(async () => ({ state: authMocks.kimiGlobalState })), + resolveKimiCnAuthCached: vi.fn(async () => ({ state: authMocks.kimiCnState })), })); -import { runCliShowCommand } from "../src/lib/cli-show.js"; -import { discoverOpenCodeService } from "../src/lib/opencode-service.js"; -import { callQuotaRpc, QuotaRpcTransportError } from "../src/lib/quota-rpc-client.js"; +vi.mock("../src/providers/registry.js", () => ({ + getProviders: () => mockProviders, +})); -const ENDPOINT = { url: "http://127.0.0.1:4096", headers: { authorization: "Basic abc" } }; +vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ + getOpencodeRuntimeDirs: () => runtimeDirs.value, +})); + +import { createCliQuotaClient, runCliShowCommand } from "../src/lib/cli-show.js"; +import { + clearReadAuthFileCacheForTests, + getCredentialSourceDiagnostics, + readCredentialRows, +} from "../src/lib/opencode-auth.js"; +import { __resetQuotaStateForTests } from "../src/lib/quota-state.js"; +import { writeCredentialDatabase } from "./helpers/credential-database.js"; function createCaptureStream() { let output = ""; @@ -31,179 +69,503 @@ function createCaptureStream() { }; } -async function runShow(argv: string[]) { - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const code = await runCliShowCommand({ - argv, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - return { code, stdout: stdout.output, stderr: stderr.output }; -} - describe("runCliShowCommand", () => { + let tempDir: string; + let globalConfigDir: string; + let workspaceDir: string; + let savedConfigDir: string | undefined; + beforeEach(() => { - vi.mocked(discoverOpenCodeService).mockReset().mockResolvedValue(ENDPOINT); - vi.mocked(callQuotaRpc) - .mockReset() - .mockResolvedValue({ exitCode: 0, stdout: "Synthetic Weekly 75%\n", stderr: "" }); + authMocks.anthropicConfigured = false; + authMocks.kimiGlobalState = "none"; + authMocks.kimiCnState = "none"; + savedConfigDir = process.env.OPENCODE_CONFIG_DIR; + delete process.env.OPENCODE_CONFIG_DIR; + tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-cli-show-")); + globalConfigDir = join(tempDir, "global-config", "opencode"); + workspaceDir = join(tempDir, "workspace"); + mkdirSync(globalConfigDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + runtimeDirs.value = { + dataDir: "/tmp/opencode-quota-cli-show-data", + configDir: globalConfigDir, + cacheDir: join(tempDir, "cache"), + stateDir: "/tmp/opencode-quota-cli-show-state", + }; + vi.stubEnv("OPENCODE_DB", join(tempDir, "opencode.db")); + mockProviders.length = 0; + __resetQuotaStateForTests(); + }); + + afterEach(() => { + vi.useRealTimers(); + if (savedConfigDir !== undefined) process.env.OPENCODE_CONFIG_DIR = savedConfigDir; + else delete process.env.OPENCODE_CONFIG_DIR; + mockProviders.length = 0; + __resetQuotaStateForTests(); + clearReadAuthFileCacheForTests(); + rmSync(tempDir, { recursive: true, force: true }); }); - it("asks the running OpenCode service for the report at the home folder and prints it", async () => { - const result = await runShow([]); + it("synthesizes offline Kimi runtime ids independently by region", async () => { + writeFileSync(join(workspaceDir, "opencode.json"), "{}", "utf8"); + + const unauthenticated = await createCliQuotaClient({ + configRootDir: workspaceDir, + }).config.providers(); + expect(unauthenticated.data?.providers).toEqual([]); + + authMocks.anthropicConfigured = true; + authMocks.kimiCnState = "configured"; + const cnOnly = await createCliQuotaClient({ + configRootDir: workspaceDir, + }).config.providers(); + expect(cnOnly.data?.providers).toEqual([{ id: "anthropic" }, { id: "kimi-code-plan-cn" }]); + + authMocks.kimiGlobalState = "configured"; + const bothRegions = await createCliQuotaClient({ + configRootDir: workspaceDir, + }).config.providers(); + expect(bothRegions.data?.providers).toEqual([ + { id: "anthropic" }, + { id: "kimi-code-plan-global" }, + { id: "kimi-code-plan-cn" }, + ]); + }); - expect(result).toEqual({ code: 0, stdout: "Synthetic Weekly 75%\n", stderr: "" }); - expect(callQuotaRpc).toHaveBeenCalledWith( - ENDPOINT, - "cli", - { command: "show", providerId: undefined, threshold: undefined }, - { directory: homedir(), timeoutMs: 60_000 }, + it("renders a compact quota glance and returns zero when quota rows are available", async () => { + const provider = { + id: "synthetic", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Synthetic Weekly", percentRemaining: 75 }], + errors: [], + }), + }; + mockProviders.push(provider); + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ + experimental: { + quotaToast: { + enabledProviders: ["synthetic"], + showSessionTokens: true, + }, + }, + }), + "utf8", ); - }); - it("forwards --json and --threshold and returns the report's exit code", async () => { - vi.mocked(callQuotaRpc).mockResolvedValue({ - exitCode: 2, - stdout: '{\n "version": 2\n}\n', - stderr: "", + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: [], + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, }); - const result = await runShow(["--json", "--threshold", "5"]); + expect(code).toBe(0); + expect(stdout.output).toContain("Synthetic Weekly"); + expect(stdout.output).toContain("75%"); + expect(stderr.output).toBe(""); + expect(provider.fetch).toHaveBeenCalledOnce(); + }); - expect(result).toEqual({ code: 2, stdout: '{\n "version": 2\n}\n', stderr: "" }); - expect(callQuotaRpc).toHaveBeenCalledWith( - ENDPOINT, - "cli", - { command: "show-json", providerId: undefined, threshold: 5 }, - expect.anything(), + it("reads logins from OpenCode's database, read-only, only while the command runs", async () => { + const databasePath = join(tempDir, "opencode.db"); + writeCredentialDatabase(databasePath, [ + { + id: "cred_synthetic", + integrationId: "synthetic", + active: 1, + updated: 1, + value: { type: "key", key: "synthetic-key" }, + }, + ]); + const databaseBefore = readFileSync(databasePath); + let rowsDuringRun: unknown; + let sourceDuringRun: unknown; + const provider = { + id: "synthetic", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn(async () => { + rowsDuringRun = await readCredentialRows(["synthetic"]); + sourceDuringRun = getCredentialSourceDiagnostics(); + return { + attempted: true, + entries: [ + { accounting: TEST_ACCOUNTING, name: "Synthetic Weekly", percentRemaining: 75 }, + ], + errors: [], + }; + }), + }; + mockProviders.push(provider); + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { enabledProviders: ["synthetic"] } } }), + "utf8", ); + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: [], + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, + }); + + expect(code).toBe(0); + expect(stderr.output).toBe(""); + expect(rowsDuringRun).toEqual([ + { + id: "cred_synthetic", + integrationId: "synthetic", + label: "default", + active: true, + value: { type: "api", key: "synthetic-key" }, + }, + ]); + expect(sourceDuringRun).toMatchObject({ state: "bound", kind: "sqlite" }); + expect(getCredentialSourceDiagnostics().state).toBe("unbound"); + expect(readFileSync(databasePath).equals(databaseBefore)).toBe(true); }); - it("prints the report's stderr and exit code when the server reports an error", async () => { - vi.mocked(callQuotaRpc).mockResolvedValue({ - exitCode: 1, - stdout: "", - stderr: "Quota disabled in config (enabled: false).\n", - }); + it("normalizes --provider aliases and uses the provider as an invocation override", async () => { + const copilotProvider = { + id: "copilot", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 50 }], + errors: [], + }), + }; + const openAiProvider = { + id: "openai", + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ attempted: true, entries: [], errors: [] }), + }; + mockProviders.push(openAiProvider, copilotProvider); + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { enabledProviders: ["openai"] } } }), + "utf8", + ); - const result = await runShow([]); + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); - expect(result).toEqual({ - code: 1, - stdout: "", - stderr: "Quota disabled in config (enabled: false).\n", + const code = await runCliShowCommand({ + argv: ["--provider=github-copilot"], + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, }); - }); - it("normalizes --provider aliases before calling OpenCode", async () => { - const result = await runShow(["--provider=github-copilot"]); - - expect(result.code).toBe(0); - expect(callQuotaRpc).toHaveBeenCalledWith( - ENDPOINT, - "cli", - { command: "show", providerId: "copilot", threshold: undefined }, - expect.anything(), - ); + expect(code).toBe(0); + expect(stdout.output).toContain("Copilot"); + expect(copilotProvider.fetch).toHaveBeenCalledOnce(); + expect(openAiProvider.fetch).not.toHaveBeenCalled(); + expect(stderr.output).toBe(""); }); - it("exits 3 with an empty stdout when OpenCode is not running", async () => { - vi.mocked(discoverOpenCodeService).mockResolvedValue(undefined); + it("rejects an unknown provider before probing providers", async () => { + const provider = { + id: "copilot", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn(), + }; + mockProviders.push(provider); + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: ["--provider", "not-a-provider"], + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, + }); - const result = await runShow(["--json"]); + expect(code).toBe(1); + expect(stdout.output).toBe(""); + expect(stderr.output).toContain("Unknown provider: not-a-provider"); + expect(provider.isAvailable).not.toHaveBeenCalled(); + expect(provider.fetch).not.toHaveBeenCalled(); + }); - expect(result).toEqual({ - code: 3, - stdout: "", - stderr: "OpenCode is not running. Start OpenCode and try again.\n", + it("rejects missing provider values", async () => { + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: ["--provider"], + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, }); - expect(callQuotaRpc).not.toHaveBeenCalled(); + + expect(code).toBe(1); + expect(stdout.output).toBe(""); + expect(stderr.output).toContain("Missing value for --provider"); + expect(stderr.output).toContain("opencode-quota show"); }); - it("exits 3 with an empty stdout when the call to OpenCode fails", async () => { - vi.mocked(callQuotaRpc).mockRejectedValue( - new QuotaRpcTransportError( - "OpenCode Quota's server plugin is not loaded. Check the plugin entry in opencode.json and restart OpenCode.", - ), + it("returns non-zero when quota is disabled in config", async () => { + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), + "utf8", ); + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: [], + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, + }); - const result = await runShow(["--json"]); + expect(code).toBe(1); + expect(stdout.output).toBe(""); + expect(stderr.output).toContain("Quota disabled in config"); + }); - expect(result).toEqual({ - code: 3, - stdout: "", - stderr: - "OpenCode Quota's server plugin is not loaded. Check the plugin entry in opencode.json and restart OpenCode.\n", + it("prefers the git worktree root over a nested cwd for config loading", async () => { + const nestedDir = join(workspaceDir, "packages", "app"); + mkdirSync(nestedDir, { recursive: true }); + mkdirSync(join(workspaceDir, ".git")); + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), + "utf8", + ); + writeFileSync( + join(nestedDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { enabled: true } } }), + "utf8", + ); + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: [], + cwd: nestedDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, }); - }); - it("exits 3 when OpenCode returns an output that is not a report", async () => { - vi.mocked(callQuotaRpc).mockResolvedValue({ exitCode: 0, stdout: 5 }); + expect(code).toBe(1); + expect(stdout.output).toBe(""); + expect(stderr.output).toContain("Quota disabled in config"); + }); - const result = await runShow([]); + it("reads worktree root config even when OPENCODE_CONFIG_DIR is set", async () => { + const nestedDir = join(workspaceDir, "packages", "app"); + const provider = { + id: "synthetic", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn(), + }; + mockProviders.push(provider); + mkdirSync(nestedDir, { recursive: true }); + mkdirSync(join(workspaceDir, ".git")); + process.env.OPENCODE_CONFIG_DIR = ".opencode"; + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), + "utf8", + ); + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: [], + cwd: nestedDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, + }); - expect(result.code).toBe(3); - expect(result.stdout).toBe(""); - expect(result.stderr).toContain("returned an unexpected result"); + expect(code).toBe(1); + expect(stdout.output).toBe(""); + expect(stderr.output).toContain("Quota disabled in config"); + expect(provider.fetch).not.toHaveBeenCalled(); }); - it("rejects an unknown provider before contacting OpenCode", async () => { - const result = await runShow(["--provider", "not-a-provider"]); + it("uses root-level OpenCode provider ids for standalone provider availability", async () => { + const provider = { + id: "copilot", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn(async (ctx: any) => { + const response = await ctx.client.config.providers(); + return response.data.providers.some((item: { id: string }) => item.id === "github-copilot"); + }), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Copilot", percentRemaining: 88 }], + errors: [], + }), + }; + mockProviders.push(provider); + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ provider: { "github-copilot": {} } }), + "utf8", + ); + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: [], + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, + }); - expect(result.code).toBe(1); - expect(result.stdout).toBe(""); - expect(result.stderr).toContain("Unknown provider: not-a-provider"); - expect(discoverOpenCodeService).not.toHaveBeenCalled(); - expect(callQuotaRpc).not.toHaveBeenCalled(); + expect(code).toBe(0); + expect(stdout.output).toContain("Copilot"); + expect(provider.fetch).toHaveBeenCalledOnce(); + expect(stderr.output).toBe(""); }); - it("rejects missing provider values", async () => { - const result = await runShow(["--provider"]); + // ────────────────────────────────────────────── + // --json / --threshold tests + // ────────────────────────────────────────────── + + it("--json outputs valid JSON to stdout with cached provider data", async () => { + const provider = { + id: "synthetic", + cachePolicy: { kind: "account-neutral" as const }, + isAvailable: vi.fn().mockResolvedValue(true), + fetch: vi.fn().mockResolvedValue({ + attempted: true, + entries: [{ accounting: TEST_ACCOUNTING, name: "Synthetic", percentRemaining: 75 }], + errors: [], + }), + }; + mockProviders.push(provider); + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ + experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, + }), + "utf8", + ); - expect(result.code).toBe(1); - expect(result.stdout).toBe(""); - expect(result.stderr).toContain("Missing value for --provider"); - expect(result.stderr).toContain("opencode-quota show"); - expect(discoverOpenCodeService).not.toHaveBeenCalled(); + // Run non-JSON show first to populate the cache. + const textOut = createCaptureStream(); + const textErr = createCaptureStream(); + const textCode = await runCliShowCommand({ + argv: [], + cwd: workspaceDir, + stdout: textOut.stream as any, + stderr: textErr.stream as any, + }); + expect(textCode).toBe(0); + expect(provider.fetch).toHaveBeenCalledOnce(); + + // Now run --json (reads from cache). + const jsonOut = createCaptureStream(); + const jsonErr = createCaptureStream(); + const jsonCode = await runCliShowCommand({ + argv: ["--json"], + cwd: workspaceDir, + stdout: jsonOut.stream as any, + stderr: jsonErr.stream as any, + }); + + expect(jsonCode).toBe(0); + expect(jsonErr.output).toBe(""); + + const parsed = JSON.parse(jsonOut.output); + expect(parsed).toHaveProperty("version", 2); + expect(parsed).toHaveProperty("exportedAt"); + expect(parsed).toHaveProperty("fromCache", true); + expect(parsed).toHaveProperty("cacheAgeSeconds"); + expect(parsed.providers).toHaveProperty("synthetic"); + expect(parsed.providers.synthetic.status).toBe("ok"); + expect(parsed.providers.synthetic.entries[0].name).toBe("Synthetic"); + expect(parsed.providers.synthetic.entries[0].percentRemaining).toBe(75); + expect(parsed.providers.synthetic.entries[0].renderType).toBe("percent"); + expect(parsed.providers.synthetic.entries[0]).not.toHaveProperty("unlimited"); + expect(provider.fetch).toHaveBeenCalledTimes(1); // still only called from text path }); it("reports unknown flag with --json as error on stderr with exit code 1", async () => { - const result = await runShow(["--json", "--bogus-flag"]); + const jsonOut = createCaptureStream(); + const jsonErr = createCaptureStream(); + + const jsonCode = await runCliShowCommand({ + argv: ["--json", "--bogus-flag"], + cwd: workspaceDir, + stdout: jsonOut.stream as any, + stderr: jsonErr.stream as any, + }); - expect(result.code).toBe(1); - expect(result.stderr).toContain("Unknown option: --bogus-flag"); - expect(result.stderr).toContain("opencode-quota show"); + expect(jsonCode).toBe(1); + expect(jsonErr.output).toContain("Unknown option: --bogus-flag"); + expect(jsonErr.output).toContain("opencode-quota show"); }); it("--threshold validates input and requires --json", async () => { - let result = await runShow(["--json", "--threshold", "abc"]); + const run = async (argv: string[]) => { + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + const code = await runCliShowCommand({ + argv, + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, + }); + return { code, stderr: stderr.output }; + }; + + // Invalid value + let result = await run(["--json", "--threshold", "abc"]); expect(result.code).toBe(1); expect(result.stderr).toContain("--threshold must be a positive finite number"); - result = await runShow(["--json", "--threshold", "0"]); + // Zero + result = await run(["--json", "--threshold", "0"]); expect(result.code).toBe(1); expect(result.stderr).toContain("--threshold must be a positive finite number"); - result = await runShow(["--json", "--threshold"]); + // Missing value + result = await run(["--json", "--threshold"]); expect(result.code).toBe(1); expect(result.stderr).toContain("Missing value for --threshold"); - result = await runShow(["--threshold", "5"]); + // Without --json + result = await run(["--threshold", "5"]); expect(result.code).toBe(1); expect(result.stderr).toContain("--threshold requires --json"); - - expect(discoverOpenCodeService).not.toHaveBeenCalled(); }); - it("prints help that names the OpenCode requirement and exit code 3", async () => { - const result = await runShow(["--help"]); + it("prints help without an OpenCode requirement or exit code 3", async () => { + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + + const code = await runCliShowCommand({ + argv: ["--help"], + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, + }); - expect(result.code).toBe(0); - expect(result.stderr).toBe(""); - expect(result.stdout).toContain("opencode-quota show"); - expect(result.stdout).toContain("Needs OpenCode running"); - expect(result.stdout).toContain("Exit code 3"); - expect(discoverOpenCodeService).not.toHaveBeenCalled(); + expect(code).toBe(0); + expect(stderr.output).toBe(""); + expect(stdout.output).toContain("opencode-quota show"); + expect(stdout.output).not.toContain("OpenCode running"); + expect(stdout.output).not.toMatch(/exit code 3/i); }); }); diff --git a/tests/lib.cli-status.test.ts b/tests/lib.cli-status.test.ts index d7629bf8..609132bf 100644 --- a/tests/lib.cli-status.test.ts +++ b/tests/lib.cli-status.test.ts @@ -1,20 +1,51 @@ -import { homedir } from "node:os"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockProviders, runtimeDirs, statusData } = vi.hoisted(() => ({ + mockProviders: [] as any[], + runtimeDirs: { + value: { + dataDir: "/tmp/opencode-quota-cli-status-data", + configDir: "/tmp/opencode-quota-cli-status-config", + cacheDir: "/tmp/opencode-quota-cli-status-cache", + stateDir: "/tmp/opencode-quota-cli-status-state", + }, + }, + statusData: { + value: null as null | { + output: string; + payload: Record; + hasComparableProviderData?: boolean; + }, + }, +})); -vi.mock("../src/lib/opencode-service.js", () => ({ - discoverOpenCodeService: vi.fn(), +vi.mock("../src/providers/registry.js", () => ({ + getProviders: () => mockProviders, })); -vi.mock("../src/lib/quota-rpc-client.js", async (importOriginal) => ({ - ...(await importOriginal()), - callQuotaRpc: vi.fn(), +vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ + getOpencodeRuntimeDirs: () => runtimeDirs.value, })); -import { runCliStatusCommand } from "../src/lib/cli-status.js"; -import { discoverOpenCodeService } from "../src/lib/opencode-service.js"; -import { callQuotaRpc, QuotaRpcTransportError } from "../src/lib/quota-rpc-client.js"; +vi.mock("../src/lib/quota-dialog-commands.js", () => ({ + buildStatusReportData: vi.fn(async () => { + if (!statusData.value) { + return { output: null, payload: null }; + } + return { + output: statusData.value.output, + payload: statusData.value.payload, + hasComparableProviderData: statusData.value.hasComparableProviderData ?? true, + }; + }), +})); -const ENDPOINT = { url: "http://127.0.0.1:4096", headers: {} }; +import { runCliStatusCommand } from "../src/lib/cli-status.js"; +import { getCredentialSourceDiagnostics } from "../src/lib/opencode-auth.js"; +import { buildStatusReportData } from "../src/lib/quota-dialog-commands.js"; function createCaptureStream() { let output = ""; @@ -31,113 +62,189 @@ function createCaptureStream() { }; } -async function runStatus(argv: string[]) { - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const code = await runCliStatusCommand({ - argv, - stdout: stdout.stream as any, - stderr: stderr.stream as any, - }); - return { code, stdout: stdout.output, stderr: stderr.output }; +function basePayload(overrides: Record = {}) { + return { + version: "3.11.2", + generatedAt: "2026-07-16T00:00:00.000Z", + config: { + configSource: "workspace", + configPaths: ["/tmp/opencode.json"], + enabledProviders: ["synthetic"], + onlyCurrentModel: false, + pricingSnapshotSource: "auto", + }, + providers: [ + { id: "synthetic", enabled: true, available: true, matchesCurrentModel: undefined }, + ], + pricing: { + selection: "auto", + activeSource: "bundled", + snapshot: { + source: "bundled", + generatedAt: "2026-01-01T00:00:00.000Z", + units: "USD per 1M tokens", + }, + snapshotPath: "/tmp/pricing.json", + refreshStatePath: "/tmp/pricing-state.json", + }, + liveProbes: [{ id: "synthetic", ok: true }], + ...overrides, + }; +} + +function useStatusData( + payload = basePayload(), + hasComparableProviderData?: boolean, + output = "report text", +): void { + statusData.value = { output, payload, hasComparableProviderData }; } describe("runCliStatusCommand", () => { - beforeEach(() => { - vi.mocked(discoverOpenCodeService).mockReset().mockResolvedValue(ENDPOINT); - vi.mocked(callQuotaRpc).mockReset().mockResolvedValue({ - exitCode: 0, - stdout: "Quota Status (opencode-quota v5.0.0)\n", - stderr: "", + let tempDir: string; + let globalConfigDir: string; + let workspaceDir: string; + let savedConfigDir: string | undefined; + + async function runStatus(argv: string[]) { + const stdout = createCaptureStream(); + const stderr = createCaptureStream(); + const code = await runCliStatusCommand({ + argv, + cwd: workspaceDir, + stdout: stdout.stream as any, + stderr: stderr.stream as any, }); + return { code, stdout: stdout.output, stderr: stderr.output }; + } + + beforeEach(() => { + savedConfigDir = process.env.OPENCODE_CONFIG_DIR; + delete process.env.OPENCODE_CONFIG_DIR; + tempDir = mkdtempSync(join(tmpdir(), "opencode-quota-cli-status-")); + globalConfigDir = join(tempDir, "global-config", "opencode"); + workspaceDir = join(tempDir, "workspace"); + mkdirSync(globalConfigDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + runtimeDirs.value = { + dataDir: "/tmp/opencode-quota-cli-status-data", + configDir: globalConfigDir, + cacheDir: join(tempDir, "cache"), + stateDir: "/tmp/opencode-quota-cli-status-state", + }; + vi.stubEnv("OPENCODE_DB", join(tempDir, "opencode.db")); + mockProviders.length = 0; + statusData.value = null; + vi.mocked(buildStatusReportData).mockClear(); + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ + experimental: { quotaToast: { enabledProviders: ["synthetic"] } }, + }), + "utf8", + ); }); - it("asks the running OpenCode service for the report at the home folder and prints it", async () => { + afterEach(() => { + if (savedConfigDir !== undefined) process.env.OPENCODE_CONFIG_DIR = savedConfigDir; + else delete process.env.OPENCODE_CONFIG_DIR; + mockProviders.length = 0; + statusData.value = null; + rmSync(tempDir, { recursive: true, force: true }); + }); + + it("prints a plain-text Quota Status report and returns zero", async () => { + useStatusData( + basePayload(), + undefined, + "Quota Status (opencode-quota v3.11.2)\ntoast:\n- enabledProviders: synthetic", + ); const result = await runStatus([]); - expect(result).toEqual({ - code: 0, - stdout: "Quota Status (opencode-quota v5.0.0)\n", - stderr: "", - }); - expect(callQuotaRpc).toHaveBeenCalledWith( - ENDPOINT, - "cli", - { command: "status", providerId: undefined }, - { directory: homedir(), timeoutMs: 60_000 }, + expect(result.code).toBe(0); + expect(result.stdout).toContain("Quota Status"); + expect(result.stdout).toContain("enabledProviders: synthetic"); + expect(result.stderr).toBe(""); + expect(buildStatusReportData).toHaveBeenCalledWith( + expect.objectContaining({ providerFilterId: undefined }), ); }); - it("forwards --json and returns the report's exit code", async () => { - vi.mocked(callQuotaRpc).mockResolvedValue({ - exitCode: 2, - stdout: '{\n "liveProbes": []\n}\n', - stderr: "", - }); + it("--json emits a structured payload and returns zero when live probes exist", async () => { + useStatusData(); + const result = await runStatus(["--json"]); + + expect(result.code).toBe(0); + expect(result.stderr).toBe(""); + const parsed = JSON.parse(result.stdout); + expect(parsed).toHaveProperty("version", "3.11.2"); + expect(parsed).toHaveProperty("generatedAt"); + expect(parsed).toHaveProperty("config"); + expect(parsed).toHaveProperty("providers"); + expect(parsed).toHaveProperty("pricing"); + expect(parsed).toHaveProperty("liveProbes"); + expect(parsed.liveProbes).toHaveLength(1); + expect(parsed.liveProbes[0]).toEqual({ id: "synthetic", ok: true }); + }); + it("--json exits 2 when there is no comparable provider data", async () => { + useStatusData(basePayload({ liveProbes: [] }), false); const result = await runStatus(["--json"]); - expect(result).toEqual({ code: 2, stdout: '{\n "liveProbes": []\n}\n', stderr: "" }); - expect(callQuotaRpc).toHaveBeenCalledWith( - ENDPOINT, - "cli", - { command: "status-json", providerId: undefined }, - expect.anything(), - ); + expect(result.code).toBe(2); + expect(result.stderr).toBe(""); + // JSON still prints on exit 2. + const parsed = JSON.parse(result.stdout); + expect(parsed.liveProbes).toEqual([]); }); - it("--provider --json forwards the provider filter", async () => { - const result = await runStatus(["--json", "--provider", "synthetic"]); + it("--json exits 2 when probes fail without producing quota entries", async () => { + useStatusData(basePayload({ liveProbes: [{ id: "synthetic", ok: false }] }), false); + const result = await runStatus(["--json"]); + + expect(result.code).toBe(2); + expect(JSON.parse(result.stdout).liveProbes).toEqual([{ id: "synthetic", ok: false }]); + }); + + it("--json succeeds when a partial probe produced quota entries", async () => { + useStatusData(basePayload({ liveProbes: [{ id: "synthetic", ok: false }] }), true); + + expect((await runStatus(["--json"])).code).toBe(0); + }); + + it("--provider filters the report to one provider", async () => { + useStatusData(); + const result = await runStatus(["--provider", "synthetic"]); expect(result.code).toBe(0); - expect(callQuotaRpc).toHaveBeenCalledWith( - ENDPOINT, - "cli", - { command: "status-json", providerId: "synthetic" }, - expect.anything(), + expect(result.stderr).toBe(""); + expect(buildStatusReportData).toHaveBeenCalledWith( + expect.objectContaining({ providerFilterId: "synthetic" }), ); }); it("resolves a case-insensitive provider synonym before filtering", async () => { + useStatusData(); const result = await runStatus(["--provider", " CLAUDE "]); expect(result.code).toBe(0); expect(result.stderr).toBe(""); - expect(callQuotaRpc).toHaveBeenCalledWith( - ENDPOINT, - "cli", - { command: "status", providerId: "anthropic" }, - expect.anything(), + expect(buildStatusReportData).toHaveBeenCalledWith( + expect.objectContaining({ providerFilterId: "anthropic" }), ); }); - it("exits 3 with an empty stdout when OpenCode is not running", async () => { - vi.mocked(discoverOpenCodeService).mockResolvedValue(undefined); - - const result = await runStatus(["--json"]); - - expect(result).toEqual({ - code: 3, - stdout: "", - stderr: "OpenCode is not running. Start OpenCode and try again.\n", - }); - expect(callQuotaRpc).not.toHaveBeenCalled(); - }); + it("--provider --json forwards the provider filter and still emits JSON", async () => { + useStatusData(); + const result = await runStatus(["--json", "--provider", "synthetic"]); - it("exits 3 with an empty stdout when the call to OpenCode fails", async () => { - vi.mocked(callQuotaRpc).mockRejectedValue( - new QuotaRpcTransportError( - "OpenCode's service password changed. Restart OpenCode and try again.", - ), + expect(result.code).toBe(0); + expect(buildStatusReportData).toHaveBeenCalledWith( + expect.objectContaining({ providerFilterId: "synthetic" }), ); - - const result = await runStatus([]); - - expect(result).toEqual({ - code: 3, - stdout: "", - stderr: "OpenCode's service password changed. Restart OpenCode and try again.\n", - }); + const parsed = JSON.parse(result.stdout); + expect(parsed.providers).toHaveLength(1); + expect(parsed).not.toHaveProperty("providerFilterId"); }); it("rejects --threshold with a redirect to show --json --threshold", async () => { @@ -147,7 +254,7 @@ describe("runCliStatusCommand", () => { expect(result.stdout).toBe(""); expect(result.stderr).toContain("--threshold is not supported by status"); expect(result.stderr).toContain("opencode-quota show --json --threshold"); - expect(discoverOpenCodeService).not.toHaveBeenCalled(); + expect(buildStatusReportData).not.toHaveBeenCalled(); }); it("rejects --threshold even when combined with --json", async () => { @@ -155,17 +262,16 @@ describe("runCliStatusCommand", () => { expect(result.code).toBe(1); expect(result.stderr).toContain("--threshold is not supported by status"); - expect(discoverOpenCodeService).not.toHaveBeenCalled(); + expect(buildStatusReportData).not.toHaveBeenCalled(); }); - it("rejects an unknown provider before contacting OpenCode", async () => { + it("rejects an unknown provider before building the report", async () => { const result = await runStatus(["--provider", "not-a-provider"]); expect(result.code).toBe(1); expect(result.stdout).toBe(""); expect(result.stderr).toContain("Unknown provider: not-a-provider"); - expect(discoverOpenCodeService).not.toHaveBeenCalled(); - expect(callQuotaRpc).not.toHaveBeenCalled(); + expect(buildStatusReportData).not.toHaveBeenCalled(); }); it("rejects a missing --provider value", async () => { @@ -184,17 +290,54 @@ describe("runCliStatusCommand", () => { expect(result.stderr).toContain("opencode-quota status"); }); + it("returns non-zero when quota is disabled in config", async () => { + writeFileSync( + join(workspaceDir, "opencode.json"), + JSON.stringify({ experimental: { quotaToast: { enabled: false } } }), + "utf8", + ); + const result = await runStatus([]); + + expect(result.code).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("Quota disabled in config"); + expect(buildStatusReportData).not.toHaveBeenCalled(); + }); + it("prints help and returns zero for --help", async () => { const result = await runStatus(["--help"]); expect(result.code).toBe(0); expect(result.stderr).toBe(""); expect(result.stdout).toContain("opencode-quota status"); - expect(result.stdout).toContain("Needs OpenCode running"); expect(result.stdout).toContain("Exit codes:"); - expect(result.stdout).toContain( - "3 OpenCode is not running or the plugin could not be reached", - ); - expect(discoverOpenCodeService).not.toHaveBeenCalled(); + expect(buildStatusReportData).not.toHaveBeenCalled(); + }); + + it("returns non-zero when report building throws", async () => { + vi.mocked(buildStatusReportData).mockRejectedValueOnce(new Error("boom")); + const result = await runStatus([]); + + expect(result.code).toBe(1); + expect(result.stderr).toContain("Failed to generate quota status: boom"); + expect(getCredentialSourceDiagnostics().state).toBe("unbound"); + }); + + it("builds the report with the database login source bound and unbinds it afterwards", async () => { + let sourceDuringRun: unknown; + vi.mocked(buildStatusReportData).mockImplementationOnce(async () => { + sourceDuringRun = getCredentialSourceDiagnostics(); + return { + output: "Quota Status (opencode-quota v3.11.2)", + payload: basePayload(), + hasComparableProviderData: true, + } as never; + }); + + const result = await runStatus([]); + + expect(result.code).toBe(0); + expect(sourceDuringRun).toMatchObject({ state: "bound", kind: "sqlite" }); + expect(getCredentialSourceDiagnostics().state).toBe("unbound"); }); }); diff --git a/tests/lib.opencode-auth-sqlite.test.ts b/tests/lib.opencode-auth-sqlite.test.ts new file mode 100644 index 00000000..caa0b5e5 --- /dev/null +++ b/tests/lib.opencode-auth-sqlite.test.ts @@ -0,0 +1,370 @@ +import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { resolveAgyAccounts } from "../src/lib/google-agy.js"; +import { resolveGeminiCliAccounts } from "../src/lib/google-gemini-cli.js"; +import { + bindCredentialSource, + clearReadAuthFileCacheForTests, + getCredentialSourceDiagnostics, + readAuthFile, + readCredentialRows, +} from "../src/lib/opencode-auth.js"; +import { createSqliteCredentialSource } from "../src/lib/opencode-auth-sqlite.js"; +import { + type CredentialDatabaseRow, + writeCredentialDatabase, +} from "./helpers/credential-database.js"; + +/** Three logins without an `active` flag, newest first: DeepSeek, OpenAI, Copilot. */ +const BASE_ROWS: CredentialDatabaseRow[] = [ + { + id: "copilot", + integrationId: "github-copilot", + active: null, + updated: 1, + value: { + type: "oauth", + access: "copilot-access", + refresh: "copilot-refresh", + expires: 10, + metadata: { enterpriseUrl: "example.ghe.com" }, + }, + }, + { + id: "openai", + integrationId: "openai", + active: null, + updated: 2, + value: { type: "oauth", access: "openai-access", refresh: "openai-refresh", expires: 20 }, + }, + { + id: "deepseek", + integrationId: "deepseek", + active: null, + updated: 3, + value: { type: "key", key: "deepseek-key" }, + }, +]; + +function withRow(id: string, changes: Partial): CredentialDatabaseRow[] { + return BASE_ROWS.map((row) => (row.id === id ? { ...row, ...changes } : row)); +} + +function sha256(path: string): string { + return createHash("sha256").update(readFileSync(path)).digest("hex"); +} + +let root: string; +let dataDir: string; +let databasePath: string; +let unbind: () => void; + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "opencode-quota-auth-sqlite-")); + dataDir = join(root, "opencode"); + mkdirSync(dataDir, { recursive: true }); + databasePath = join(dataDir, "opencode.db"); + vi.stubEnv("XDG_DATA_HOME", root); + vi.stubEnv("OPENCODE_DB", databasePath); + unbind = bindCredentialSource(createSqliteCredentialSource()); +}); + +afterEach(() => { + unbind(); + clearReadAuthFileCacheForTests(); + vi.unstubAllEnvs(); + rmSync(root, { recursive: true, force: true }); +}); + +describe("the terminal command's database login reader", () => { + it("reads OAuth and key logins from OpenCode's database, metadata on top and key as api", async () => { + writeCredentialDatabase(databasePath, BASE_ROWS); + + await expect( + readAuthFile({ integrationIds: ["github-copilot", "deepseek", "openai"] }), + ).resolves.toMatchObject({ + "github-copilot": { access: "copilot-access", enterpriseUrl: "example.ghe.com" }, + deepseek: { type: "api", key: "deepseek-key" }, + openai: { access: "openai-access" }, + }); + }); + + it("exposes every credential row with active rows first", async () => { + writeCredentialDatabase(databasePath, [ + ...withRow("openai", { label: "Work", active: 0 }), + { + id: "openai-active", + integrationId: "openai", + label: "Personal", + active: 1, + updated: 4, + value: { type: "oauth", access: "personal-access" }, + }, + ]); + + await expect(readCredentialRows(["openai"])).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "openai-active", + integrationId: "openai", + label: "Personal", + active: true, + value: expect.objectContaining({ access: "personal-access" }), + }), + expect.objectContaining({ id: "openai", active: false }), + ]), + ); + const rows = await readCredentialRows(["openai"]); + expect(rows.findIndex((row) => row.id === "openai-active")).toBeLessThan( + rows.findIndex((row) => row.id === "openai"), + ); + }); + + it("ignores legacy auth.json entries", async () => { + writeCredentialDatabase(databasePath, BASE_ROWS); + writeFileSync( + join(dataDir, "auth.json"), + JSON.stringify({ openai: { type: "oauth", access: "file-access" } }), + ); + + await expect( + readAuthFile({ integrationIds: ["github-copilot", "openai"] }), + ).resolves.toMatchObject({ + "github-copilot": { access: "copilot-access" }, + openai: { access: "openai-access" }, + }); + }); + + it("returns only rows of the requested ids, in database order", async () => { + writeCredentialDatabase(databasePath, BASE_ROWS); + + expect((await readCredentialRows(["openai"])).map((row) => row.id)).toEqual(["openai"]); + // Database order (most recently updated first), not request order. + expect((await readCredentialRows(["github-copilot", "openai"])).map((row) => row.id)).toEqual([ + "openai", + "copilot", + ]); + await expect(readCredentialRows([])).resolves.toEqual([]); + await expect(readAuthFile({ integrationIds: ["xai"] })).resolves.toBeNull(); + await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toEqual({ + openai: { type: "oauth", access: "openai-access", refresh: "openai-refresh", expires: 20 }, + }); + }); + + it("keeps only the requested connection methods", async () => { + writeCredentialDatabase(databasePath, [ + ...BASE_ROWS, + { + id: "console", + integrationId: "opencode", + active: 1, + updated: 5, + value: { type: "oauth", access: "console-access", refresh: "console-refresh", expires: 1 }, + }, + { + id: "workspace-key", + integrationId: "opencode", + active: 0, + updated: 4, + value: { type: "key", key: "workspace-key" }, + }, + ]); + + const ids = ["opencode", "openai", "deepseek"]; + expect((await readCredentialRows(ids, { methods: ["key"] })).map((row) => row.id)).toEqual([ + "workspace-key", + "deepseek", + ]); + expect((await readCredentialRows(ids, { methods: ["oauth"] })).map((row) => row.id)).toEqual([ + "console", + "openai", + ]); + expect( + (await readCredentialRows(ids, { methods: ["key", "oauth"] })).map((row) => row.id), + ).toEqual(["console", "workspace-key", "deepseek", "openai"]); + }); + + it("keeps the first (active) row per id when firstOnly is set", async () => { + writeCredentialDatabase(databasePath, [ + ...withRow("openai", { active: 1 }), + { + id: "openai-newer-inactive", + integrationId: "openai", + active: 0, + updated: 9, + value: { type: "oauth", access: "inactive-access" }, + }, + ]); + + expect( + (await readCredentialRows(["openai", "deepseek"], { firstOnly: true })).map((row) => row.id), + ).toEqual(["openai", "deepseek"]); + await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toMatchObject({ + openai: { access: "openai-access" }, + }); + }); + + it("applies firstOnly before the method filter, so a mismatched active row yields nothing", async () => { + writeCredentialDatabase(databasePath, [ + ...BASE_ROWS, + { + id: "openai-key", + integrationId: "openai", + active: 1, + updated: 9, + value: { type: "key", key: "openai-key" }, + }, + ]); + + await expect( + readCredentialRows(["openai"], { firstOnly: true, methods: ["oauth"] }), + ).resolves.toEqual([]); + }); + + it("parses the opencode-gemini-auth 2.x and AGY alpha OAuth rows", async () => { + writeCredentialDatabase(databasePath, [ + ...BASE_ROWS, + // opencode-gemini-auth 2.0.1: `gemini-cli` method on OpenCode 2's `google` integration. + { + id: "gemini", + integrationId: "google", + label: "user@example.com", + active: 1, + updated: 5, + value: { + type: "oauth", + methodID: "gemini-cli", + refresh: "gemini-refresh|gemini-project|", + access: "gemini-access", + expires: 50, + metadata: { email: "user@example.com" }, + }, + }, + // @anthonyhaussman/opencode-agy-auth 1.2.11-alpha.0: `oauth` method on `google-agy`. + { + id: "agy", + integrationId: "google-agy", + label: "OAuth", + active: 1, + updated: 5, + value: { + type: "oauth", + methodID: "oauth", + refresh: "agy-refresh|agy-project|agy-managed-project", + access: "agy-access", + expires: 60, + }, + }, + ]); + const auth = await readAuthFile({ integrationIds: ["google", "google-agy"] }); + + expect(resolveGeminiCliAccounts(auth)).toEqual([ + { + sourceKey: "google", + refreshToken: "gemini-refresh", + projectId: "gemini-project", + email: "user@example.com", + accessToken: "gemini-access", + expiresAt: 50, + }, + ]); + expect(resolveAgyAccounts(auth)).toEqual([ + { + sourceKey: "google-agy", + refreshToken: "agy-refresh", + projectId: "agy-managed-project", + accessToken: "agy-access", + expiresAt: 60, + }, + ]); + }); + + it("returns no rows for a missing database, a non-database file or OPENCODE_DB=:memory:, and never creates the file", async () => { + await expect(readCredentialRows(["openai"])).resolves.toEqual([]); + await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toBeNull(); + expect(existsSync(databasePath)).toBe(false); + + const notDatabasePath = join(dataDir, "not-a-database.db"); + writeFileSync(notDatabasePath, "not a SQLite database"); + vi.stubEnv("OPENCODE_DB", notDatabasePath); + await expect(readCredentialRows(["openai"])).resolves.toEqual([]); + expect(readFileSync(notDatabasePath, "utf8")).toBe("not a SQLite database"); + + vi.stubEnv("OPENCODE_DB", ":memory:"); + await expect(readCredentialRows(["openai"])).resolves.toEqual([]); + + expect(readdirSync(root)).toEqual(["opencode"]); + expect(readdirSync(dataDir)).toEqual(["not-a-database.db"]); + }); + + it("skips rows whose value is not a JSON object and never returns a failed row", async () => { + writeCredentialDatabase(databasePath, [ + { id: "string-value", integrationId: "openai", active: 1, updated: 5, value: "oauth" }, + { id: "array-value", integrationId: "openai", active: 0, updated: 4, value: ["oauth"] }, + { id: "null-value", integrationId: "openai", active: 0, updated: 3, value: null }, + { + id: "usable", + integrationId: "openai", + active: 0, + updated: 1, + value: { type: "oauth", access: "usable-access" }, + }, + ]); + const database = new DatabaseSync(databasePath); + database + .prepare( + "INSERT INTO credential VALUES ('broken-json', 'openai', 'default', '{broken', NULL, NULL, 0, 1, 2)", + ) + .run(); + database.close(); + + await expect(readCredentialRows(["openai"])).resolves.toEqual([ + { + id: "usable", + integrationId: "openai", + label: "default", + active: false, + value: { type: "oauth", access: "usable-access" }, + }, + ]); + await expect(readAuthFile({ integrationIds: ["openai"] })).resolves.toEqual({ + openai: { type: "oauth", access: "usable-access" }, + }); + }); + + it("leaves the database file byte-for-byte unchanged", async () => { + writeCredentialDatabase(databasePath, BASE_ROWS); + const before = sha256(databasePath); + + await readCredentialRows(["github-copilot", "openai", "deepseek"]); + await readCredentialRows(["openai"], { firstOnly: true, methods: ["oauth"] }); + await readAuthFile({ integrationIds: ["deepseek"], methods: ["key"] }); + + expect(sha256(databasePath)).toBe(before); + expect(readdirSync(dataDir)).toEqual(["opencode.db"]); + }); + + it("reports sqlite as the bound login source with no list error or failures", async () => { + writeCredentialDatabase(databasePath, BASE_ROWS); + await readCredentialRows(["openai", "deepseek"]); + + expect(getCredentialSourceDiagnostics()).toEqual({ + state: "bound", + kind: "sqlite", + failures: [], + }); + }); +}); diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 91ed9c52..308eaa1a 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -529,7 +529,6 @@ describe("integration credential source", () => { expect(warn.mock.calls).toEqual([["[opencode-quota] credential source is not bound"]]); expect(unboundReader.getCredentialSourceDiagnostics()).toEqual({ state: "unbound", - kind: "opencode-integration-api", failures: [], }); warn.mockRestore(); @@ -545,7 +544,10 @@ describe("integration credential source", () => { expect((await readCredentialRows(["deepseek"]))[0]?.id).toBe("cred_second"); unbindFirst(); expect((await readCredentialRows(["deepseek"]))[0]?.id).toBe("cred_second"); - expect(getCredentialSourceDiagnostics().state).toBe("bound"); + expect(getCredentialSourceDiagnostics()).toMatchObject({ + state: "bound", + kind: "opencode-integration-api", + }); unbindSecond(); unbindSecond(); await expect(readCredentialRows(["deepseek"])).resolves.toEqual([]); @@ -667,7 +669,7 @@ describe("readAuthFile and readAuthFileCached", () => { let rows = new Promise[]>((resolve) => { releaseOldRead = resolve; }); - unbinds.push(bindCredentialSource({ readRows: () => rows })); + unbinds.push(bindCredentialSource({ kind: "opencode-integration-api", readRows: () => rows })); const oldRead = readAuthFileCached({ maxAgeMs: 60_000, integrationIds: ["openai"] }); notifyCredentialsChanged(); diff --git a/tests/lib.opencode-service.test.ts b/tests/lib.opencode-service.test.ts deleted file mode 100644 index ebcb7397..00000000 --- a/tests/lib.opencode-service.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { createServer, type IncomingHttpHeaders, type Server } from "node:http"; -import type { AddressInfo } from "node:net"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { discoverOpenCodeService } from "../src/lib/opencode-service.js"; - -type InfoAnswer = { status: number; body: unknown } | "never"; - -describe("discoverOpenCodeService", () => { - let stateHome: string; - let savedStateHome: string | undefined; - let server: Server | undefined; - let requests: Array<{ url?: string; headers: IncomingHttpHeaders }>; - - // Every test points XDG_STATE_HOME at a temp folder, so the real service.json is never read. - beforeEach(() => { - savedStateHome = process.env.XDG_STATE_HOME; - stateHome = mkdtempSync(join(tmpdir(), "opencode-quota-service-")); - process.env.XDG_STATE_HOME = stateHome; - requests = []; - }); - - afterEach(async () => { - if (savedStateHome === undefined) delete process.env.XDG_STATE_HOME; - else process.env.XDG_STATE_HOME = savedStateHome; - if (server) { - server.closeAllConnections(); - await new Promise((resolve) => server?.close(resolve)); - server = undefined; - } - rmSync(stateHome, { recursive: true, force: true }); - }); - - async function startInfoServer(answer: InfoAnswer): Promise { - server = createServer((request, response) => { - requests.push({ url: request.url, headers: request.headers }); - if (answer === "never") return; - response.writeHead(answer.status, { "content-type": "application/json" }); - response.end(JSON.stringify(answer.body)); - }); - await new Promise((resolve) => server?.listen(0, "127.0.0.1", resolve)); - return `http://127.0.0.1:${(server.address() as AddressInfo).port}`; - } - - function writeRegistration(value: unknown): void { - mkdirSync(join(stateHome, "opencode"), { recursive: true }); - writeFileSync( - join(stateHome, "opencode", "service.json"), - typeof value === "string" ? value : JSON.stringify(value), - "utf8", - ); - } - - it("returns undefined when no service is registered", async () => { - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - }); - - it("returns undefined for a registration file that is not valid JSON", async () => { - writeRegistration("{not json"); - - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - }); - - it("returns undefined for a registration without a url or an integer pid", async () => { - const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 42 } }); - - writeRegistration({ pid: 42 }); - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - writeRegistration({ url, pid: "42" }); - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - writeRegistration({ url, pid: 4.2 }); - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - expect(requests).toEqual([]); - }); - - it("finds a service without a password and sends no authorization header", async () => { - const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 42 } }); - writeRegistration({ id: "service", version: "2.0.16", url, pid: 42 }); - - await expect(discoverOpenCodeService()).resolves.toEqual({ url, headers: {} }); - expect(requests).toHaveLength(1); - expect(requests[0].url).toBe("/api/info"); - expect(requests[0].headers.authorization).toBeUndefined(); - }); - - it("finds a service with a password and returns its basic auth header", async () => { - const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 42 } }); - writeRegistration({ id: "service", version: "2.0.16", url, pid: 42, password: "secret" }); - const authorization = `Basic ${Buffer.from("opencode:secret").toString("base64")}`; - - await expect(discoverOpenCodeService()).resolves.toEqual({ - url, - headers: { authorization }, - }); - expect(requests[0].headers.authorization).toBe(authorization); - }); - - it("returns undefined when another process answers", async () => { - const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 7 } }); - writeRegistration({ version: "2.0.16", url, pid: 42 }); - - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - }); - - it("returns undefined when the answering version differs from the registration", async () => { - const url = await startInfoServer({ status: 200, body: { version: "2.0.17", pid: 42 } }); - writeRegistration({ version: "2.0.16", url, pid: 42 }); - - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - }); - - it("returns undefined for a non-2xx answer or a malformed info body", async () => { - const url = await startInfoServer({ status: 401, body: { message: "Unauthorized" } }); - writeRegistration({ url, pid: 42, password: "old" }); - - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - server?.closeAllConnections(); - await new Promise((resolve) => server?.close(resolve)); - - const malformed = await startInfoServer({ status: 200, body: { version: 2, pid: 42 } }); - writeRegistration({ url: malformed, pid: 42 }); - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - }); - - it("returns undefined when nothing listens at the registered url", async () => { - const url = await startInfoServer({ status: 200, body: { version: "2.0.16", pid: 42 } }); - server?.closeAllConnections(); - await new Promise((resolve) => server?.close(resolve)); - server = undefined; - writeRegistration({ url, pid: 42 }); - - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - }); - - it("returns undefined when the service does not answer within 2 seconds", async () => { - const url = await startInfoServer("never"); - writeRegistration({ url, pid: 42 }); - - const startedAt = Date.now(); - await expect(discoverOpenCodeService()).resolves.toBeUndefined(); - expect(Date.now() - startedAt).toBeGreaterThanOrEqual(1900); - expect(requests).toHaveLength(1); - }); -}); diff --git a/tests/lib.quota-rpc-client.test.ts b/tests/lib.quota-rpc-client.test.ts deleted file mode 100644 index 40a8f0eb..00000000 --- a/tests/lib.quota-rpc-client.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { createServer, type IncomingHttpHeaders, type Server } from "node:http"; -import type { AddressInfo } from "node:net"; -import { afterEach, describe, expect, it } from "vitest"; - -import { callQuotaRpc, QuotaRpcTransportError } from "../src/lib/quota-rpc-client.js"; - -type Answer = { status: number; body: string } | "never"; - -describe("callQuotaRpc", () => { - let server: Server | undefined; - let requests: Array<{ - method?: string; - url?: string; - headers: IncomingHttpHeaders; - body: string; - }>; - - afterEach(async () => { - if (server) { - server.closeAllConnections(); - await new Promise((resolve) => server?.close(resolve)); - server = undefined; - } - }); - - async function startServer(answer: Answer): Promise { - requests = []; - server = createServer((request, response) => { - let body = ""; - request.on("data", (chunk) => { - body += chunk; - }); - request.on("end", () => { - requests.push({ method: request.method, url: request.url, headers: request.headers, body }); - if (answer === "never") return; - response.writeHead(answer.status, { "content-type": "application/json" }); - response.end(answer.body); - }); - }); - await new Promise((resolve) => server?.listen(0, "127.0.0.1", resolve)); - return `http://127.0.0.1:${(server.address() as AddressInfo).port}`; - } - - async function callFailure(url: string, timeoutMs = 5000): Promise { - return callQuotaRpc( - { url, headers: {} }, - "cli", - { command: "show" }, - { directory: "/home/user", timeoutMs }, - ).then( - () => { - throw new Error("expected the call to fail"); - }, - (error: unknown) => error, - ); - } - - it("posts the input to the plugin's method at the location and returns the output", async () => { - const url = await startServer({ - status: 200, - body: JSON.stringify({ output: { exitCode: 0, stdout: "ok\n", stderr: "" } }), - }); - - const output = await callQuotaRpc( - { url, headers: { authorization: "Basic abc" } }, - "cli", - { command: "show-json", threshold: 5 }, - { directory: "/Users/Jöhn Doe", timeoutMs: 5000 }, - ); - - expect(output).toEqual({ exitCode: 0, stdout: "ok\n", stderr: "" }); - expect(requests).toHaveLength(1); - expect(requests[0].method).toBe("POST"); - expect(requests[0].url).toBe("/api/rpc/slkiser.opencode-quota/cli"); - expect(requests[0].headers.authorization).toBe("Basic abc"); - expect(requests[0].headers["content-type"]).toBe("application/json"); - expect(requests[0].headers["x-opencode-directory"]).toBe("%2FUsers%2FJ%C3%B6hn%20Doe"); - expect(decodeURIComponent(requests[0].headers["x-opencode-directory"] as string)).toBe( - "/Users/Jöhn Doe", - ); - expect(JSON.parse(requests[0].body)).toEqual({ - input: { command: "show-json", threshold: 5 }, - }); - }); - - it("explains an unloaded server plugin", async () => { - const url = await startServer({ - status: 400, - body: JSON.stringify({ - _tag: "RpcError", - type: "rpc.unavailable", - message: "RPC is unavailable: slkiser.opencode-quota", - }), - }); - - const error = await callFailure(url); - - expect(error).toBeInstanceOf(QuotaRpcTransportError); - expect((error as Error).message).toBe( - "OpenCode Quota's server plugin is not loaded. Check the plugin entry in opencode.json and restart OpenCode.", - ); - }); - - it("explains a rejected service password", async () => { - const url = await startServer({ - status: 401, - body: JSON.stringify({ _tag: "UnauthorizedError", message: "Unauthorized" }), - }); - - const error = await callFailure(url); - - expect(error).toBeInstanceOf(QuotaRpcTransportError); - expect((error as Error).message).toBe( - "OpenCode's service password changed. Restart OpenCode and try again.", - ); - }); - - it("passes other RPC errors on as a sanitized single line", async () => { - const url = await startServer({ - status: 400, - body: JSON.stringify({ - _tag: "RpcError", - type: "rpc.method_not_found", - message: "Unknown RPC method:\u001b[31m slkiser.opencode-quota.cli\n", - }), - }); - - const error = await callFailure(url); - - expect(error).toBeInstanceOf(QuotaRpcTransportError); - expect((error as Error).message).toBe( - "OpenCode could not run OpenCode Quota: Unknown RPC method: slkiser.opencode-quota.cli", - ); - }); - - it("names the HTTP status when the error body has no message", async () => { - const url = await startServer({ status: 502, body: "Bad gateway" }); - - const error = await callFailure(url); - - expect((error as Error).message).toBe("OpenCode could not run OpenCode Quota: HTTP 502"); - }); - - it("rejects a successful answer without an output", async () => { - const url = await startServer({ status: 200, body: JSON.stringify({ result: {} }) }); - - const error = await callFailure(url); - - expect(error).toBeInstanceOf(QuotaRpcTransportError); - expect((error as Error).message).toBe("OpenCode returned an answer without an output."); - }); - - it("reports a network error with its reason", async () => { - const url = await startServer({ status: 200, body: "{}" }); - server?.closeAllConnections(); - await new Promise((resolve) => server?.close(resolve)); - server = undefined; - - const error = await callFailure(url); - - expect(error).toBeInstanceOf(QuotaRpcTransportError); - expect((error as Error).message).toMatch(/^Could not reach OpenCode: .*ECONNREFUSED/); - }); - - it("reports a call that does not answer in time", async () => { - const url = await startServer("never"); - - const error = await callFailure(url, 200); - - expect(error).toBeInstanceOf(QuotaRpcTransportError); - expect((error as Error).message).toBe("OpenCode did not answer within 0.2 s. Try again."); - }); -}); diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index 6e2327f4..32997e96 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -1489,10 +1489,27 @@ describe("buildQuotaStatusReport", () => { `); }); + it("reports the terminal command's database login source", async () => { + credentialSourceMocks.getCredentialSourceDiagnostics.mockReturnValueOnce({ + state: "bound", + kind: "sqlite", + failures: [], + } as never); + + const report = await buildQuotaStatusReportForTest(); + + expect(getReportSection(report, "credential_source:")).toMatchInlineSnapshot(` + "credential_source: + - source: sqlite + - list_error: (none) + - failures: (none) + " + `); + }); + it("reports an unbound login source", async () => { credentialSourceMocks.getCredentialSourceDiagnostics.mockReturnValueOnce({ state: "unbound", - kind: "opencode-integration-api", failures: [], } as never); diff --git a/tests/plugin-rpc.test.ts b/tests/plugin-rpc.test.ts index e3b71f53..5a09c907 100644 --- a/tests/plugin-rpc.test.ts +++ b/tests/plugin-rpc.test.ts @@ -141,7 +141,7 @@ describe("server quota RPC", () => { await rm(TEST_RUNTIME_ROOT, { recursive: true, force: true }); }); - it("registers the quota RPC with its five methods during setup", async () => { + it("registers the quota RPC with its four methods during setup", async () => { const { QuotaRpc } = await import("../src/rpc.js"); const { definition, handlers } = await setupServer(); @@ -152,7 +152,6 @@ describe("server quota RPC", () => { "footer", "writeExport", "command", - "cli", ]); expect(Object.keys(handlers)).toEqual(Object.keys(definition.methods)); }); @@ -280,91 +279,6 @@ describe("server quota RPC", () => { }); }); - it("builds the terminal show report and its cached JSON with threshold exit codes", async () => { - useConfig({}); - const { call } = await setupServer(); - - const text = (await call("cli", { command: "show" })) as { - exitCode: number; - stdout: string; - stderr: string; - }; - expect(text.exitCode).toBe(0); - expect(text.stdout).toContain("Copilot"); - expect(text.stdout).toContain("81%"); - expect(text.stderr).toBe(""); - - const json = (await call("cli", { command: "show-json", providerId: "copilot" })) as { - exitCode: number; - stdout: string; - stderr: string; - }; - expect(json).toEqual({ exitCode: 0, stdout: expect.any(String), stderr: "" }); - expect(JSON.parse(json.stdout).providers.copilot).toEqual( - expect.objectContaining({ status: "ok" }), - ); - await expect(call("cli", { command: "show-json", threshold: 50 })).resolves.toEqual( - expect.objectContaining({ exitCode: 0 }), - ); - await expect(call("cli", { command: "show-json", threshold: 90 })).resolves.toEqual( - expect.objectContaining({ exitCode: 1 }), - ); - }); - - it("builds the terminal status report as text and JSON", async () => { - useConfig({}); - const dialogModule = await import("../src/lib/quota-dialog-commands.js"); - const buildStatusReportData = vi - .spyOn(dialogModule, "buildStatusReportData") - .mockResolvedValue({ - output: "Quota Status (opencode-quota v5.0.0)", - payload: { version: "5.0.0", liveProbes: [] } as never, - hasComparableProviderData: false, - }); - const { call } = await setupServer(); - - await expect(call("cli", { command: "status" })).resolves.toEqual({ - exitCode: 0, - stdout: "Quota Status (opencode-quota v5.0.0)\n", - stderr: "", - }); - expect(buildStatusReportData).toHaveBeenLastCalledWith( - expect.objectContaining({ providerFilterId: undefined }), - ); - - await expect(call("cli", { command: "status-json", providerId: "copilot" })).resolves.toEqual({ - exitCode: 2, - stdout: `${JSON.stringify({ version: "5.0.0", liveProbes: [] }, null, 2)}\n`, - stderr: "", - }); - expect(buildStatusReportData).toHaveBeenLastCalledWith( - expect.objectContaining({ providerFilterId: "copilot" }), - ); - }); - - it("returns disabled quota and failures as the terminal command's stderr", async () => { - useConfig({ enabled: false }); - const disabled = await setupServer(); - await expect(disabled.call("cli", { command: "show-json" })).resolves.toEqual({ - exitCode: 1, - stdout: "", - stderr: "Quota disabled in config (enabled: false).\n", - }); - - mocks.loadConfig.mockRejectedValue(new Error("boom")); - const failing = await setupServer(); - await expect(failing.call("cli", { command: "show" })).resolves.toEqual({ - exitCode: 1, - stdout: "", - stderr: "Failed to show quota: boom\n", - }); - await expect(failing.call("cli", { command: "status-json" })).resolves.toEqual({ - exitCode: 1, - stdout: "", - stderr: "Failed to generate quota status: boom\n", - }); - }); - it("treats a session the server cannot find as a session without a model", async () => { useConfig({ onlyCurrentModel: true }); const input = { surface: "sidebar", sessionID: "missing" }; @@ -402,9 +316,8 @@ describe("server quota RPC", () => { await call("surface", { surface: "sidebar", sessionID: "session-1" }); await call("footer", { surface: "home" }); await call("command", { command: "quota", sessionID: "session-1" }); - await call("cli", { command: "show" }); - expect(workspaceRoots).toHaveLength(4); + expect(workspaceRoots).toHaveLength(3); expect(new Set(workspaceRoots)).toEqual(new Set([project])); expect(project).not.toBe(process.cwd()); }); diff --git a/tests/plugin-v2.test.ts b/tests/plugin-v2.test.ts index e476346e..6d922750 100644 --- a/tests/plugin-v2.test.ts +++ b/tests/plugin-v2.test.ts @@ -306,7 +306,10 @@ describe("V2 server plugin", () => { })); const cleanup = await plugin.setup({ ...ctx, integration, event: { subscribe } } as never); - expect(getCredentialSourceDiagnostics().state).toBe("bound"); + expect(getCredentialSourceDiagnostics()).toMatchObject({ + state: "bound", + kind: "opencode-integration-api", + }); await expect(readAuthFile({ integrationIds: ["deepseek"] })).resolves.toEqual({ deepseek: { type: "api", key: "deepseek-key" }, }); diff --git a/tests/rpc-definition.test.ts b/tests/rpc-definition.test.ts index 0d03020d..88115224 100644 --- a/tests/rpc-definition.test.ts +++ b/tests/rpc-definition.test.ts @@ -33,11 +33,11 @@ async function expectRejected(schema: StandardSchema, value: unknown, message: s const methods = QuotaRpc.methods; describe("quota RPC definition", () => { - it("defines the plain RPC id, no events and five methods", () => { + it("defines the plain RPC id, no events and four methods", () => { expect(QUOTA_RPC_ID).toBe("slkiser.opencode-quota"); expect(QuotaRpc.id).toBe(QUOTA_RPC_ID); expect(QuotaRpc.events).toEqual({}); - expect(Object.keys(methods)).toEqual(["surface", "footer", "writeExport", "command", "cli"]); + expect(Object.keys(methods)).toEqual(["surface", "footer", "writeExport", "command"]); }); it("uses Standard Schema v1 validators that carry no runtime types", () => { @@ -127,41 +127,6 @@ describe("quota RPC definition", () => { ); }); - it("validates cli input", async () => { - for (const command of ["show", "show-json", "status", "status-json"]) { - await expectAccepted(methods.cli.input, { command }); - } - await expectAccepted(methods.cli.input, { - command: "show-json", - providerId: "copilot", - threshold: 5, - }); - await expectAccepted(methods.cli.input, { command: "status", providerId: "anthropic" }); - await expectRejected(methods.cli.input, "show", "input must be an object"); - await expectRejected( - methods.cli.input, - {}, - "command must be one of show, show-json, status, status-json", - ); - await expectRejected( - methods.cli.input, - { command: "update" }, - "command must be one of show, show-json, status, status-json", - ); - await expectRejected( - methods.cli.input, - { command: "show", providerId: 1 }, - "providerId must be a string", - ); - for (const threshold of ["5", 0, -1]) { - await expectRejected( - methods.cli.input, - { command: "show-json", threshold }, - "threshold must be a positive finite number", - ); - } - }); - it("requires every output to be an object", async () => { for (const method of Object.values(methods)) { await expectAccepted(method.output, { any: "object" }); diff --git a/tests/tui-dist-import-graph.test.ts b/tests/tui-dist-import-graph.test.ts index 2cf6ee8d..ba085ffa 100644 --- a/tests/tui-dist-import-graph.test.ts +++ b/tests/tui-dist-import-graph.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; const distDir = fileURLToPath(new URL("../dist/", import.meta.url)); -/** Every dist file that dist/tui.js loads through relative `import` and `export ... from`. */ +/** Every dist file that `entry` loads through relative `import` and `export ... from`. */ function reachableFrom(entry: string): string[] { const reached = new Set(); const pending = [entry]; @@ -22,6 +22,13 @@ function reachableFrom(entry: string): string[] { return [...reached].map((file) => relative(distDir, file)).sort(); } +/** The reached files whose source queries OpenCode's credential table. */ +function filesQueryingCredentials(reached: string[]): string[] { + return reached.filter((file) => + readFileSync(resolve(distDir, file), "utf8").includes("FROM credential"), + ); +} + describe("tui dist import graph", () => { // Run after `pnpm build`. The TUI asks the server plugin for quota over the RPC, so it // must load no login reader and no provider code. @@ -33,16 +40,28 @@ describe("tui dist import graph", () => { expect(reached).toContain("rpc.js"); expect(reached).not.toContain("lib/opencode-auth.js"); + expect(reached).not.toContain("lib/opencode-auth-sqlite.js"); expect(reached.filter((file) => file.startsWith("providers/"))).toEqual([]); // node:sqlite is allowed: the session and token-history storage (opencode-storage.js -> // opencode-sqlite.js) is reachable through config.js -> quota-providers.js -> // quota-stats.js, but it opens a database only when called, and the TUI never calls it. - // It reads no logins: logins come only through OpenCode's plugin API, and no reachable - // module may query OpenCode's credential table. - expect( - reached.filter((file) => - readFileSync(resolve(distDir, file), "utf8").includes("FROM credential"), - ), - ).toEqual([]); + // It reads no logins: the TUI binds no login source, and no reachable module may query + // OpenCode's credential table (only the terminal command's reader does). + expect(filesQueryingCredentials(reached)).toEqual([]); + }); + + // Inside OpenCode, logins come only through the plugin API. The terminal command's + // read-only database reader must stay out of the server plugin. + it("keeps the terminal command's database login reader out of the server plugin", () => { + const entry = resolve(distDir, "index.js"); + expect(existsSync(entry)).toBe(true); + + const reached = reachableFrom(entry); + + expect(reached).toContain("plugin.js"); + expect(reached).not.toContain("lib/opencode-auth-sqlite.js"); + expect(reached).not.toContain("lib/cli-show.js"); + expect(reached).not.toContain("lib/cli-status.js"); + expect(filesQueryingCredentials(reached)).toEqual([]); }); }); From 9af0433fead0550e9464a85dddddea4d49c5a8fd Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:19:27 +0200 Subject: [PATCH 76/97] docs(providers): say Alibaba bl is found on your shell's PATH from the terminal Inside OpenCode, bl is found on the OpenCode service's PATH; the terminal command uses the shell's PATH. --- docs/readme/providers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/readme/providers.md b/docs/readme/providers.md index 6d5244c5..79d7c276 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -435,7 +435,7 @@ npm install -g bailian-cli bl auth login --console ``` -OpenCode Quota runs only `bl usage token-plan --output json`. It does not install `bl`, open a login flow, read console cookies, or accept a custom command. macOS and Linux resolve `bl` from absolute directories on OpenCode's `PATH`. Relative entries are ignored, and so is anything inside the project folder quota is shown for, including a `bl` that links into it. Your home folder does not count as a project, so `~/.local/bin` and nvm folders work. On Windows, use WSL. Native Windows `bl.exe` and `.cmd` shims are not supported in this release. +OpenCode Quota runs only `bl usage token-plan --output json`. It does not install `bl`, open a login flow, read console cookies, or accept a custom command. macOS and Linux resolve `bl` from absolute directories on the `PATH`: the OpenCode service's inside OpenCode, your shell's from the terminal command. Relative entries are ignored, and so is anything inside the project folder quota is shown for, including a `bl` that links into it. Your home folder does not count as a project, so `~/.local/bin` and nvm folders work. On Windows, use WSL. Native Windows `bl.exe` and `.cmd` shims are not supported in this release. Team plans, China-only `alibaba-token-plan-cn` runtimes, and cookie-based console scraping are out of scope. After you change the CLI's active console account, restart OpenCode or wait for the next live probe. `/quota_status` has an `alibaba_token_plan` live probe that stays separate from Alibaba Coding Plan diagnostics. From aab2eb9135a33bf753cac5e56ddafa82be6373fc Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:34:20 +0200 Subject: [PATCH 77/97] fix(toast): space reset countdowns like the sidebar The toast formatter call dropped resetTimeSpaced (and percentLabelStyle and accountingDetail), so toasts showed dense countdowns like 3d0h35m while the sidebar, footers, /quota and the CLI showed 3d 0h 35m. Pass the same settings the 4.x toast passed. A cross-surface test checks toast, sidebar, Home footer and prompt bar for both spaced and dense settings. --- src/lib/quota-surface-data.ts | 3 ++ tests/lib.quota-surface-data.test.ts | 57 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/lib/quota-surface-data.ts b/src/lib/quota-surface-data.ts index 72110a6b..14fa9613 100644 --- a/src/lib/quota-surface-data.ts +++ b/src/lib/quota-surface-data.ts @@ -186,7 +186,10 @@ function getToastMessage(params: { errors: data?.errors ?? [], style: params.style, percentDisplayMode: config.percentDisplayMode, + percentLabelStyle: config.percentLabelStyle, + accountingDetail: config.accountingDetail, resetTimeDecimals: config.resetTimeDecimals, + resetTimeSpaced: config.resetTimeSpaced, sessionTokens: data?.sessionTokens, }); if (!config.debug) return formatted; diff --git a/tests/lib.quota-surface-data.test.ts b/tests/lib.quota-surface-data.test.ts index 3154f18b..f937c337 100644 --- a/tests/lib.quota-surface-data.test.ts +++ b/tests/lib.quota-surface-data.test.ts @@ -179,6 +179,63 @@ describe("quota surface data", () => { expect(collect).toHaveBeenCalledWith(expect.objectContaining({ formatStyle: "allWindows" })); }); + it("spaces reset countdowns the same way on the toast, sidebar and footers", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z")); + try { + const copilot = makeCopilotProvider(); + copilot.fetch.mockResolvedValue({ + attempted: true, + entries: [ + { + accounting: TEST_ACCOUNTING, + name: "Copilot", + percentRemaining: 81, + resetTimeIso: "2026-01-04T00:35:00.000Z", + }, + ], + errors: [], + }); + mocks.getProviders.mockReturnValue([copilot]); + const { getQuotaFooter, getQuotaMessage } = await import("../src/lib/quota-surface-data.js"); + const configBase = { + enabled: true, + enabledProviders: ["copilot"], + minIntervalMs: 0, + maintainerAnnouncements: { enabled: false, home: false }, + tuiSidebarPanel: { enabled: true }, + tuiPromptBar: { enabled: true }, + tuiCompactStatus: { enabled: true, homeBottom: true, sessionPrompt: true, maxWidth: 96 }, + } as const; + + mocks.loadConfig.mockResolvedValue(makeQuotaToastTestConfig(configBase)); + const spacedToast = await getQuotaMessage(createHost(), "session-1", "idle"); + const spacedSidebar = await getQuotaMessage(createHost(), "session-1", "sidebar"); + const spacedHome = await getQuotaFooter(createHost(), undefined, "home"); + const spacedPrompt = await getQuotaFooter(createHost(), "session-1", "prompt"); + for (const text of [ + spacedToast?.message, + spacedSidebar?.message, + spacedHome.join("\n"), + spacedPrompt.join("\n"), + ]) { + expect(text).toContain("3d 0h 35m"); + } + + mocks.loadConfig.mockResolvedValue( + makeQuotaToastTestConfig({ ...configBase, resetTimeSpaced: false }), + ); + const denseToast = await getQuotaMessage(createHost(), "session-1", "idle"); + const denseSidebar = await getQuotaMessage(createHost(), "session-1", "sidebar"); + const denseHome = await getQuotaFooter(createHost(), undefined, "home"); + for (const text of [denseToast?.message, denseSidebar?.message, denseHome.join("\n")]) { + expect(text).toContain("3d0h35m"); + } + } finally { + vi.useRealTimers(); + } + }); + it("computes no prompt footer for the Home prompt, which has no session", async () => { const runtimeModule = await import("../src/lib/quota-runtime-context.js"); const resolve = vi.spyOn(runtimeModule, "resolveQuotaRuntimeContext"); From 5c604b67bd230de52c61f7904353d25096c4dc41 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:37:00 +0200 Subject: [PATCH 78/97] fix(zen): treat an empty Console org budget as no budget An OpenCode Console org without a monthly budget returns null from budgets/org (issue #216), which showed the error row "Could not parse OpenCode Console budgets/org response". A null body, an empty body, or {} now means no org budget, so Zen falls back to billing/account plus usage/cost-by-day without an error. Other malformed bodies stay errors. Zen error rows now read "OpenCode Zen:" instead of "OpenCode:"; Go errors already read "OpenCode Go:". --- src/lib/opencode-zen.ts | 13 +++++++++++-- src/providers/opencode-zen.ts | 9 ++++----- tests/lib.opencode-zen.test.ts | 26 ++++++++++++++++++++++++++ tests/providers.opencode-zen.test.ts | 20 ++++++++++---------- tests/quota-render-data.test.ts | 4 ++-- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/lib/opencode-zen.ts b/src/lib/opencode-zen.ts index eaa71a10..a2d4ef0a 100644 --- a/src/lib/opencode-zen.ts +++ b/src/lib/opencode-zen.ts @@ -127,14 +127,21 @@ function parseMonthlyUsage(json: unknown, now: Date): number { return Number.isFinite(total) ? total : invalidResponse(); } -/** Parses the org budget; a null limit means the org has no budget configured. */ +/** + * Parses the org budget. The org has no budget configured when the Console returns + * `null`, an empty body, `{}`, or a null limit. + */ function parseOrgBudget(json: unknown): { limitMicroCents: number | null; spentMicroCents: number | null; resetsAt: string | null; } { + if (json === null) return { limitMicroCents: null, spentMicroCents: null, resetsAt: null }; const budget = asRecord(json); if (!budget) return invalidResponse(); + if (Object.keys(budget).length === 0) { + return { limitMicroCents: null, spentMicroCents: null, resetsAt: null }; + } const limitValue = budget.limitMicroCents; if (limitValue === undefined) return invalidResponse(); @@ -202,7 +209,9 @@ async function fetchConsoleRoute(params: { const text = await response.text(); try { - return { success: true, data: params.parse(JSON.parse(text)) }; + // An empty body carries no value, so the route parser sees it as JSON null. + const json: unknown = text.trim() === "" ? null : JSON.parse(text); + return { success: true, data: params.parse(json) }; } catch { return { success: false, diff --git a/src/providers/opencode-zen.ts b/src/providers/opencode-zen.ts index adc98dfb..62ea9649 100644 --- a/src/providers/opencode-zen.ts +++ b/src/providers/opencode-zen.ts @@ -25,7 +25,6 @@ import { withStatusDetails, } from "./result-helpers.js"; -const OPENCODE_PROVIDER_LABEL = "OpenCode"; const OPENCODE_ZEN_GROUP = "OpenCode Zen"; const OPENCODE_ZEN_BALANCE_ACCOUNTING: AccountingMetadata = { resultType: "balance", @@ -65,7 +64,7 @@ function isExplicitlyEnabled(ctx: QuotaProviderContext): boolean { function signInFailedResult(state: "expired" | "invalid", detail: string): QuotaProviderResult { return withStatusDetails( attemptedErrorResult( - OPENCODE_PROVIDER_LABEL, + OPENCODE_ZEN_GROUP, `OpenCode Console sign-in failed: ${detail}. ${LOGIN_HINT}`, ), [{ key: "console_auth_state", value: state }], @@ -113,7 +112,7 @@ export const opencodeZenProvider: QuotaProvider = { } return withStatusDetails( attemptedErrorResult( - OPENCODE_PROVIDER_LABEL, + OPENCODE_ZEN_GROUP, `No OpenCode Console sign-in found. ${LOGIN_HINT}`, ), noneStatusDetails, @@ -139,7 +138,7 @@ export const opencodeZenProvider: QuotaProvider = { }); if (!result.success) { - return withStatusDetails(attemptedErrorResult(OPENCODE_PROVIDER_LABEL, result.error), [ + return withStatusDetails(attemptedErrorResult(OPENCODE_ZEN_GROUP, result.error), [ ...statusDetails, { key: "live_fetch_error", value: result.error }, ]); @@ -230,7 +229,7 @@ export const opencodeZenProvider: QuotaProvider = { }); } - const errors = result.errors.map((message) => ({ label: OPENCODE_PROVIDER_LABEL, message })); + const errors = result.errors.map((message) => ({ label: OPENCODE_ZEN_GROUP, message })); return withStatusDetails(attemptedResult(entries, errors), [ ...statusDetails, diff --git a/tests/lib.opencode-zen.test.ts b/tests/lib.opencode-zen.test.ts index 117f36f1..7d4ab2c8 100644 --- a/tests/lib.opencode-zen.test.ts +++ b/tests/lib.opencode-zen.test.ts @@ -236,6 +236,30 @@ describe("queryOpenCodeZenQuota", () => { }); }); + it.each([ + ["a null body", () => json(null)], + ["an empty body", () => new Response("", { headers: { "content-type": "application/json" } })], + ["an empty object", () => json({})], + ])("treats %s from budgets/org as no org budget", async (_label, orgBudget) => { + routes({ + "billing/account": () => json({ ...ACCOUNT, creditLimitMicroCents: "10000000000" }), + "usage/cost-by-day": () => json([{ date: "2026-09-24", totalCostMicroCents: "75000000" }]), + "budgets/org": orgBudget, + }); + + await expect(queryOpenCodeZenQuota(credential)).resolves.toMatchObject({ + success: true, + data: { + balance: 0, + monthlyLimit: 100, + monthlyUsage: 75_000_000, + budgetResetIso: null, + budgetSource: "credit_limit", + }, + errors: [], + }); + }); + it("falls back and lists an error when the org budget route fails", async () => { routes({ "billing/status": () => json({ ...STATUS, balanceMicroCents: "4250000000" }), @@ -555,6 +579,8 @@ describe("queryOpenCodeZenQuota", () => { ["billing/account", () => json({ ...ACCOUNT, creditLimitMicroCents: "9".repeat(309) })], ["billing/auto-recharge", () => json({ ...AUTO_RECHARGE, enabled: "no" })], ["budgets/org", () => json([])], + ["budgets/org", () => json("none")], + ["budgets/org", () => json({ spentMicroCents: "0" })], ["budgets/org", () => json({ limitMicroCents: "abc", spentMicroCents: "0" })], ["budgets/org", () => json({ limitMicroCents: "6000000000", resetsAt: 7 })], ["usage/cost-by-day", () => json({ days: [] })], diff --git a/tests/providers.opencode-zen.test.ts b/tests/providers.opencode-zen.test.ts index 21f4656b..e511f0e0 100644 --- a/tests/providers.opencode-zen.test.ts +++ b/tests/providers.opencode-zen.test.ts @@ -222,9 +222,9 @@ describe("opencode Zen provider", () => { const result = await opencodeZenProvider.fetch(context({ enabledProviders: ["opencode"] })); - expectAttemptedWithErrorLabel(result, "OpenCode"); + expectAttemptedWithErrorLabel(result, "OpenCode Zen"); expect(result.errors).toEqual([ - { label: "OpenCode", message: `No OpenCode Console sign-in found. ${LOGIN_HINT}` }, + { label: "OpenCode Zen", message: `No OpenCode Console sign-in found. ${LOGIN_HINT}` }, ]); expect(result.statusDetails).toEqual([{ key: "console_auth_state", value: "none" }]); expect(mocks.queryOpenCodeZenQuota).not.toHaveBeenCalled(); @@ -239,10 +239,10 @@ describe("opencode Zen provider", () => { const result = await opencodeZenProvider.fetch(context()); - expectAttemptedWithErrorLabel(result, "OpenCode"); + expectAttemptedWithErrorLabel(result, "OpenCode Zen"); expect(result.errors).toEqual([ { - label: "OpenCode", + label: "OpenCode Zen", message: `OpenCode Console sign-in failed: refresh_failed: token [redacted] was rejected. ${LOGIN_HINT}`, }, ]); @@ -259,10 +259,10 @@ describe("opencode Zen provider", () => { const result = await opencodeZenProvider.fetch(context()); - expectAttemptedWithErrorLabel(result, "OpenCode"); + expectAttemptedWithErrorLabel(result, "OpenCode Zen"); expect(result.errors).toEqual([ { - label: "OpenCode", + label: "OpenCode Zen", message: `OpenCode Console sign-in failed: the sign-in expired. ${LOGIN_HINT}`, }, ]); @@ -281,7 +281,7 @@ describe("opencode Zen provider", () => { const result = await opencodeZenProvider.fetch(context()); - expectAttemptedWithErrorLabel(result, "OpenCode"); + expectAttemptedWithErrorLabel(result, "OpenCode Zen"); expect(result.errors[0]?.message).toContain("opencode auth login opencode"); }); @@ -514,8 +514,8 @@ describe("opencode Zen provider", () => { expect(result.attempted).toBe(true); expect(result.entries).toEqual([balanceEntry("primary")]); expect(result.errors).toEqual([ - { label: "OpenCode", message: "OpenCode Console billing/auto-recharge error 500" }, - { label: "OpenCode", message: "OpenCode Console usage/cost-by-day error 500" }, + { label: "OpenCode Zen", message: "OpenCode Console billing/auto-recharge error 500" }, + { label: "OpenCode Zen", message: "OpenCode Console usage/cost-by-day error 500" }, ]); expect(result.statusDetails).toContainEqual({ key: "auto_reload", value: "(unknown)" }); expect(result.statusDetails).toContainEqual({ @@ -544,7 +544,7 @@ describe("opencode Zen provider", () => { autoReloadEntry(), ]); expect(result.errors).toEqual([ - { label: "OpenCode", message: "OpenCode Console billing/account error 500" }, + { label: "OpenCode Zen", message: "OpenCode Console billing/account error 500" }, ]); }); diff --git a/tests/quota-render-data.test.ts b/tests/quota-render-data.test.ts index 0f909222..19445a11 100644 --- a/tests/quota-render-data.test.ts +++ b/tests/quota-render-data.test.ts @@ -139,7 +139,7 @@ describe("collectQuotaRenderData shared quota state", () => { }); expect(failed.active).toEqual([opencodeZenProvider]); expect(failed.data?.errors).toContainEqual({ - label: "OpenCode", + label: "OpenCode Zen", message: "OpenCode Console sign-in failed: refresh_failed: boom. Run `opencode auth login opencode`.", }); @@ -167,7 +167,7 @@ describe("collectQuotaRenderData shared quota state", () => { }); expect(explicitNone.active).toEqual([opencodeZenProvider]); expect(explicitNone.data?.errors).toContainEqual({ - label: "OpenCode", + label: "OpenCode Zen", message: "No OpenCode Console sign-in found. Run `opencode auth login opencode`.", }); }); From 13e6d69dd8e1ca878e1f645e18b72fa8a9ce382c Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:41:30 +0200 Subject: [PATCH 79/97] fix(tui): wrap long lines in the sidebar quota panel The sidebar renders each line with wrapMode="none" so bars stay intact, which cut long error rows at the panel edge ("OpenCode: Could not parse OpenCode Co"). The formatter already fits every bar and value row to the 36-column sidebar, so buildSidebarQuotaPanelLines now word-wraps only the lines longer than that (errors and notices). Bars and aligned rows are unchanged. Toasts already word-wrap in OpenCode, and the compact footers are single-line by design. --- src/lib/tui-sidebar-format.ts | 10 ++++++- tests/providers.nanogpt.surfaces.test.ts | 5 +++- tests/providers.synthetic.surfaces.test.ts | 3 +- tests/quota-providers-phase4-surfaces.test.ts | 3 +- tests/tui-sidebar-format.test.ts | 28 +++++++++++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/lib/tui-sidebar-format.ts b/src/lib/tui-sidebar-format.ts index c7f4fe5a..0d9fd79f 100644 --- a/src/lib/tui-sidebar-format.ts +++ b/src/lib/tui-sidebar-format.ts @@ -1,5 +1,6 @@ import { sanitizeQuotaRenderData } from "./display-sanitize.js"; import { formatQuotaRows } from "./format.js"; +import { wrapDisplayText } from "./format-utils.js"; import type { QuotaRenderData } from "./quota-render-data.js"; import type { QuotaToastConfig } from "./types.js"; @@ -36,5 +37,12 @@ export function buildSidebarQuotaPanelLines(params: { wrapLabels: params.config.quotaProjection === "runway", sessionTokens: data.sessionTokens, }); - return quotaBody ? quotaBody.split("\n") : []; + if (!quotaBody) return []; + // The formatter fits every bar and value row to the sidebar width. Only free text + // (errors, notices) runs longer, and the TUI would cut it, so wrap it here. + return quotaBody + .split("\n") + .flatMap((line) => + line.length > TUI_SIDEBAR_MAX_WIDTH ? wrapDisplayText(line, TUI_SIDEBAR_MAX_WIDTH) : [line], + ); } diff --git a/tests/providers.nanogpt.surfaces.test.ts b/tests/providers.nanogpt.surfaces.test.ts index 5a6993ba..06e854fb 100644 --- a/tests/providers.nanogpt.surfaces.test.ts +++ b/tests/providers.nanogpt.surfaces.test.ts @@ -110,8 +110,11 @@ describe("NanoGPT structured four-surface formatting", () => { for (const output of [outputs.command, outputs.toast, outputs.sidebar]) { expect(output).toContain("26.71801147 NANO"); - expect(output).toContain("invalid usd_balance decimal"); } + expect(outputs.command).toContain("invalid usd_balance decimal"); + expect(outputs.toast).toContain("invalid usd_balance decimal"); + // The sidebar wraps long error rows to its width. + expect(outputs.sidebar.replaceAll("\n", " ")).toContain("invalid usd_balance decimal"); expect(outputs.compact).toContain("26.71801147 NANO"); expect(outputs.compact).toContain("1 issue"); }); diff --git a/tests/providers.synthetic.surfaces.test.ts b/tests/providers.synthetic.surfaces.test.ts index 0c89b9ae..fc743753 100644 --- a/tests/providers.synthetic.surfaces.test.ts +++ b/tests/providers.synthetic.surfaces.test.ts @@ -21,7 +21,8 @@ describe("Synthetic empty-object four-surface formatting", () => { }); for (const output of Object.values(outputs)) { - expect(output).toContain(EMPTY_OBJECT_DIAGNOSTIC); + // The sidebar wraps long error rows to its width. + expect(output.replaceAll("\n", " ")).toContain(EMPTY_OBJECT_DIAGNOSTIC); expect(output).not.toContain("5h:"); expect(output).not.toContain("Weekly:"); expect(output).not.toContain("0/0"); diff --git a/tests/quota-providers-phase4-surfaces.test.ts b/tests/quota-providers-phase4-surfaces.test.ts index 58386492..247d42e9 100644 --- a/tests/quota-providers-phase4-surfaces.test.ts +++ b/tests/quota-providers-phase4-surfaces.test.ts @@ -76,7 +76,8 @@ describe("quota provider four-surface formatting", () => { } expect(command).toContain("one source unavailable"); expect(toast).toContain("one source unavailable"); - expect(sidebar).toContain("one source unavailable"); + // The sidebar wraps long error rows to its width. + expect(sidebar.replaceAll("\n", " ")).toContain("one source unavailable"); expect(compact).toContain("issue"); }); }); diff --git a/tests/tui-sidebar-format.test.ts b/tests/tui-sidebar-format.test.ts index 5d2de8d3..3beab6b9 100644 --- a/tests/tui-sidebar-format.test.ts +++ b/tests/tui-sidebar-format.test.ts @@ -67,6 +67,34 @@ describe("buildSidebarQuotaPanelLines", () => { expect(rendered).toContain("gpt-5"); }); + it("wraps long error rows within the sidebar width and keeps bar rows unchanged", () => { + const message = "Could not parse OpenCode Console budgets/org response"; + const data = { + entries: [{ name: "Copilot", percentRemaining: 75 }], + errors: [{ label: "OpenCode Zen", message }], + sessionTokens: undefined, + }; + const unwrapped = formatQuotaRows({ + version: "1.0.0", + layout: TUI_SIDEBAR_LAYOUT, + entries: data.entries, + errors: data.errors, + style: "singleWindow", + percentDisplayMode: "remaining", + }).split("\n"); + + const lines = buildSidebarQuotaPanelLines({ + data, + config: { formatStyle: "singleWindow", percentDisplayMode: "remaining" }, + }); + + expect(lines.every((line) => line.length <= TUI_SIDEBAR_MAX_WIDTH)).toBe(true); + const errorStart = lines.findIndex((line) => line.startsWith("OpenCode Zen:")); + expect(lines.slice(0, errorStart)).toEqual(unwrapped.slice(0, errorStart)); + expect(lines.slice(errorStart).join(" ")).toBe(`OpenCode Zen: ${message}`); + expect(lines.length).toBeGreaterThan(unwrapped.length); + }); + it("uses the fixed sidebar layout instead of toast layout settings", () => { const data = { entries: [ From 0ee82b620b210b4defe1332cd48952d2bb67715c Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:43:12 +0200 Subject: [PATCH 80/97] fix(cli): hide Node's experimental SQLite warning show and status read OpenCode's database through node:sqlite, and Node 22+ printed "ExperimentalWarning: SQLite is an experimental feature and might change at any time" on every run. The CLI entry now filters process .emitWarning for that one ExperimentalWarning before any command loads node:sqlite. Every other warning still prints. --- src/bin/opencode-quota.ts | 20 +++++++++++++ tests/bin.opencode-quota.test.ts | 49 ++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/bin/opencode-quota.ts b/src/bin/opencode-quota.ts index d1c2e825..75584a87 100644 --- a/src/bin/opencode-quota.ts +++ b/src/bin/opencode-quota.ts @@ -59,6 +59,25 @@ export function cliShouldRunMain( return resolvePath(modulePath) === resolvePath(argv1); } +/** + * `show` and `status` read OpenCode's database through node:sqlite, and Node prints + * "ExperimentalWarning: SQLite is an experimental feature..." when it loads. Hide only + * that warning; every other warning still prints. Call before node:sqlite loads. + */ +export function hideSqliteExperimentalWarning(): void { + const emitWarning = process.emitWarning; + process.emitWarning = ((warning: string | Error, ...rest: unknown[]) => { + if ( + typeof warning === "string" && + rest[0] === "ExperimentalWarning" && + warning.startsWith("SQLite is an experimental feature") + ) { + return; + } + return (emitWarning as (...args: unknown[]) => void).call(process, warning, ...rest); + }) as typeof process.emitWarning; +} + export async function main(argv = process.argv.slice(2)): Promise { const [command, ...rest] = argv; @@ -107,6 +126,7 @@ export async function main(argv = process.argv.slice(2)): Promise { } if (cliShouldRunMain()) { + hideSqliteExperimentalWarning(); void main().then((code) => { process.exitCode = code; }); diff --git a/tests/bin.opencode-quota.test.ts b/tests/bin.opencode-quota.test.ts index 5fe874d4..40ca470e 100644 --- a/tests/bin.opencode-quota.test.ts +++ b/tests/bin.opencode-quota.test.ts @@ -1,7 +1,8 @@ -import { mkdtempSync, rmSync, symlinkSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, rmSync, symlinkSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -187,4 +188,48 @@ describe("opencode-quota bin", () => { rmSync(tempDir, { recursive: true, force: true }); } }); + + it("hides only Node's experimental SQLite warning", async () => { + const { hideSqliteExperimentalWarning } = await import("../src/bin/opencode-quota.js"); + const originalEmitWarning = process.emitWarning; + const emitWarning = vi.fn(); + process.emitWarning = emitWarning as unknown as typeof process.emitWarning; + try { + hideSqliteExperimentalWarning(); + + process.emitWarning( + "SQLite is an experimental feature and might change at any time", + "ExperimentalWarning", + ); + process.emitWarning("Some other feature is experimental", "ExperimentalWarning"); + process.emitWarning("SQLite is an experimental feature and might change at any time"); + + expect(emitWarning.mock.calls).toEqual([ + ["Some other feature is experimental", "ExperimentalWarning"], + ["SQLite is an experimental feature and might change at any time"], + ]); + } finally { + process.emitWarning = originalEmitWarning; + } + }); + + // Run after `pnpm build`: loads node:sqlite in a real Node process. + it("keeps the SQLite warning off stderr when the built CLI loads node:sqlite", () => { + const bin = fileURLToPath(new URL("../dist/bin/opencode-quota.js", import.meta.url)); + expect(existsSync(bin)).toBe(true); + const script = [ + `const { hideSqliteExperimentalWarning } = await import(${JSON.stringify(pathToFileURL(bin).href)});`, + "hideSqliteExperimentalWarning();", + 'await import("node:sqlite");', + 'process.emitWarning("Some other feature is experimental", "ExperimentalWarning");', + ].join("\n"); + + const { status, stderr } = spawnSync(process.execPath, ["--input-type=module", "-e", script], { + encoding: "utf8", + }); + + expect(status).toBe(0); + expect(stderr).not.toContain("SQLite"); + expect(stderr).toContain("ExperimentalWarning: Some other feature is experimental"); + }); }); From 02f0476fb8e1b8a8b68fb01c68de5cb0fe2aa275 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:44:19 +0200 Subject: [PATCH 81/97] docs(readme): add an OpenCode listing badge Add a flat-square badge to the top badge row that links to anomalyco/opencode#38281, and remove the "Like this plugin?" tip it replaces. The in-app opencode-ecosystem-listing-thumbs-up announcement stays. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 3879def8..7db04bd3 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,9 @@ npm downloads CI License: MIT + Help us get listed: opencode#38281

-> [!TIP] -> **Like this plugin?** A 👍 on [opencode#38281](https://github.com/anomalyco/opencode/issues/38281) helps get it listed, and [#43132](https://github.com/anomalyco/opencode/issues/43132) asks for Web/Desktop panels. - [![OpenCode Quota sidebar](https://shawnkiser.com/opencode-quota/opencode-quota-sidebar.webp)](https://github.com/slkiser/opencode-quota) --- From 93e29ebeaad397a5db2d285426d44eb69b8a07d1 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 02:29:23 +0200 Subject: [PATCH 82/97] fix(tui): open quota commands locally so they never flash or create a session In "dialog" mode (the default), Enter on a prompt that holds exactly a quota slash command (for example `/quota` or `/tokens_between 2026-09-01 2026-09-25`) now clears the prompt and runs the command over the quota RPC, before OpenCode submits it. The report opens only in the popup: it no longer shows in the chat for a moment, and on Home no new session is created. A keymap layer binds Enter at priority 1, above OpenCode's prompt, with no mode, so it is off while the "/" list or a dialog is open. Other text, and every command in "inline" mode, goes on to OpenCode's submit as before. tuiCommandDisplay is read at start and again after each typed quota command. The "/" list still shows only the server commands, and Web and Desktop are unchanged. Closes #296 --- src/lib/quota-dialog-command-specs.ts | 16 ++ src/plugin.ts | 8 +- src/tui-v2.tsx | 76 +++++++-- tests/plugin.command-handled-boundary.test.ts | 6 +- tests/plugin.tokens-session-all.test.ts | 5 +- tests/tui-v2-commands.test.ts | 159 ++++++++++++++++-- tests/v4-phase5-cross-surface.test.ts | 16 +- 7 files changed, 258 insertions(+), 28 deletions(-) diff --git a/src/lib/quota-dialog-command-specs.ts b/src/lib/quota-dialog-command-specs.ts index 81fe218a..fa40b412 100644 --- a/src/lib/quota-dialog-command-specs.ts +++ b/src/lib/quota-dialog-command-specs.ts @@ -199,3 +199,19 @@ export const QUOTA_DIALOG_COMMANDS_BY_ID: ReadonlyMap< export function isQuotaDialogCommand(command: string): command is QuotaDialogCommandId { return QUOTA_DIALOG_COMMANDS_BY_ID.has(command as QuotaDialogCommandId); } + +/** + * Reads prompt text as a typed quota slash command. As in OpenCode, the command name ends + * at the first whitespace; like the server command, the rest is trimmed into its arguments. + * Returns undefined when the text is not exactly one of the quota commands. + */ +export function parseQuotaSlashCommand( + text: string, +): { command: QuotaDialogCommandId; argumentsText: string | undefined } | undefined { + const trimmed = text.trim(); + if (!trimmed.startsWith("/")) return undefined; + const name = trimmed.slice(1).split(/\s/, 1)[0]; + const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.slashName === name); + if (!spec) return undefined; + return { command: spec.id, argumentsText: trimmed.slice(1 + name.length).trim() || undefined }; +} diff --git a/src/plugin.ts b/src/plugin.ts index 5fb61a91..c3e09777 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -196,9 +196,11 @@ export const QuotaToastPlugin = Plugin.define({ }); }); - // Web, Desktop, and the TUI list these in their "/" menu. When a report arrives, the TUI - // follows tuiCommandDisplay: "dialog" opens its dialog and cancels the posted report, - // "inline" leaves it in the chat. Its command palette runs the same reports without posting them. + // Web, Desktop, and the TUI list these in their "/" menu. In "dialog" mode the TUI runs a + // typed command itself over the quota RPC, so it never reaches this. When a report does + // arrive, the TUI follows tuiCommandDisplay: "dialog" opens its dialog and cancels the + // posted report, "inline" leaves it in the chat. Its command palette runs the same reports + // without posting them. await ctx.command.transform((editor) => { for (const spec of QUOTA_DIALOG_COMMANDS) { editor.add({ diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index dbe3dcd3..3b7af787 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -9,10 +9,12 @@ import { loadConfig } from "./lib/config.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { + parseQuotaSlashCommand, QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, } from "./lib/quota-dialog-command-specs.js"; import { readQuotaReport, readQuotaReportMetadata } from "./lib/quota-report-message.js"; +import type { TuiCommandDisplay } from "./lib/types.js"; import { QuotaRpc, type QuotaRpcCommandInput, @@ -38,11 +40,12 @@ type Toast = { }; type KeymapCommand = { id?: string; - title: string; - group: string; + title?: string; + group?: string; bind?: string; palette?: true; - run: () => void | Promise; + /** Returning false lets the key continue to the next layer. */ + run: () => void | false | Promise; }; type DialogTheme = { text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA } } }; @@ -69,6 +72,7 @@ type QuotaRpcClient = { }; type TuiContext = { location?: { directory: string }; + renderer: { currentFocusedEditor: { plainText: string; clear: () => void } | null }; client: { rpc: (definition: typeof QuotaRpc) => QuotaRpcClient; session: { @@ -87,7 +91,10 @@ type TuiContext = { }; }; keymap: { - layer: (build: () => { mode: "global" | "modal"; commands: KeymapCommand[] }) => void; + /** Without a mode, a layer is active only in OpenCode's base mode. */ + layer: ( + build: () => { mode?: "global" | "modal"; priority?: number; commands: KeymapCommand[] }, + ) => void; }; ui: { slot: ( @@ -372,16 +379,20 @@ function showQuotaOutputDialog( }); } -/** Runs a quota command from the command palette. It opens the dialog and posts no message. */ +/** + * Runs a quota command from the command palette or the prompt. It opens the dialog and + * posts no message. `typed` holds the arguments typed after the command name in the prompt. + */ async function runQuotaCommand( context: TuiContext, command: QuotaDialogCommandId, sessionID: string | undefined, + typed?: { argumentsText: string | undefined }, ): Promise { const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.id === command)!; - let argumentsText: string | undefined; + let argumentsText = typed?.argumentsText; // Only /tokens_between needs arguments; the palette asks for them. - if (command === "tokens_between") { + if (command === "tokens_between" && !typed) { const value = await context.ui.dialog.prompt({ title: spec.title, placeholder: "YYYY-MM-DD YYYY-MM-DD", @@ -411,9 +422,9 @@ async function runQuotaCommand( } /** - * Typed quota slash commands run on the server, which posts the report into the chat as a - * pending inbox item. When a report arrives for the session on screen, tuiCommandDisplay - * decides what the TUI shows: "dialog" opens the report in the dialog and cancels the pending + * Quota slash commands that reach the server (from Web or Desktop, queued in the TUI, or + * typed in "inline" mode) post the report into the chat as a pending inbox item. When a + * report arrives for the session on screen, tuiCommandDisplay decides what the TUI shows: "dialog" opens the report in the dialog and cancels the pending * item, so no chat message remains; "inline" leaves the report in the chat. * The TUI cannot tell which client typed the command, so in "dialog" mode it also removes a * report that Web or Desktop requested for the same session while the TUI shows it. @@ -467,6 +478,50 @@ function registerQuotaCommands(context: TuiContext): void { })); } +/** + * Runs typed quota slash commands in the TUI. On Enter, before OpenCode submits the prompt, + * a prompt that holds exactly a quota command is cleared and the command opens its dialog. + * So the report never shows in the chat, and on Home no session is created for it. + * Everything else, and every command in "inline" mode, goes on to OpenCode's submit, and + * the server command posts the report as before. + * The layer has no mode, so it is off while the "/" list or a dialog is open. tuiCommandDisplay + * must be known when Enter is pressed, so it is read at start and again after each typed + * quota command; until the first read finishes, typed commands go to the server. + */ +function registerTypedQuotaCommands(context: TuiContext): void { + let commandDisplay: TuiCommandDisplay | undefined; + const readCommandDisplay = () => { + void loadConfig(quotaClient(context), undefined, { + configRootDir: quotaRoots(context).configRoot, + }) + .then((config) => { + commandDisplay = config.tuiCommandDisplay; + }) + .catch(reportFailure); + }; + readCommandDisplay(); + context.keymap.layer(() => ({ + // Above OpenCode's textarea layer, which submits the prompt on Enter. + priority: 1, + commands: [ + { + bind: "return", + // Synchronous on purpose: a returned promise counts as handled. + run: () => { + const editor = context.renderer.currentFocusedEditor; + const typed = editor ? parseQuotaSlashCommand(editor.plainText) : undefined; + if (!editor || !typed) return false; + const display = commandDisplay; + readCommandDisplay(); + if (display !== "dialog") return false; + editor.clear(); + void runQuotaCommand(context, typed.command, getRouteSessionID(context), typed); + }, + }, + ], + })); +} + function SidebarQuotaView(props: { context: TuiContext; sessionID: string }): JSX.Element { const [open, setOpen] = createSignal(true); const [quota, setQuota] = createSignal< @@ -530,6 +585,7 @@ const plugin = Plugin.define({ render: () => { if (disposeEvents) return null; registerQuotaCommands(api); + registerTypedQuotaCommands(api); const trigger = (event: TuiEvent, reason: "idle" | "compacted" | "question") => { const sessionID = getSessionID(event); if (!sessionID) return; diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 5f6d9dae..436d81b9 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -52,10 +52,14 @@ function startTui() { layer: vi.fn( ( build: () => { + mode?: string; commands: Array<{ id: string; run: () => Promise; slash?: unknown }>; }, ) => { - for (const command of build().commands) commands.set(command.id, command); + // The palette layer; the typed-command Enter layer has no mode. + const layer = build(); + if (layer.mode !== "global") return; + for (const command of layer.commands) commands.set(command.id, command); }, ), }, diff --git a/tests/plugin.tokens-session-all.test.ts b/tests/plugin.tokens-session-all.test.ts index 67741c63..c48fcfdc 100644 --- a/tests/plugin.tokens-session-all.test.ts +++ b/tests/plugin.tokens-session-all.test.ts @@ -122,8 +122,9 @@ describe("/tokens_session_all command", () => { plugin.setup({ client: { rpc }, keymap: { - layer: (build: () => { commands: typeof commands }) => { - commands = build().commands; + layer: (build: () => { mode?: string; commands: typeof commands }) => { + const layer = build(); + if (layer.mode === "global") commands = layer.commands; }, }, data: { on: () => () => {} }, diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 843ee517..8058f0f7 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -13,6 +13,7 @@ vi.mock("../src/lib/config.js", async (importOriginal) => { return { ...original, loadConfig }; }); +import { parseQuotaSlashCommand } from "../src/lib/quota-dialog-command-specs.js"; import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; import plugin from "../src/tui-v2.tsx"; @@ -42,6 +43,11 @@ type RegisteredCommand = { run: () => Promise; }; type Listener = (event: { data?: Record }) => void; +type Layer = { + mode?: string; + priority?: number; + commands: Array unknown }>; +}; function startTui( location?: { directory: string }, @@ -49,8 +55,10 @@ function startTui( ) { let layer: { commands: RegisteredCommand[] } | undefined; const listeners = new Map(); + const editor = { plainText: "", clear: vi.fn() }; const context = { location, + renderer: { currentFocusedEditor: editor as typeof editor | null }, client: { rpc: vi.fn(() => rpc), session: { inbox: { cancel: vi.fn().mockResolvedValue(undefined) } }, @@ -93,7 +101,8 @@ function startTui( const commands = layer!.commands; const command = (id: string) => commands.find((item) => item.id === `quota.${id}`)!; const emit = (name: string, data: Record) => listeners.get(name)?.({ data }); - return { context, commands, command, emit }; + const layers = () => context.keymap.layer.mock.calls.map(([build]) => build() as Layer); + return { context, editor, commands, command, emit, layers }; } describe("V2 quota TUI commands", () => { @@ -243,7 +252,8 @@ describe("V2 quota TUI commands", () => { }); await settle(); - expect(loadConfig).not.toHaveBeenCalled(); + // Only the read at start, for typed commands. + expect(loadConfig).toHaveBeenCalledOnce(); expect(context.ui.dialog.show).not.toHaveBeenCalled(); expect(context.client.session.inbox.cancel).not.toHaveBeenCalled(); }); @@ -288,8 +298,9 @@ describe("V2 quota TUI commands", () => { }); await settle(); - expect(loadConfig).toHaveBeenCalledOnce(); - await expect(loadConfig.mock.results[0].value).resolves.toMatchObject({ + // The read at start, then the read for the report. + expect(loadConfig).toHaveBeenCalledTimes(2); + await expect(loadConfig.mock.results[1].value).resolves.toMatchObject({ tuiCommandDisplay: "inline", }); expect(context.ui.dialog.show).not.toHaveBeenCalled(); @@ -297,6 +308,132 @@ describe("V2 quota TUI commands", () => { }); }); + describe("typed quota slash commands", () => { + let projectDir: string; + + beforeEach(() => { + projectDir = realpathSync(mkdtempSync(join(tmpdir(), "opencode-quota-tui-typed-"))); + mkdirSync(join(projectDir, "global")); + mkdirSync(join(projectDir, "opencode-quota")); + vi.stubEnv("OPENCODE_CONFIG_DIR", join(projectDir, "global")); + return () => rmSync(projectDir, { recursive: true, force: true }); + }); + + /** Starts the TUI and waits for its first read of tuiCommandDisplay. */ + async function startTyped( + display: "dialog" | "inline", + route: { type: "home" } | { type: "session"; sessionID: string } = { type: "home" }, + ) { + writeFileSync( + join(projectDir, "opencode-quota", "quota-toast.json"), + JSON.stringify({ tuiCommandDisplay: display }), + ); + const tui = startTui({ directory: projectDir }, route); + await loadConfig.mock.results.at(-1)?.value; + const enter = tui.layers().find((layer) => layer.priority === 1)!; + return { ...tui, enter: enter.commands[0] }; + } + + it("parses only exact quota command names, with their arguments", () => { + expect(parseQuotaSlashCommand("/quota")).toEqual({ + command: "quota", + argumentsText: undefined, + }); + expect(parseQuotaSlashCommand(" /quota_status \n")).toEqual({ + command: "quota_status", + argumentsText: undefined, + }); + expect(parseQuotaSlashCommand("/tokens_between 2026-09-01 2026-09-25 ")).toEqual({ + command: "tokens_between", + argumentsText: "2026-09-01 2026-09-25", + }); + expect(parseQuotaSlashCommand("/pricing_refresh\tnow")).toEqual({ + command: "pricing_refresh", + argumentsText: "now", + }); + for (const text of ["/quotax", "/quota_statu", "quota", "hello /quota", "/", "", "/Quota"]) { + expect(parseQuotaSlashCommand(text)).toBeUndefined(); + } + }); + + it("registers one Enter binding above OpenCode's prompt, only in the base mode", async () => { + const { layers, enter } = await startTyped("dialog"); + + const layer = layers().find((item) => item.priority === 1)!; + expect(layer.mode).toBeUndefined(); + expect(layer.commands).toHaveLength(1); + expect(enter.bind).toBe("return"); + expect("id" in enter).toBe(false); + expect("slash" in enter).toBe(false); + }); + + it("dialog mode clears the prompt and runs the command with its arguments, synchronously", async () => { + const { context, editor, enter } = await startTyped("dialog", { + type: "session", + sessionID: "ses_open", + }); + editor.plainText = "/tokens_between 2026-09-01 2026-09-25"; + + const result = enter.run(); + + // A returned promise would count as handled, so run must not return one. + expect(result).toBeUndefined(); + expect(editor.clear).toHaveBeenCalledOnce(); + expect(context.ui.dialog.prompt).not.toHaveBeenCalled(); + await vi.waitFor(() => expect(context.ui.dialog.show).toHaveBeenCalledOnce()); + expect(rpc.command).toHaveBeenCalledExactlyOnceWith( + { command: "tokens_between", arguments: "2026-09-01 2026-09-25", sessionID: "ses_open" }, + expect.anything(), + ); + }); + + it("dialog mode runs /quota on Home without a session and without a prompt", async () => { + const { context, editor, enter } = await startTyped("dialog"); + editor.plainText = "/quota "; + + expect(enter.run()).toBeUndefined(); + + expect(editor.clear).toHaveBeenCalledOnce(); + await vi.waitFor(() => expect(context.ui.dialog.show).toHaveBeenCalledOnce()); + expect(rpc.command).toHaveBeenCalledExactlyOnceWith( + { command: "quota", arguments: undefined, sessionID: undefined }, + expect.anything(), + ); + }); + + it("leaves other text, inline mode, and a missing editor to OpenCode's submit", async () => { + const dialog = await startTyped("dialog"); + for (const text of ["hello", "/quotax", "/help", ""]) { + dialog.editor.plainText = text; + expect(dialog.enter.run()).toBe(false); + } + dialog.context.renderer.currentFocusedEditor = null; + expect(dialog.enter.run()).toBe(false); + expect(dialog.editor.clear).not.toHaveBeenCalled(); + + const inline = await startTyped("inline"); + inline.editor.plainText = "/quota"; + expect(inline.enter.run()).toBe(false); + expect(inline.editor.clear).not.toHaveBeenCalled(); + expect(rpc.command).not.toHaveBeenCalled(); + }); + + it("reads tuiCommandDisplay again after each typed command", async () => { + const { editor, enter } = await startTyped("inline"); + writeFileSync( + join(projectDir, "opencode-quota", "quota-toast.json"), + JSON.stringify({ tuiCommandDisplay: "dialog" }), + ); + editor.plainText = "/quota"; + + // The setting read at start still applies; the new one applies to the next command. + expect(enter.run()).toBe(false); + await loadConfig.mock.results.at(-1)?.value; + expect(enter.run()).toBeUndefined(); + expect(editor.clear).toHaveBeenCalledOnce(); + }); + }); + it("reads tuiCommandDisplay from the location's Git worktree root, like the server plugin", async () => { const repo = realpathSync(mkdtempSync(join(tmpdir(), "opencode-quota-tui-roots-"))); try { @@ -318,9 +455,12 @@ describe("V2 quota TUI commands", () => { }, }, }); - await vi.waitFor(() => expect(loadConfig).toHaveBeenCalledOnce()); + await vi.waitFor(() => expect(loadConfig).toHaveBeenCalledTimes(2)); - expect(loadConfig.mock.calls[0][2]).toEqual({ configRootDir: repo }); + expect(loadConfig.mock.calls.map((call) => call[2])).toEqual([ + { configRootDir: repo }, + { configRootDir: repo }, + ]); } finally { rmSync(repo, { recursive: true, force: true }); } @@ -384,7 +524,7 @@ describe("V2 quota TUI commands", () => { output, dialogSize: "xlarge", }); - const { context, command } = startTui(); + const { context, command, layers } = startTui(); await command("quota_status").run(); @@ -397,10 +537,7 @@ describe("V2 quota TUI commands", () => { expect(scrollbox?.props.maxHeight).toBe(22); expect(findNode(scrollbox?.props.children, "text")?.props.children).toBe(output); - const dialogLayer = context.keymap.layer.mock.calls[1][0]() as { - mode: string; - commands: Array<{ bind: string; run: () => void }>; - }; + const dialogLayer = layers().find((item) => item.mode === "modal")!; expect(dialogLayer.mode).toBe("modal"); expect(dialogLayer.commands.map((item) => item.bind)).toEqual([ "return", diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 3b24804d..465c4b24 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -192,6 +192,9 @@ async function setupV2Surfaces(client: ReturnType, provider for (const callback of events.get(event) ?? []) callback({ data: { sessionID } }); }; let commands: Array<{ id: string; run: () => Promise }> = []; + // The Enter binding that runs a quota command typed in the TUI prompt. + let enter: (() => unknown) | undefined; + const editor = { plainText: "", clear: vi.fn() }; // Records the title and scrollbox text of each quota output dialog the TUI shows. const dialog = vi.fn((_input: { title: string; message: string }) => {}); type Node = { type: string; props: Record }; @@ -227,6 +230,7 @@ async function setupV2Surfaces(client: ReturnType, provider const dispose = tuiPlugin.setup({ client: { ...client, rpc: createQuotaRpcBridge(handlers) }, location: { directory: process.cwd() }, + renderer: { currentFocusedEditor: editor }, theme: { surface: () => ({ text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, @@ -247,9 +251,10 @@ async function setupV2Surfaces(client: ReturnType, provider }, }, keymap: { - layer: (build: () => { mode: string; commands: typeof commands }) => { + layer: (build: () => { mode?: string; priority?: number; commands: typeof commands }) => { const layer = build(); if (layer.mode === "global") commands = layer.commands; + if (layer.priority === 1) enter = layer.commands[0].run; }, }, ui: { @@ -284,6 +289,10 @@ async function setupV2Surfaces(client: ReturnType, provider toast, emit, quota: quota!, + typeCommand: (text: string) => { + editor.plainText = text; + return enter!(); + }, renderSidebar: (sessionID: string) => renderSurface("sidebar.content", renderedSurfaces.sidebar, { sessionID }), renderSessionPrompt: (sessionID: string) => @@ -557,6 +566,11 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(client.session.prompt).not.toHaveBeenCalled(); const serverOutput = v2.dialog.mock.calls[0][0].message; expect(serverOutput).toMatch(/^Quota \(\/quota\)/); + // /quota typed in the TUI prompt opens the same report in the dialog and posts nothing. + expect(v2.typeCommand("/quota")).toBeUndefined(); + await vi.waitFor(() => expect(v2.dialog).toHaveBeenCalledTimes(2)); + expect(v2.dialog.mock.calls[1][0].message).toMatch(/^Quota \(\/quota\)/); + expect(client.session.prompt).not.toHaveBeenCalled(); expect(serverOutput).not.toContain("```"); expect(serverOutput).not.toMatch(/^#{1,6} /mu); expect(serverOutput).toMatch(/→ \[Team Accounting\]\n {2}Month quota/u); From db9eee107ad6697f6c0b7e7e44c3901da9c6f2d2 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:14:49 +0200 Subject: [PATCH 83/97] fix(tui): handle quota commands typed right after OpenCode starts OpenCode 2 mounts the TUI "app" slot again when it reconnects to its server. That happens when the server restarts, and it can also happen right after a cold start. Leaving a mount drops the keymap layers and event listeners registered in it, but the plugin skipped registering them again on the next mount. After such a remount, a typed /quota was no longer caught on Enter, and the listener that cancels a posted report was gone too. So OpenCode created a new session and the full report stayed in the chat, with no popup. The palette entries and quota toasts were also lost. Each mount of the app slot now disposes the previous listeners and registers the commands, the Enter binding, and the listeners again. While tuiCommandDisplay is still being read, a typed quota command now follows the default, "dialog", and opens the popup. Users with "inline" may see one popup if they type a quota command in the first moments after OpenCode starts. Reports for a session created from Home already work: OpenCode opens the new session before the server runs the command, so the report arrives for the session on screen. A test now covers that case. --- src/tui-v2.tsx | 11 ++++-- tests/tui-v2-commands.test.ts | 66 +++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 3b7af787..2c9533ef 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -426,6 +426,8 @@ async function runQuotaCommand( * typed in "inline" mode) post the report into the chat as a pending inbox item. When a * report arrives for the session on screen, tuiCommandDisplay decides what the TUI shows: "dialog" opens the report in the dialog and cancels the pending * item, so no chat message remains; "inline" leaves the report in the chat. + * A command sent from Home creates its session, and OpenCode opens that session before the + * server runs the command, so its report also arrives for the session on screen. * The TUI cannot tell which client typed the command, so in "dialog" mode it also removes a * report that Web or Desktop requested for the same session while the TUI shows it. */ @@ -486,7 +488,7 @@ function registerQuotaCommands(context: TuiContext): void { * the server command posts the report as before. * The layer has no mode, so it is off while the "/" list or a dialog is open. tuiCommandDisplay * must be known when Enter is pressed, so it is read at start and again after each typed - * quota command; until the first read finishes, typed commands go to the server. + * quota command. Until the first read finishes, typed commands follow the default, "dialog". */ function registerTypedQuotaCommands(context: TuiContext): void { let commandDisplay: TuiCommandDisplay | undefined; @@ -513,7 +515,7 @@ function registerTypedQuotaCommands(context: TuiContext): void { if (!editor || !typed) return false; const display = commandDisplay; readCommandDisplay(); - if (display !== "dialog") return false; + if (display === "inline") return false; editor.clear(); void runQuotaCommand(context, typed.command, getRouteSessionID(context), typed); }, @@ -583,7 +585,10 @@ const plugin = Plugin.define({ const disposeApp = api.ui.slot({ append: "app", render: () => { - if (disposeEvents) return null; + // OpenCode mounts this slot again when it reconnects to its server, which can also + // happen right after it starts. Leaving a mount drops its keymap layers and event + // listeners, so each mount replaces the listeners and registers everything again. + disposeEvents?.(); registerQuotaCommands(api); registerTypedQuotaCommands(api); const trigger = (event: TuiEvent, reason: "idle" | "compacted" | "question") => { diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 8058f0f7..b02c2839 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -54,6 +54,7 @@ function startTui( route: { type: "home" } | { type: "session"; sessionID: string } = { type: "home" }, ) { let layer: { commands: RegisteredCommand[] } | undefined; + let renderApp: (() => unknown) | undefined; const listeners = new Map(); const editor = { plainText: "", clear: vi.fn() }; const context = { @@ -84,11 +85,14 @@ function startTui( }, ui: { slot: vi.fn((claim) => { - if (claim.append === "app") claim.render(); + if (claim.append === "app") { + renderApp = claim.render; + claim.render(); + } return vi.fn(); }), toast: { show: vi.fn() }, - router: { current: vi.fn(() => route) }, + router: { current: vi.fn((): typeof route => route) }, dialog: { show: vi.fn((_render: () => unknown, onClose?: () => void) => onClose?.()), clear: vi.fn(), @@ -102,7 +106,9 @@ function startTui( const command = (id: string) => commands.find((item) => item.id === `quota.${id}`)!; const emit = (name: string, data: Record) => listeners.get(name)?.({ data }); const layers = () => context.keymap.layer.mock.calls.map(([build]) => build() as Layer); - return { context, editor, commands, command, emit, layers }; + /** OpenCode mounts the "app" slot again, for example after it reconnects to its server. */ + const mountAppAgain = () => renderApp!(); + return { context, editor, commands, command, emit, layers, mountAppAgain }; } describe("V2 quota TUI commands", () => { @@ -157,6 +163,21 @@ describe("V2 quota TUI commands", () => { ]); }); + it("registers its layers and listeners again when OpenCode mounts the app slot again", () => { + const { context, mountAppAgain } = startTui(); + const firstListeners = context.data.on.mock.results.map((result) => result.value); + const firstLayers = context.keymap.layer.mock.calls.length; + + mountAppAgain(); + + // The previous mount's layers left with it, so both layers are registered again. + expect(context.keymap.layer).toHaveBeenCalledTimes(firstLayers * 2); + expect(context.data.on).toHaveBeenCalledTimes(firstListeners.length * 2); + for (const dispose of firstListeners) expect(dispose).toHaveBeenCalledOnce(); + const secondListeners = context.data.on.mock.results.slice(firstListeners.length); + for (const { value: dispose } of secondListeners) expect(dispose).not.toHaveBeenCalled(); + }); + it("prompts for /tokens_between dates in the palette and stops when cancelled", async () => { const { context, command } = startTui(); @@ -284,6 +305,25 @@ describe("V2 quota TUI commands", () => { expect(rpc.command).not.toHaveBeenCalled(); }); + it("dialog mode handles the report of a session created from Home", async () => { + const { context, emit } = startTui({ directory: projectDir }, { type: "home" }); + // OpenCode opens the new session before the server runs the command. + context.ui.router.current.mockReturnValue({ type: "session", sessionID: "ses_new" }); + + emit("session.inbox.enqueued", { + sessionID: "ses_new", + inboxID: "msg_report", + item: item(report, metadata), + }); + await settle(); + + expect(context.client.session.inbox.cancel).toHaveBeenCalledExactlyOnceWith({ + sessionID: "ses_new", + inboxID: "msg_report", + }); + expect(context.ui.dialog.show).toHaveBeenCalledOnce(); + }); + it("inline mode leaves the report in the chat and opens no dialog", async () => { writeCommandDisplay("inline"); const { context, emit } = startTui( @@ -418,6 +458,26 @@ describe("V2 quota TUI commands", () => { expect(rpc.command).not.toHaveBeenCalled(); }); + it("opens the dialog, the default, before the first read of tuiCommandDisplay finishes", async () => { + writeFileSync( + join(projectDir, "opencode-quota", "quota-toast.json"), + JSON.stringify({ tuiCommandDisplay: "inline" }), + ); + const { context, editor, layers } = startTui({ directory: projectDir }); + const enter = layers().find((layer) => layer.priority === 1)!.commands[0]; + editor.plainText = "/quota"; + + // Enter right after start: the setting is still unknown. + expect(enter.run()).toBeUndefined(); + expect(editor.clear).toHaveBeenCalledOnce(); + await vi.waitFor(() => expect(context.ui.dialog.show).toHaveBeenCalledOnce()); + + // Once read, "inline" goes on to OpenCode's submit. + await loadConfig.mock.results.at(-1)?.value; + expect(enter.run()).toBe(false); + expect(editor.clear).toHaveBeenCalledOnce(); + }); + it("reads tuiCommandDisplay again after each typed command", async () => { const { editor, enter } = await startTyped("inline"); writeFileSync( From 69f364561aa26708cc06e8f90ee487ae67a5763d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:48:16 +0000 Subject: [PATCH 84/97] chore(release): sync package version to 5.0.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f0fff55..6e65c63f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slkiser/opencode-quota", - "version": "5.0.0", + "version": "5.0.0-beta.1", "packageManager": "pnpm@11.0.0", "description": "OpenCode quota & tokens usage with zero context window pollution. Supports GitHub Copilot, OpenAI (Plus/Pro), Chutes AI, Synthetic, Google AGY, Z.ai coding plan and more.", "type": "module", From b21b28a49111100670a1542460ccfdbdf4cb7a78 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:22:55 +0200 Subject: [PATCH 85/97] fix(tui): show quota report text in the base color like OpenCode's output dialog --- src/tui-v2.tsx | 2 +- tests/tui-v2-commands.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 2c9533ef..97b81b96 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -349,7 +349,7 @@ function QuotaOutputDialog(props: { }} maxHeight={maxHeight()} > - {props.message} + {props.message} diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index b02c2839..6e537ecb 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -595,7 +595,9 @@ describe("V2 quota TUI commands", () => { const scrollbox = findNode(tree, "scrollbox"); // 40 terminal rows: three quarters is 30, minus 8 rows of dialog chrome. expect(scrollbox?.props.maxHeight).toBe(22); - expect(findNode(scrollbox?.props.children, "text")?.props.children).toBe(output); + const text = findNode(scrollbox?.props.children, "text"); + expect(text?.props.children).toBe(output); + expect(text?.props.fg).toBe("base"); const dialogLayer = layers().find((item) => item.mode === "modal")!; expect(dialogLayer.mode).toBe("modal"); From 43a3da4192064a3d8ac72875cb6267add51a6cd1 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:33:36 +0200 Subject: [PATCH 86/97] feat(tui): draw quota reports from their structured document in the dialog --- src/lib/markdown-table.ts | 28 ++ src/lib/quota-command-format.ts | 2 +- src/lib/quota-dialog-command-specs.ts | 4 + src/lib/quota-dialog-commands.ts | 268 ++++++++++-------- src/lib/quota-report-message.ts | 22 +- src/lib/quota-stats-format.ts | 14 +- src/lib/quota-status.ts | 23 +- src/lib/report-document.ts | 98 ++++++- src/plugin.ts | 8 +- src/tui-v2.tsx | 93 +++++- tests/lib.quota-report-message.test.ts | 20 +- tests/plugin-rpc.test.ts | 9 + tests/plugin-v2.test.ts | 12 +- tests/plugin.maintainer-announcements.test.ts | 6 +- tests/plugin.quota-command.test.ts | 65 ++++- tests/plugin.quota-status-command.test.ts | 20 +- tests/plugin.tokens-session-all.test.ts | 59 +++- tests/report-document.test.ts | 122 +++++++- tests/tui-v2-commands.test.ts | 94 +++++- tests/v4-phase5-cross-surface.test.ts | 13 +- 20 files changed, 781 insertions(+), 199 deletions(-) diff --git a/src/lib/markdown-table.ts b/src/lib/markdown-table.ts index 629acf64..1c92477a 100644 --- a/src/lib/markdown-table.ts +++ b/src/lib/markdown-table.ts @@ -144,3 +144,31 @@ export function renderMarkdownTable(params: { for (const row of safeRows) lines.push(fmtRow(row)); return lines.join("\n"); } + +/** + * Pads each cell to its column width, as renderMarkdownTable does, for a table drawn + * without pipes. Widths count raw graphemes and cells are not escaped. + */ +export function padTableColumns(params: { + headers: string[]; + rows: string[][]; + aligns: Array<"left" | "right">; +}): { header: string[]; rows: string[][] } { + const colCount = params.headers.length; + const rows = params.rows.map((row) => { + const out: string[] = []; + for (let i = 0; i < colCount; i++) out.push((row[i] ?? "").replace(/\r?\n/g, " ")); + return out; + }); + + const widths = params.headers.map((header) => measureWidth(header)); + for (const row of rows) { + for (let i = 0; i < colCount; i++) { + widths[i] = Math.max(widths[i], measureWidth(row[i])); + } + } + + const padRow = (cells: string[]) => + cells.map((cell, i) => padCell(cell, widths[i], params.aligns[i] ?? "left", "raw")); + return { header: padRow(params.headers), rows: rows.map(padRow) }; +} diff --git a/src/lib/quota-command-format.ts b/src/lib/quota-command-format.ts index c8a4fb71..58f85353 100644 --- a/src/lib/quota-command-format.ts +++ b/src/lib/quota-command-format.ts @@ -125,7 +125,7 @@ function getCommandBasisLines(basis: AccountingRowInterpretation["basis"]): stri return details.map((detail) => ` ${detail}`); } -function buildQuotaCommandDocument(params: { +export function buildQuotaCommandDocument(params: { entries: QuotaToastEntry[]; errors: QuotaToastError[]; sessionTokens?: SessionTokensData; diff --git a/src/lib/quota-dialog-command-specs.ts b/src/lib/quota-dialog-command-specs.ts index fa40b412..fde57955 100644 --- a/src/lib/quota-dialog-command-specs.ts +++ b/src/lib/quota-dialog-command-specs.ts @@ -4,6 +4,7 @@ * does not load the quota and token report code. */ import { formatYmd, type Ymd } from "./command-parsing.js"; +import type { ReportDocument } from "./report-document.js"; export type QuotaDialogCommandId = | "quota" @@ -26,7 +27,10 @@ export type QuotaDialogCommandOutputResult = state: "output"; command: QuotaDialogCommandId; title: string; + /** The report as text, for the chat, the quota_status tool, and the CLI. */ output: string; + /** The same report, structured; the TUI dialog renders it. */ + document: ReportDocument; dialogSize: "medium" | "large" | "xlarge"; } | { diff --git a/src/lib/quota-dialog-commands.ts b/src/lib/quota-dialog-commands.ts index d162d91b..af6f9d17 100644 --- a/src/lib/quota-dialog-commands.ts +++ b/src/lib/quota-dialog-commands.ts @@ -5,7 +5,6 @@ import { } from "./command-parsing.js"; import type { RuntimeContextRootHints } from "./config-file-utils.js"; import { isCursorProviderId } from "./cursor-pricing.js"; -import { renderCommandHeading } from "./format-utils.js"; import { BUNDLED_MAINTAINER_ANNOUNCEMENTS, getMaintainerAnnouncementsSummary, @@ -20,7 +19,7 @@ import { setPricingSnapshotAutoRefresh, setPricingSnapshotSelection, } from "./modelsdev-pricing.js"; -import { formatQuotaCommand } from "./quota-command-format.js"; +import { buildQuotaCommandDocument } from "./quota-command-format.js"; import { QUOTA_DIALOG_COMMANDS_BY_ID, type QuotaDialogCommandId, @@ -52,8 +51,15 @@ import { SessionNotFoundError, type SessionTreeNode, } from "./quota-stats.js"; -import { formatQuotaStatsReport } from "./quota-stats-format.js"; -import { buildQuotaStatusReport, type SessionTokenError } from "./quota-status.js"; +import { buildQuotaStatsReportDocument } from "./quota-stats-format.js"; +import { buildQuotaStatusReportDocument, type SessionTokenError } from "./quota-status.js"; +import { + messageDocument, + type ReportDocument, + type ReportKvRow, + renderMarkdownReport, + renderPlainTextReport, +} from "./report-document.js"; import type { PricingSnapshotSource } from "./types.js"; import { getPackageVersion } from "./version.js"; @@ -74,6 +80,13 @@ function isTokenReportCommand(cmd: string): cmd is TokenReportCommandId { return TOKEN_REPORT_COMMANDS_BY_ID.has(cmd as TokenReportCommandId); } +/** A command's report: the document the TUI renders and the text everything else shows. */ +type CommandReport = { document: ReportDocument; output: string }; + +function plainTextReport(document: ReportDocument): CommandReport { + return { document, output: renderPlainTextReport(document) }; +} + function describeQuotaCommandCurrentSelection(params: { currentModel?: string; currentProviderID?: string; @@ -198,14 +211,14 @@ async function buildQuotaReport(params: { nodes: SessionTreeNode[]; }; generatedAtMs: number; -}): Promise { +}): Promise { const result = await aggregateUsage({ sinceMs: params.sinceMs, untilMs: params.untilMs, sessionID: params.filterSessionID, sessionIDs: params.filterSessionIDs, }); - return formatQuotaStatsReport({ + const document = buildQuotaStatsReportDocument({ title: params.title, result, topModels: params.topModels, @@ -220,6 +233,7 @@ async function buildQuotaReport(params: { modelNameMaxWidth: TUI_TOKEN_REPORT_MODEL_MAX_WIDTH, }, }); + return { document, output: renderMarkdownReport(document) }; } export interface QuotaStatusReportConfigPayload { @@ -259,6 +273,7 @@ export interface QuotaStatusReportPayload { } export interface QuotaStatusReportData { + document: ReportDocument | null; output: string | null; payload: QuotaStatusReportPayload | null; hasComparableProviderData: boolean; @@ -285,7 +300,7 @@ export async function buildStatusReportData(params: { }): Promise { const runtimeConfig = params.runtime.config; if (!runtimeConfig.enabled) { - return { output: null, payload: null, hasComparableProviderData: false }; + return { document: null, output: null, payload: null, hasComparableProviderData: false }; } await kickPricingRefresh({ reason: "status", @@ -375,7 +390,7 @@ export async function buildStatusReportData(params: { enabledProviders: announcementProviderIds, }); - const output = await buildQuotaStatusReport({ + const document = await buildQuotaStatusReportDocument({ runtimeRoots: params.runtime.roots, configSource: params.runtime.configMeta.source, configPaths: params.runtime.configMeta.paths, @@ -436,34 +451,24 @@ export async function buildStatusReportData(params: { }; return { - output, + document, + output: renderPlainTextReport(document), payload, hasComparableProviderData: providerLiveProbes.some((probe) => probe.result.entries.length > 0), }; } -async function buildStatusReport(params: { - runtime: QuotaRuntimeContext; - sessionID?: string; - generatedAtMs: number; - lastSessionTokenError?: SessionTokenError; - log?: (message: string, extra?: Record) => Promise; - onDetectedProviderIds?: (providerIds: string[]) => Promise; -}): Promise { - return (await buildStatusReportData(params)).output; -} - function formatIsoTimestamp(timestampMs: number | undefined): string { return typeof timestampMs === "number" && Number.isFinite(timestampMs) && timestampMs > 0 ? new Date(timestampMs).toISOString() : "(none)"; } -function buildPricingRefreshCommandOutput(params: { +function buildPricingRefreshCommandReport(params: { result: PricingRefreshResult; configuredSelection: string; generatedAtMs: number; -}): string { +}): CommandReport { const meta = getPricingSnapshotMeta(); const activeSource = getPricingSnapshotSource(); const resultLabel = @@ -471,61 +476,80 @@ function buildPricingRefreshCommandOutput(params: { params.result.state.lastResult ?? (params.result.updated ? "success" : "unknown"); - const lines = [ - renderCommandHeading({ - title: "Pricing Refresh (/pricing_refresh)", - generatedAtMs: params.generatedAtMs, - }), - "", - "refresh:", - `- attempted: ${params.result.attempted ? "true" : "false"}`, - `- result: ${resultLabel}`, - `- runtime_snapshot_persisted: ${params.result.updated ? "true" : "false"}`, + const refreshRows: ReportKvRow[] = [ + { key: "attempted", value: params.result.attempted ? "true" : "false" }, + { key: "result", value: resultLabel }, + { key: "runtime_snapshot_persisted", value: params.result.updated ? "true" : "false" }, ]; if (params.result.error) { - lines.push(`- error: ${params.result.error}`); + refreshRows.push({ key: "error", value: params.result.error }); } - lines.push(""); - lines.push("pricing_snapshot:"); - lines.push(`- selection: configured=${params.configuredSelection} active=${activeSource}`); - lines.push( - `- active_snapshot: source=${meta.source} generated_at=${formatIsoTimestamp(meta.generatedAt)} units=${meta.units}`, - ); - lines.push( - `- runtime_paths: snapshot=${getRuntimePricingSnapshotPath()} refresh_state=${getRuntimePricingRefreshStatePath()}`, - ); + const snapshotRows: ReportKvRow[] = [ + { key: "selection", value: `configured=${params.configuredSelection} active=${activeSource}` }, + { + key: "active_snapshot", + value: `source=${meta.source} generated_at=${formatIsoTimestamp(meta.generatedAt)} units=${meta.units}`, + }, + { + key: "runtime_paths", + value: `snapshot=${getRuntimePricingSnapshotPath()} refresh_state=${getRuntimePricingRefreshStatePath()}`, + }, + ]; if (params.configuredSelection === "bundled" && params.result.updated) { - lines.push( - "- selection_note: runtime snapshot refreshed locally, but active reports remain pinned to bundled pricing", - ); + snapshotRows.push({ + key: "selection_note", + value: + "runtime snapshot refreshed locally, but active reports remain pinned to bundled pricing", + }); } - return lines.join("\n"); + return plainTextReport({ + heading: { title: "Pricing Refresh (/pricing_refresh)", generatedAtMs: params.generatedAtMs }, + sections: [ + { id: "refresh", title: "refresh:", blocks: [{ kind: "kv", rows: refreshRows }] }, + { + id: "pricing_snapshot", + title: "pricing_snapshot:", + blocks: [{ kind: "kv", rows: snapshotRows }], + }, + ], + }); } -function buildTokenReportUnavailableOutput(params: { +function buildTokenReportUnavailableReport(params: { command: `/${string}`; generatedAtMs: number; error: SessionNotFoundError; -}): string { - const lines = [ - renderCommandHeading({ +}): CommandReport { + return plainTextReport({ + heading: { title: `Token report unavailable (${params.command})`, generatedAtMs: params.generatedAtMs, - }), - "", - "session_lookup_error:", - `- session_id: ${params.error.sessionID}`, - `- error: ${params.error.message}`, - `- checked_path: ${params.error.checkedPath}`, - ]; - - return lines.join("\n"); + }, + sections: [ + { + id: "session_lookup_error", + title: "session_lookup_error:", + blocks: [ + { + kind: "kv", + rows: [ + { key: "session_id", value: params.error.sessionID }, + { key: "error", value: params.error.message }, + { key: "checked_path", value: params.error.checkedPath }, + ], + }, + ], + }, + ], + }); } -async function buildQuotaAnnouncementsCommandOutput(runtime: QuotaRuntimeContext): Promise { +async function buildQuotaAnnouncementsCommandReport( + runtime: QuotaRuntimeContext, +): Promise { let activeAnnouncements: ReturnType< typeof getMaintainerAnnouncementsSummary >["activeAnnouncements"] = []; @@ -546,11 +570,10 @@ async function buildQuotaAnnouncementsCommandOutput(runtime: QuotaRuntimeContext activeAnnouncements = summary.activeAnnouncements; } - const lines = ["Maintainer announcements", ""]; + const lines: string[] = []; if (activeAnnouncements.length === 0) { lines.push("No current announcements."); - return lines.join("\n"); } for (const evaluation of activeAnnouncements) { @@ -560,31 +583,37 @@ async function buildQuotaAnnouncementsCommandOutput(runtime: QuotaRuntimeContext } } - return lines.join("\n"); + return plainTextReport({ + sections: [ + { id: "title", blocks: [{ kind: "lines", lines: ["Maintainer announcements"] }] }, + { id: "announcements", blocks: [{ kind: "lines", lines }] }, + ], + }); } -function outputResult(params: { - command: QuotaDialogCommandId; - output: string; -}): QuotaDialogCommandOutputResult { - const spec = QUOTA_DIALOG_COMMANDS_BY_ID.get(params.command)!; +function outputResult( + command: QuotaDialogCommandId, + report: CommandReport, +): QuotaDialogCommandOutputResult { + const spec = QUOTA_DIALOG_COMMANDS_BY_ID.get(command)!; return { state: "output", - command: params.command, + command, title: spec.title, - output: params.output, + output: report.output, + document: report.document, dialogSize: spec.dialogSize, }; } -async function buildTokenReportCommandOutput(params: { +async function buildTokenReportCommandReport(params: { command: TokenReportCommandId; arguments?: string; sessionID?: string; generatedAtMs: number; runtime: QuotaRuntimeContext; log?: (message: string, extra?: Record) => Promise; -}): Promise { +}): Promise { const spec = TOKEN_REPORT_COMMANDS_BY_ID.get(params.command)!; const sessionID = params.sessionID; const untilMs = params.generatedAtMs; @@ -596,7 +625,7 @@ async function buildTokenReportCommandOutput(params: { }); if (!sessionID && (spec.kind === "session" || spec.kind === "session_tree")) { - return buildTokenReportUnavailableOutput({ + return buildTokenReportUnavailableReport({ command: spec.template, generatedAtMs: params.generatedAtMs, error: new SessionNotFoundError("(none)", "(none)"), @@ -607,7 +636,11 @@ async function buildTokenReportCommandOutput(params: { if (spec.kind === "between") { const parsed = parseQuotaBetweenArgs(params.arguments); if (!parsed.ok) { - return `Invalid arguments for /${spec.id}\n\n${parsed.error}\n\nExpected: /${spec.id} YYYY-MM-DD YYYY-MM-DD\nExample: /${spec.id} 2026-01-01 2026-01-15`; + return plainTextReport( + messageDocument( + `Invalid arguments for /${spec.id}\n\n${parsed.error}\n\nExpected: /${spec.id} YYYY-MM-DD YYYY-MM-DD\nExample: /${spec.id} 2026-01-01 2026-01-15`, + ), + ); } const sinceMs = startOfLocalDayMs(parsed.startYmd); @@ -674,7 +707,7 @@ async function buildTokenReportCommandOutput(params: { }); } catch (err) { if (err instanceof SessionNotFoundError) { - return buildTokenReportUnavailableOutput({ + return buildTokenReportUnavailableReport({ command: spec.template, generatedAtMs: params.generatedAtMs, error: err, @@ -725,27 +758,29 @@ export async function buildQuotaDialogCommandOutput(params: { (reportData.selection?.filteringByCurrentSelection && reportData.selection.filtered.length === 0) ) { - return outputResult({ - command: params.command, - output: buildQuotaCommandUnavailableMessage(reportData), - }); + return outputResult( + params.command, + plainTextReport(messageDocument(buildQuotaCommandUnavailableMessage(reportData))), + ); } - return outputResult({ - command: params.command, - output: formatQuotaCommand({ - ...reportData.data, - generatedAtMs, - percentDisplayMode: runtime.config.percentDisplayMode, - percentLabelStyle: runtime.config.percentLabelStyle, - accountingDetail: runtime.config.accountingDetail, - resetTimeSpaced: runtime.config.resetTimeSpaced, - }), - }); + return outputResult( + params.command, + plainTextReport( + buildQuotaCommandDocument({ + ...reportData.data, + generatedAtMs, + percentDisplayMode: runtime.config.percentDisplayMode, + percentLabelStyle: runtime.config.percentLabelStyle, + accountingDetail: runtime.config.accountingDetail, + resetTimeSpaced: runtime.config.resetTimeSpaced, + }), + ), + ); } if (params.command === "quota_status") { - const output = await buildStatusReport({ + const data = await buildStatusReportData({ runtime, sessionID: params.sessionID, generatedAtMs, @@ -753,33 +788,36 @@ export async function buildQuotaDialogCommandOutput(params: { log: params.log, onDetectedProviderIds: params.onDetectedProviderIds, }); - return output - ? outputResult({ command: params.command, output }) + return data.document && data.output + ? outputResult(params.command, { document: data.document, output: data.output }) : { state: "noop", command: params.command, reason: "disabled" }; } if (params.command === "quota_announcements") { if ((params.arguments ?? "").trim()) { - return outputResult({ - command: params.command, - output: - "Invalid arguments for /quota_announcements\n\nThis command does not accept arguments.\n\nUsage: /quota_announcements", - }); + return outputResult( + params.command, + plainTextReport( + messageDocument( + "Invalid arguments for /quota_announcements\n\nThis command does not accept arguments.\n\nUsage: /quota_announcements", + ), + ), + ); } - return outputResult({ - command: params.command, - output: await buildQuotaAnnouncementsCommandOutput(runtime), - }); + return outputResult(params.command, await buildQuotaAnnouncementsCommandReport(runtime)); } if (params.command === "pricing_refresh") { if ((params.arguments ?? "").trim()) { - return outputResult({ - command: params.command, - output: - "Invalid arguments for /pricing_refresh\n\nThis command does not accept arguments.\n\nUsage:\n/pricing_refresh", - }); + return outputResult( + params.command, + plainTextReport( + messageDocument( + "Invalid arguments for /pricing_refresh\n\nThis command does not accept arguments.\n\nUsage:\n/pricing_refresh", + ), + ), + ); } const result = await maybeRefreshPricingSnapshot({ @@ -788,20 +826,20 @@ export async function buildQuotaDialogCommandOutput(params: { snapshotSelection: runtime.config.pricingSnapshot.source, allowRefreshWhenSelectionBundled: true, }); - return outputResult({ - command: params.command, - output: buildPricingRefreshCommandOutput({ + return outputResult( + params.command, + buildPricingRefreshCommandReport({ result, configuredSelection: runtime.config.pricingSnapshot.source, generatedAtMs, }), - }); + ); } if (isTokenReportCommand(params.command)) { - return outputResult({ - command: params.command, - output: await buildTokenReportCommandOutput({ + return outputResult( + params.command, + await buildTokenReportCommandReport({ command: params.command, arguments: params.arguments, sessionID: params.sessionID, @@ -809,7 +847,7 @@ export async function buildQuotaDialogCommandOutput(params: { runtime, log: params.log, }), - }); + ); } return { state: "noop", command: params.command, reason: "disabled" }; diff --git a/src/lib/quota-report-message.ts b/src/lib/quota-report-message.ts index bc111dd6..4c23e6b4 100644 --- a/src/lib/quota-report-message.ts +++ b/src/lib/quota-report-message.ts @@ -4,6 +4,7 @@ * can keep them out of every model request, and the TUI can show them in its dialog. */ import type { QuotaDialogCommandId } from "./quota-dialog-command-specs.js"; +import { isReportDocument, type ReportDocument } from "./report-document.js"; /** Metadata key on every quota report message. OpenCode stores message metadata with the message. */ export const QUOTA_REPORT_METADATA_KEY = "opencodeQuota"; @@ -12,6 +13,8 @@ export type QuotaReportMetadata = { command: QuotaDialogCommandId; title: string; at: number; + /** The report, structured, so the TUI can show it in its dialog. */ + document: ReportDocument; }; /** @@ -28,14 +31,6 @@ export function formatQuotaReportMessage(report: string): string { return `${QUOTA_REPORT_START}\n${report}\n${QUOTA_REPORT_END}`; } -/** The report inside a quota report message, without its first and last line. */ -export function readQuotaReport(text: string): string { - const prefix = `${QUOTA_REPORT_START}\n`; - const suffix = `\n${QUOTA_REPORT_END}`; - if (!text.startsWith(prefix) || !text.endsWith(suffix)) return text; - return text.slice(prefix.length, text.length - suffix.length); -} - export function containsQuotaReport(text: string): boolean { return text.includes(QUOTA_REPORT_START); } @@ -50,9 +45,14 @@ export function readQuotaReportMetadata( ): QuotaReportMetadata | undefined { const value = metadata?.[QUOTA_REPORT_METADATA_KEY]; if (!value || typeof value !== "object") return undefined; - const { command, title, at } = value as Record; - if (typeof command !== "string" || typeof title !== "string" || typeof at !== "number") { + const { command, title, at, document } = value as Record; + if ( + typeof command !== "string" || + typeof title !== "string" || + typeof at !== "number" || + !isReportDocument(document) + ) { return undefined; } - return { command: command as QuotaDialogCommandId, title, at }; + return { command: command as QuotaDialogCommandId, title, at, document }; } diff --git a/src/lib/quota-stats-format.ts b/src/lib/quota-stats-format.ts index 37cb36b1..ddaa0993 100644 --- a/src/lib/quota-stats-format.ts +++ b/src/lib/quota-stats-format.ts @@ -154,7 +154,7 @@ function truncateTitle(title: string | undefined): string { return trimmed.slice(0, 10) + "…" + trimmed.slice(-10); } -export function formatQuotaStatsReport(params: { +type QuotaStatsReportParams = { title: string; result: AggregateResult; topModels?: number; @@ -169,7 +169,13 @@ export function formatQuotaStatsReport(params: { }; generatedAtMs?: number; tableOptions?: QuotaStatsReportTableOptions; -}): string { +}; + +export function formatQuotaStatsReport(params: QuotaStatsReportParams): string { + return renderMarkdownReport(buildQuotaStatsReportDocument(params)); +} + +export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): ReportDocument { const topModels = params.topModels ?? 12; const topSessions = params.topSessions ?? 8; const r = params.result; @@ -488,13 +494,11 @@ export function formatQuotaStatsReport(params: { }); } - const document: ReportDocument = { + return { heading: { title: params.title, generatedAtMs: params.generatedAtMs, }, sections, }; - - return renderMarkdownReport(document); } diff --git a/src/lib/quota-status.ts b/src/lib/quota-status.ts index 592fe142..fd856427 100644 --- a/src/lib/quota-status.ts +++ b/src/lib/quota-status.ts @@ -39,7 +39,12 @@ import { getQuotaProviderDisplayLabel } from "./provider-metadata.js"; import type { QuotaProviderDefinition } from "./quota-providers.js"; import { isMaintainedQuotaProviderTuning } from "./quota-providers.js"; import { aggregateUsage } from "./quota-stats.js"; -import { type ReportKvRow, type ReportSection, renderPlainTextReport } from "./report-document.js"; +import { + type ReportDocument, + type ReportKvRow, + type ReportSection, + renderPlainTextReport, +} from "./report-document.js"; import { totalTokenBuckets } from "./token-buckets.js"; import type { CursorQuotaPlan, @@ -669,7 +674,7 @@ function supportedProviderPricingRow(params: { }; } -export async function buildQuotaStatusReport(params: { +type QuotaStatusReportParams = { configSource: string; configPaths: string[]; globalConfigPaths?: string[]; @@ -697,7 +702,15 @@ export async function buildQuotaStatusReport(params: { summary: MaintainerAnnouncementsSummary; }; generatedAtMs?: number; -}): Promise { +}; + +export async function buildQuotaStatusReport(params: QuotaStatusReportParams): Promise { + return renderPlainTextReport(await buildQuotaStatusReportDocument(params)); +} + +export async function buildQuotaStatusReportDocument( + params: QuotaStatusReportParams, +): Promise { const version = await getPackageVersion(); const v = version ?? "unknown"; const modelDisplay = params.currentModel @@ -1114,11 +1127,11 @@ export async function buildQuotaStatusReport(params: { } sections.push(createKvSection("unknown_pricing", "unknown_pricing:", unknownRows)); - return renderPlainTextReport({ + return { heading: { title: `Quota Status (opencode-quota v${v}) (/quota_status)`, generatedAtMs: params.generatedAtMs, }, sections, - }); + }; } diff --git a/src/lib/report-document.ts b/src/lib/report-document.ts index e055d073..4425a4f0 100644 --- a/src/lib/report-document.ts +++ b/src/lib/report-document.ts @@ -46,11 +46,91 @@ function hasBlockContent(block: ReportBlock): boolean { } } -function getRenderableBlocks(section: ReportSection): ReportBlock[] { - return section.blocks.filter(hasBlockContent); +/** The sections every renderer shows: empty blocks dropped, then sections left with nothing. */ +export function renderableSections(document: ReportDocument): ReportSection[] { + return document.sections + .map((section) => ({ ...section, blocks: section.blocks.filter(hasBlockContent) })) + .filter((section) => section.title || section.blocks.length > 0); +} + +/** A document of one plain message; it renders back to the same text. */ +export function messageDocument(text: string): ReportDocument { + return { sections: [{ id: "message", blocks: [{ kind: "lines", lines: text.split("\n") }] }] }; +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((item) => typeof item === "string"); +} + +function isOptional(value: unknown, type: "string" | "number" | "boolean"): boolean { + return value === undefined || typeof value === type; +} + +function isReportKvRow(value: unknown): boolean { + return ( + isObject(value) && + typeof value.key === "string" && + isOptional(value.value, "string") && + (value.indent === undefined || value.indent === 0 || value.indent === 1) && + isOptional(value.trailingColon, "boolean") + ); +} + +function isReportBlock(value: unknown): boolean { + if (!isObject(value)) return false; + switch (value.kind) { + case "lines": + return isStringArray(value.lines); + case "kv": + return Array.isArray(value.rows) && value.rows.every(isReportKvRow); + case "table": + return ( + isStringArray(value.headers) && + Array.isArray(value.rows) && + value.rows.every(isStringArray) && + Array.isArray(value.aligns) && + value.aligns.every((align) => align === "left" || align === "right") && + (value.widthMode === undefined || + value.widthMode === "raw" || + value.widthMode === "markdown-conceal") + ); + default: + return false; + } +} + +/** Checks the shape of a document read back from stored message metadata. */ +export function isReportDocument(value: unknown): value is ReportDocument { + if (!isObject(value)) return false; + const heading = value.heading; + if ( + heading !== undefined && + !( + isObject(heading) && + typeof heading.title === "string" && + isOptional(heading.generatedAtMs, "number") + ) + ) { + return false; + } + return ( + Array.isArray(value.sections) && + value.sections.every( + (section) => + isObject(section) && + typeof section.id === "string" && + isOptional(section.title, "string") && + Array.isArray(section.blocks) && + section.blocks.every(isReportBlock), + ) + ); } -function renderKvRow(row: ReportKvRow): string { +export function renderKvRow(row: ReportKvRow): string { const indent = row.indent === 1 ? " " : ""; if (row.value !== undefined) { return `${indent}- ${row.key}: ${row.value}`; @@ -106,11 +186,7 @@ export function renderPlainTextReport(document: ReportDocument): string { ); } - const sections = document.sections - .map((section) => ({ ...section, blocks: getRenderableBlocks(section) })) - .filter((section) => section.title || section.blocks.length > 0); - - for (const section of sections) { + for (const section of renderableSections(document)) { if (lines.length > 0) lines.push(""); if (section.title) { @@ -138,11 +214,7 @@ export function renderMarkdownReport(document: ReportDocument): string { ); } - const sections = document.sections - .map((section) => ({ ...section, blocks: getRenderableBlocks(section) })) - .filter((section) => section.title || section.blocks.length > 0); - - for (const section of sections) { + for (const section of renderableSections(document)) { if (lines.length > 0) lines.push(""); if (section.title) { diff --git a/src/plugin.ts b/src/plugin.ts index c3e09777..7f13719c 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -32,6 +32,7 @@ import { type QuotaSurfaceHost, writeQuotaExportIfEnabled, } from "./lib/quota-surface-data.js"; +import { messageDocument } from "./lib/report-document.js"; import { QuotaRpc, type QuotaRpcCommandOutput } from "./rpc.js"; type ModelMessage = { @@ -167,11 +168,15 @@ export const QuotaToastPlugin = Plugin.define({ } catch (error) { // Return the reason as output so the TUI shows it instead of OpenCode's rpc.internal. const spec = QUOTA_DIALOG_COMMANDS.find((item) => item.id === input.command)!; + const output = sanitizeDisplayText( + error instanceof Error ? error.message : String(error), + ); return { state: "output", command: input.command, title: spec.title, - output: sanitizeDisplayText(error instanceof Error ? error.message : String(error)), + output, + document: messageDocument(output), dialogSize: spec.dialogSize, }; } @@ -221,6 +226,7 @@ export const QuotaToastPlugin = Plugin.define({ command: spec.id, title: result.title, at: Date.now(), + document: result.document, }; // The report is posted as the user's message. resume: false admits it without // starting a model turn, so it waits in the session inbox, where Web, Desktop, diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 97b81b96..30e33277 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -8,12 +8,20 @@ import { createSignal, onCleanup, Show } from "solid-js"; import { loadConfig } from "./lib/config.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; +import { formatLocalCallTimestamp } from "./lib/format-utils.js"; +import { padTableColumns } from "./lib/markdown-table.js"; import { parseQuotaSlashCommand, QUOTA_DIALOG_COMMANDS, type QuotaDialogCommandId, } from "./lib/quota-dialog-command-specs.js"; -import { readQuotaReport, readQuotaReportMetadata } from "./lib/quota-report-message.js"; +import { readQuotaReportMetadata } from "./lib/quota-report-message.js"; +import { + type ReportBlock, + type ReportDocument, + renderableSections, + renderKvRow, +} from "./lib/report-document.js"; import type { TuiCommandDisplay } from "./lib/types.js"; import { QuotaRpc, @@ -50,6 +58,7 @@ type KeymapCommand = { type DialogTheme = { text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA } } }; background: { action: { primary: { focused: RGBA } } }; + markdown: { heading: RGBA }; }; type QuotaRpcCallOptions = { location: { directory: string }; signal: AbortSignal }; type QuotaRpcClient = { @@ -285,6 +294,76 @@ function reportFailure(error: unknown): void { console.warn(`[opencode-quota] failed to load quota: ${rpcErrorMessage(error)}${hint}`); } +/** + * One block of a report. Lines and key-value rows wrap at words. A table keeps each row on + * one line: its cells are padded to their column widths and joined by two spaces, with the + * header row bold in the markdown heading color. + */ +function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX.Element { + const block = props.block; + if (block.kind === "lines") { + return {sanitizeDisplayText(block.lines.join("\n"))}; + } + if (block.kind === "kv") { + return ( + + {sanitizeDisplayText(block.rows.map(renderKvRow).join("\n"))} + + ); + } + const table = padTableColumns({ + headers: block.headers.map(sanitizeDisplayText), + rows: block.rows.map((row) => row.map(sanitizeDisplayText)), + aligns: block.aligns, + }); + return ( + + + {table.header.join(" ")} + + {table.rows.map((row) => ( + + {row.join(" ")} + + ))} + + ); +} + +/** + * Draws a report document with the same spacing as its plain text: one blank row between + * the heading, the sections, and the blocks of a section, and a section title directly + * above its first block. The heading and section titles are bold. + */ +function ReportDocumentView(props: { document: ReportDocument; theme: DialogTheme }): JSX.Element { + const heading = props.document.heading; + return ( + + {heading ? ( + + {sanitizeDisplayText( + `${heading.title} ${formatLocalCallTimestamp(heading.generatedAtMs)}`, + )} + + ) : null} + {renderableSections(props.document).map((section) => ( + + {section.title ? ( + + {sanitizeDisplayText(section.title)} + + ) : null} + + {section.blocks.map((block) => ( + + ))} + + + ))} + + ); +} + /** * Shows command output like OpenCode's alert dialog, but inside a scrollbox so long * reports (/quota_status, /tokens_*) stay reachable. The mouse wheel scrolls the box; @@ -294,7 +373,7 @@ function reportFailure(error: unknown): void { function QuotaOutputDialog(props: { context: TuiContext; title: string; - message: string; + document: ReportDocument; }): JSX.Element { const theme = () => props.context.theme.surface("dialog"); const dimensions = useTerminalDimensions(); @@ -349,7 +428,7 @@ function QuotaOutputDialog(props: { }} maxHeight={maxHeight()} > - {props.message} + @@ -368,11 +447,11 @@ function QuotaOutputDialog(props: { function showQuotaOutputDialog( context: TuiContext, - output: { title: string; message: string; dialogSize: "medium" | "large" | "xlarge" }, + output: { title: string; document: ReportDocument; dialogSize: "medium" | "large" | "xlarge" }, ): Promise { return new Promise((resolve) => { context.ui.dialog.show( - () => , + () => , resolve, ); context.ui.dialog.set({ size: output.dialogSize }); @@ -409,7 +488,7 @@ async function runQuotaCommand( if (result.state === "noop") return; await showQuotaOutputDialog(context, { title: result.title, - message: result.output, + document: result.document, dialogSize: result.dialogSize, }); } catch (error) { @@ -456,7 +535,7 @@ async function showPostedQuotaReport(context: TuiContext, event: TuiEvent): Prom }); void showQuotaOutputDialog(context, { title: report.title, - message: readQuotaReport(item.payload.text), + document: report.document, dialogSize: spec.dialogSize, }); } diff --git a/tests/lib.quota-report-message.test.ts b/tests/lib.quota-report-message.test.ts index 84a65b03..a24c1d29 100644 --- a/tests/lib.quota-report-message.test.ts +++ b/tests/lib.quota-report-message.test.ts @@ -3,21 +3,19 @@ import { describe, expect, it } from "vitest"; import { containsQuotaReport, formatQuotaReportMessage, - readQuotaReport, readQuotaReportMetadata, removeQuotaReports, } from "../src/lib/quota-report-message.js"; +import { messageDocument } from "../src/lib/report-document.js"; describe("quota report messages", () => { - it("wraps the report in a fixed first and last line and reads it back", () => { + it("wraps the report in a fixed first and last line", () => { const message = formatQuotaReportMessage("# Quota\nopenai ███░░ 42%"); expect(message).toBe( "[OpenCode Quota report]\n# Quota\nopenai ███░░ 42%\n[End of OpenCode Quota report]", ); expect(containsQuotaReport(message)).toBe(true); - expect(readQuotaReport(message)).toBe("# Quota\nopenai ███░░ 42%"); - expect(readQuotaReport("plain text")).toBe("plain text"); }); it("cuts every complete report out of text and leaves everything else", () => { @@ -33,9 +31,21 @@ describe("quota report messages", () => { }); it("reads only well-formed report metadata", () => { - const metadata = { command: "quota", title: "OpenCode Quota", at: 1 }; + const metadata = { + command: "quota", + title: "OpenCode Quota", + at: 1, + document: messageDocument("openai 42%"), + }; expect(readQuotaReportMetadata({ opencodeQuota: metadata })).toEqual(metadata); + // No report document, or a malformed one: the TUI has nothing to draw. + expect( + readQuotaReportMetadata({ opencodeQuota: { ...metadata, document: undefined } }), + ).toBeUndefined(); + expect( + readQuotaReportMetadata({ opencodeQuota: { ...metadata, document: "openai 42%" } }), + ).toBeUndefined(); expect(readQuotaReportMetadata(undefined)).toBeUndefined(); expect(readQuotaReportMetadata({ displayText: "hi" })).toBeUndefined(); expect(readQuotaReportMetadata({ opencodeQuota: "quota" })).toBeUndefined(); diff --git a/tests/plugin-rpc.test.ts b/tests/plugin-rpc.test.ts index 5a09c907..eda8c76b 100644 --- a/tests/plugin-rpc.test.ts +++ b/tests/plugin-rpc.test.ts @@ -2,6 +2,11 @@ import { existsSync } from "node:fs"; import { rm } from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + messageDocument, + type ReportDocument, + renderPlainTextReport, +} from "../src/lib/report-document.js"; import type { DEFAULT_CONFIG } from "../src/lib/types.js"; import { createFakeIntegration } from "./helpers/fake-integration.js"; import { @@ -224,6 +229,7 @@ describe("server quota RPC", () => { const output = (await call("command", { command: "quota", sessionID: "session-1" })) as { output: string; + document: ReportDocument; }; expect(output).toEqual( expect.objectContaining({ @@ -234,6 +240,7 @@ describe("server quota RPC", () => { }), ); expect(output.output).toContain("Copilot"); + expect(renderPlainTextReport(output.document)).toBe(output.output); }); it("never writes detected providers to the global config from the palette", async () => { @@ -246,6 +253,7 @@ describe("server quota RPC", () => { command: params.command, title: "OpenCode Quota Status", output: "status", + document: messageDocument("status"), dialogSize: "xlarge", }; }); @@ -275,6 +283,7 @@ describe("server quota RPC", () => { command: "quota", title: "OpenCode Quota", output: "quota broke", + document: messageDocument("quota broke"), dialogSize: "xlarge", }); }); diff --git a/tests/plugin-v2.test.ts b/tests/plugin-v2.test.ts index 6d922750..8f8ba7d4 100644 --- a/tests/plugin-v2.test.ts +++ b/tests/plugin-v2.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { messageDocument } from "../src/lib/report-document.js"; + const buildOutput = vi.hoisted(() => vi.fn()); vi.mock("../src/lib/quota-dialog-commands.js", async (importOriginal) => ({ ...(await importOriginal()), @@ -135,10 +137,12 @@ describe("V2 server plugin", () => { it("posts the report as a user message that never starts a model turn", async () => { const { ctx, commands, calls } = createContext(); + const document = messageDocument("# Quota\nopenai 42%\u001b[31m"); buildOutput.mockResolvedValue({ state: "output", title: "OpenCode Quota", output: "# Quota\nopenai 42%\u001b[31m", + document, }); vi.spyOn(Date, "now").mockReturnValue(1_790_000_000_000); @@ -154,8 +158,14 @@ describe("V2 server plugin", () => { expect(ctx.session.prompt.mock.calls[0][0]).toEqual({ sessionID: "session-web", text: formatQuotaReportMessage("# Quota\nopenai 42%"), + // The TUI sanitizes the document when it draws it. metadata: { - opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1_790_000_000_000 }, + opencodeQuota: { + command: "quota", + title: "OpenCode Quota", + at: 1_790_000_000_000, + document, + }, }, delivery: "steer", resume: false, diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index a6557062..9653929e 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -1,6 +1,7 @@ import { rm } from "fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { renderPlainTextReport } from "../src/lib/report-document.js"; import { createFakeIntegration } from "./helpers/fake-integration.js"; import { createAlibabaAuthModuleMock, @@ -202,7 +203,10 @@ async function buildAnnouncementsDialogOutput(params: { }); expect(params.client.session.prompt).not.toHaveBeenCalled(); expect(result.state).toBe("output"); - return result.state === "output" ? result.output : ""; + if (result.state !== "output") return ""; + // Every command here renders its document as plain text. + expect(renderPlainTextReport(result.document)).toBe(result.output); + return result.output; } async function flushMaintainerFallbackWork(): Promise { diff --git a/tests/plugin.quota-command.test.ts b/tests/plugin.quota-command.test.ts index f56ad6ac..de128063 100644 --- a/tests/plugin.quota-command.test.ts +++ b/tests/plugin.quota-command.test.ts @@ -2,6 +2,8 @@ import { rm } from "fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { QuotaProviderContext } from "../src/lib/entries.js"; +import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; +import { renderPlainTextReport } from "../src/lib/report-document.js"; import { DEFAULT_CONFIG } from "../src/lib/types.js"; import { createFakeIntegration } from "./helpers/fake-integration.js"; import { @@ -30,6 +32,7 @@ async function buildDialogOutput(params: { client: ReturnType; sessionID: string; arguments?: string; + generatedAtMs?: number; }) { const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); // V2 CLI obtains host provider IDs from its location cache, not V1 plugin bootstrap. @@ -41,6 +44,7 @@ async function buildDialogOutput(params: { const result = await buildQuotaDialogCommandOutput({ command: params.command ?? "quota", arguments: params.arguments, + generatedAtMs: params.generatedAtMs, client: params.client, roots: { workspaceRoot: process.cwd(), @@ -58,7 +62,10 @@ async function buildDialogOutput(params: { }); expect(params.client.session.prompt).not.toHaveBeenCalled(); expect(result.state).toBe("output"); - return result.state === "output" ? result.output : ""; + if (result.state !== "output") return ""; + // Every command here renders its document as plain text. + expect(renderPlainTextReport(result.document)).toBe(result.output); + return result.output; } async function createV2StatusTool(directory: string) { @@ -871,10 +878,12 @@ describe("/quota command behavior", () => { const client = createClient(); + const generatedAtMs = Date.UTC(2026, 0, 2, 3, 4); const injected = await buildDialogOutput({ command: "pricing_refresh", client, sessionID: "session-pricing-refresh", + generatedAtMs, }); expect(mocks.maybeRefreshPricingSnapshot).toHaveBeenCalledWith({ @@ -883,10 +892,56 @@ describe("/quota command behavior", () => { snapshotSelection: "bundled", allowRefreshWhenSelectionBundled: true, }); - expect(injected).toContain("Pricing Refresh (/pricing_refresh)"); - expect(injected).toContain("- selection: configured=bundled active=bundled"); - expect(injected).toContain( - "runtime snapshot refreshed locally, but active reports remain pinned to bundled pricing", + expect(injected).toBe( + [ + `# Pricing Refresh (/pricing_refresh) ${formatLocalCallTimestamp(generatedAtMs)}`, + "", + "refresh:", + "- attempted: true", + "- result: success", + "- runtime_snapshot_persisted: true", + "", + "pricing_snapshot:", + "- selection: configured=bundled active=bundled", + "- active_snapshot: source=https://models.dev/api.json generated_at=2026-01-01T00:00:00.000Z units=USD per 1M tokens", + "- runtime_paths: snapshot=/tmp/modelsdev-pricing.runtime.min.json refresh_state=/tmp/modelsdev-pricing.refresh-state.json", + "- selection_note: runtime snapshot refreshed locally, but active reports remain pinned to bundled pricing", + ].join("\n"), + ); + }); + + it("reports a failed /pricing_refresh with its error", async () => { + mocks.maybeRefreshPricingSnapshot.mockResolvedValue({ + attempted: true, + updated: false, + reason: "fetch_failed", + error: "network down", + state: { version: 1, updatedAt: Date.now() }, + }); + const generatedAtMs = Date.UTC(2026, 0, 2, 3, 4); + + const injected = await buildDialogOutput({ + command: "pricing_refresh", + client: createClient(), + sessionID: "session-pricing-refresh-failed", + generatedAtMs, + }); + + expect(injected).toBe( + [ + `# Pricing Refresh (/pricing_refresh) ${formatLocalCallTimestamp(generatedAtMs)}`, + "", + "refresh:", + "- attempted: true", + "- result: fetch_failed", + "- runtime_snapshot_persisted: false", + "- error: network down", + "", + "pricing_snapshot:", + "- selection: configured=auto active=runtime", + "- active_snapshot: source=https://models.dev/api.json generated_at=2026-01-01T00:00:00.000Z units=USD per 1M tokens", + "- runtime_paths: snapshot=/tmp/modelsdev-pricing.runtime.min.json refresh_state=/tmp/modelsdev-pricing.refresh-state.json", + ].join("\n"), ); }); diff --git a/tests/plugin.quota-status-command.test.ts b/tests/plugin.quota-status-command.test.ts index 36bd906e..03bcd081 100644 --- a/tests/plugin.quota-status-command.test.ts +++ b/tests/plugin.quota-status-command.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { messageDocument, renderPlainTextReport } from "../src/lib/report-document.js"; import { DEFAULT_CONFIG } from "../src/lib/types.js"; import { createAlibabaAuthModuleMock, @@ -24,7 +25,7 @@ const mocks = vi.hoisted(() => ({ resolveAlibabaCodingPlanAuthCached: vi.fn(), fetchSessionTokensForDisplay: vi.fn(), collectQuotaStatusLiveProbes: vi.fn(), - buildQuotaStatusReport: vi.fn(), + buildQuotaStatusReportDocument: vi.fn(), })); vi.mock("../src/lib/config.js", () => createConfigModuleMock(mocks.loadConfig)); @@ -52,7 +53,7 @@ vi.mock("../src/lib/quota-render-data.js", () => ({ })); vi.mock("../src/lib/quota-status.js", () => ({ - buildQuotaStatusReport: mocks.buildQuotaStatusReport, + buildQuotaStatusReportDocument: mocks.buildQuotaStatusReportDocument, })); async function buildQuotaStatusDialogOutput(params: { @@ -79,7 +80,9 @@ async function buildQuotaStatusDialogOutput(params: { }); expect(params.client.session.prompt).not.toHaveBeenCalled(); expect(result.state).toBe("output"); - return result.state === "output" ? result.output : ""; + if (result.state !== "output") return ""; + expect(renderPlainTextReport(result.document)).toBe(result.output); + return result.output; } describe("/quota_status command behavior", () => { @@ -122,7 +125,9 @@ describe("/quota_status command behavior", () => { }, }, ]); - mocks.buildQuotaStatusReport.mockResolvedValue("Injected quota status"); + mocks.buildQuotaStatusReportDocument.mockResolvedValue( + messageDocument("Injected quota status"), + ); }); afterEach(() => { @@ -196,6 +201,7 @@ describe("/quota_status command behavior", () => { }); expect(data.output).toBe("Injected quota status"); + expect(data.document).toEqual(messageDocument("Injected quota status")); expect(data.payload?.providers).toEqual([ expect.objectContaining({ id: "synthetic", enabled: true, available: true }), ]); @@ -206,7 +212,7 @@ describe("/quota_status command behavior", () => { expect(mocks.collectQuotaStatusLiveProbes).toHaveBeenCalledWith( expect.objectContaining({ providers: [synthetic] }), ); - expect(mocks.buildQuotaStatusReport).toHaveBeenCalledWith( + expect(mocks.buildQuotaStatusReportDocument).toHaveBeenCalledWith( expect.objectContaining({ providerAvailability: [ expect.objectContaining({ id: "synthetic", enabled: true, available: true }), @@ -261,7 +267,7 @@ describe("/quota_status command behavior", () => { providers: [openai, synthetic, copilot, cursor], }), ); - expect(mocks.buildQuotaStatusReport).toHaveBeenCalledWith( + expect(mocks.buildQuotaStatusReportDocument).toHaveBeenCalledWith( expect.objectContaining({ runtimeRoots: { workspaceRoot: process.cwd(), @@ -309,7 +315,7 @@ describe("/quota_status command behavior", () => { }); expect(client.session.get).not.toHaveBeenCalled(); - expect(mocks.buildQuotaStatusReport).toHaveBeenCalledWith( + expect(mocks.buildQuotaStatusReportDocument).toHaveBeenCalledWith( expect.objectContaining({ currentModel: undefined, sessionModelLookup: "no_session", diff --git a/tests/plugin.tokens-session-all.test.ts b/tests/plugin.tokens-session-all.test.ts index c48fcfdc..f1a74c01 100644 --- a/tests/plugin.tokens-session-all.test.ts +++ b/tests/plugin.tokens-session-all.test.ts @@ -1,5 +1,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; +import { + type ReportDocument, + renderMarkdownReport, + renderPlainTextReport, +} from "../src/lib/report-document.js"; import { createAlibabaAuthModuleMock, createPluginTestClient as createClient, @@ -24,7 +30,7 @@ const mocks = vi.hoisted(() => ({ resolveAlibabaCodingPlanAuthCached: vi.fn(), aggregateUsage: vi.fn(), resolveSessionTree: vi.fn(), - formatQuotaStatsReport: vi.fn(), + buildQuotaStatsReportDocument: vi.fn(), SessionNotFoundError: class SessionNotFoundError extends Error { sessionID: string; checkedPath: string; @@ -61,13 +67,16 @@ vi.mock("../src/lib/quota-stats.js", () => ({ })); vi.mock("../src/lib/quota-stats-format.js", () => ({ - formatQuotaStatsReport: mocks.formatQuotaStatsReport, + buildQuotaStatsReportDocument: mocks.buildQuotaStatsReportDocument, })); async function buildTokenDialogOutput(params: { command: "tokens_session" | "tokens_session_all"; client: ReturnType; sessionID: string; + generatedAtMs?: number; + /** The renderer the command uses for its text; the text must equal it applied to the document. */ + render: (document: ReportDocument) => string; }) { const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); const result = await buildQuotaDialogCommandOutput({ @@ -79,12 +88,27 @@ async function buildTokenDialogOutput(params: { fallbackDirectory: process.cwd(), }, sessionID: params.sessionID, + generatedAtMs: params.generatedAtMs, }); expect(params.client.session.prompt).not.toHaveBeenCalled(); expect(result.state).toBe("output"); - return result.state === "output" ? result.output : ""; + if (result.state !== "output") return ""; + expect(params.render(result.document)).toBe(result.output); + return result.output; } +// A titled section, so the markdown and plain-text renderers give different text. +const TOKEN_REPORT_DOCUMENT: ReportDocument = { + heading: { title: "Tokens used", generatedAtMs: 0 }, + sections: [ + { + id: "summary", + title: "Summary", + blocks: [{ kind: "lines", lines: ["formatted token report"] }], + }, + ], +}; + describe("/tokens_session_all command", () => { beforeEach(() => { seedDefaultPluginBootstrapMocks(mocks, { @@ -99,7 +123,7 @@ describe("/tokens_session_all command", () => { }); mocks.resolveAlibabaCodingPlanAuthCached.mockResolvedValue({ state: "none" }); mocks.aggregateUsage.mockResolvedValue({ totals: {}, bySession: [] }); - mocks.formatQuotaStatsReport.mockReturnValue("formatted token report"); + mocks.buildQuotaStatsReportDocument.mockReturnValue(TOKEN_REPORT_DOCUMENT); mocks.resolveSessionTree.mockResolvedValue([ { sessionID: "ses_parent", title: "Parent Session", depth: 0 }, { @@ -150,6 +174,7 @@ describe("/tokens_session_all command", () => { command: "tokens_session_all", client, sessionID: "ses_parent", + render: renderMarkdownReport, }); expect(mocks.resolveSessionTree).toHaveBeenCalledWith("ses_parent"); @@ -159,7 +184,7 @@ describe("/tokens_session_all command", () => { sessionID: undefined, sessionIDs: ["ses_parent", "ses_child"], }); - expect(mocks.formatQuotaStatsReport).toHaveBeenCalledWith( + expect(mocks.buildQuotaStatsReportDocument).toHaveBeenCalledWith( expect.objectContaining({ title: "Tokens used (Current Session Tree) (/tokens_session_all)", focusSessionID: "ses_parent", @@ -182,7 +207,8 @@ describe("/tokens_session_all command", () => { }, }), ); - expect(output).toContain("formatted token report"); + expect(output).toBe(renderMarkdownReport(TOKEN_REPORT_DOCUMENT)); + expect(output).toContain("## Summary"); }); it("keeps /tokens_session scoped to the selected session only", async () => { @@ -192,6 +218,7 @@ describe("/tokens_session_all command", () => { command: "tokens_session", client, sessionID: "ses_parent", + render: renderMarkdownReport, }); expect(mocks.resolveSessionTree).not.toHaveBeenCalled(); @@ -201,7 +228,7 @@ describe("/tokens_session_all command", () => { sessionID: "ses_parent", sessionIDs: undefined, }); - expect(mocks.formatQuotaStatsReport).toHaveBeenCalledWith( + expect(mocks.buildQuotaStatsReportDocument).toHaveBeenCalledWith( expect.objectContaining({ title: "Tokens used (Current Session) (/tokens_session)", focusSessionID: "ses_parent", @@ -222,15 +249,24 @@ describe("/tokens_session_all command", () => { const client = createClient(); + const generatedAtMs = Date.UTC(2026, 0, 2, 3, 4); const injected = await buildTokenDialogOutput({ command: "tokens_session_all", client, sessionID: "ses_missing", + generatedAtMs, + render: renderPlainTextReport, }); - expect(injected).toContain("Token report unavailable (/tokens_session_all)"); - expect(injected).toContain("session_lookup_error:"); - expect(injected).toContain("- session_id: ses_missing"); - expect(injected).toContain("- checked_path: /tmp/opencode.db"); + expect(injected).toBe( + [ + `# Token report unavailable (/tokens_session_all) ${formatLocalCallTimestamp(generatedAtMs)}`, + "", + "session_lookup_error:", + "- session_id: ses_missing", + "- error: Session not found: ses_missing", + "- checked_path: /tmp/opencode.db", + ].join("\n"), + ); }); it("returns a dialog session lookup error for /tokens_session", async () => { @@ -244,6 +280,7 @@ describe("/tokens_session_all command", () => { command: "tokens_session", client, sessionID: "ses_parent", + render: renderPlainTextReport, }); expect(injected).toContain("Token report unavailable (/tokens_session)"); expect(injected).toContain("- session_id: ses_parent"); diff --git a/tests/report-document.test.ts b/tests/report-document.test.ts index 95893526..00c4a0f8 100644 --- a/tests/report-document.test.ts +++ b/tests/report-document.test.ts @@ -1,6 +1,14 @@ import { describe, expect, it } from "vitest"; -import { renderMarkdownReport, renderPlainTextReport } from "../src/lib/report-document.js"; +import { padTableColumns } from "../src/lib/markdown-table.js"; +import { + isReportDocument, + messageDocument, + type ReportDocument, + renderableSections, + renderMarkdownReport, + renderPlainTextReport, +} from "../src/lib/report-document.js"; describe("report-document", () => { it("renders stable plain-text section spacing across lines and kv blocks", () => { @@ -90,4 +98,116 @@ describe("report-document", () => { Follow up note." `); }); + + it("pads table columns to their widths without pipes or escaping", () => { + expect( + padTableColumns({ + headers: ["Model", "Cost"], + rows: [["gpt|5", "$1.23"], ["claude-opus", "$10.00"], ["two\nlines"]], + aligns: ["left", "right"], + }), + ).toEqual({ + header: ["Model ", " Cost"], + rows: [ + ["gpt|5 ", " $1.23"], + ["claude-opus", "$10.00"], + ["two lines ", " "], + ], + }); + }); + + it("keeps only sections with a title or a block that has content", () => { + const document: ReportDocument = { + sections: [ + { id: "empty", blocks: [{ kind: "lines", lines: [] }] }, + { id: "titled", title: "Title", blocks: [{ kind: "kv", rows: [] }] }, + { + id: "mixed", + blocks: [ + { kind: "table", headers: [], rows: [], aligns: [] }, + { kind: "lines", lines: ["kept"] }, + ], + }, + ], + }; + + expect(renderableSections(document)).toEqual([ + { id: "titled", title: "Title", blocks: [] }, + { id: "mixed", blocks: [{ kind: "lines", lines: ["kept"] }] }, + ]); + }); + + it("renders a message document back to the same text with either renderer", () => { + const text = "Invalid arguments for /quota\n\nThis command does not accept arguments."; + + expect(messageDocument(text)).toEqual({ + sections: [ + { + id: "message", + blocks: [ + { + kind: "lines", + lines: [ + "Invalid arguments for /quota", + "", + "This command does not accept arguments.", + ], + }, + ], + }, + ], + }); + expect(renderPlainTextReport(messageDocument(text))).toBe(text); + expect(renderMarkdownReport(messageDocument(text))).toBe(text); + }); + + it("accepts well-formed documents and rejects malformed ones", () => { + const document: ReportDocument = { + heading: { title: "Report", generatedAtMs: 1 }, + sections: [ + { + id: "all", + title: "all:", + blocks: [ + { kind: "lines", lines: ["a"] }, + { kind: "kv", rows: [{ key: "k", value: "v", indent: 1, trailingColon: false }] }, + { + kind: "table", + headers: ["A"], + rows: [["1"]], + aligns: ["right"], + widthMode: "markdown-conceal", + }, + ], + }, + ], + }; + + expect(isReportDocument(document)).toBe(true); + expect(isReportDocument(messageDocument("hi"))).toBe(true); + expect(isReportDocument(JSON.parse(JSON.stringify(document)))).toBe(true); + for (const value of [ + undefined, + null, + "report", + [], + {}, + { sections: {} }, + { heading: { title: 1 }, sections: [] }, + { sections: [{ blocks: [] }] }, + { sections: [{ id: "s", blocks: [{ kind: "html", lines: [] }] }] }, + { sections: [{ id: "s", blocks: [{ kind: "lines", lines: [1] }] }] }, + { sections: [{ id: "s", blocks: [{ kind: "kv", rows: [{ key: "k", indent: 2 }] }] }] }, + { + sections: [ + { + id: "s", + blocks: [{ kind: "table", headers: ["A"], rows: [["1"]], aligns: ["center"] }], + }, + ], + }, + ]) { + expect(isReportDocument(value)).toBe(false); + } + }); }); diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 6e537ecb..c6235d66 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -1,6 +1,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { TextAttributes } from "@opentui/core"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@opentui/solid", () => ({ @@ -13,8 +14,10 @@ vi.mock("../src/lib/config.js", async (importOriginal) => { return { ...original, loadConfig }; }); +import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; import { parseQuotaSlashCommand } from "../src/lib/quota-dialog-command-specs.js"; import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; +import { messageDocument, type ReportDocument } from "../src/lib/report-document.js"; import plugin from "../src/tui-v2.tsx"; // The server plugin's quota RPC runs palette commands; the TUI shows their output. @@ -22,6 +25,14 @@ const rpc = vi.hoisted(() => ({ command: vi.fn() })); type Node = { type: string; props: Record }; +function findNodes(node: unknown, type: string): Node[] { + if (Array.isArray(node)) return node.flatMap((child) => findNodes(child, type)); + if (!node || typeof node !== "object") return []; + const element = node as Node; + if (element.type === type) return [element]; + return findNodes(element.props?.children, type); +} + function findNode(node: unknown, type: string): Node | undefined { if (Array.isArray(node)) { for (const child of node) { @@ -68,6 +79,7 @@ function startTui( surface: vi.fn(() => ({ text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, background: { action: { primary: { focused: "action-bg" } } }, + markdown: { heading: "heading" }, })), }, data: { @@ -118,6 +130,7 @@ describe("V2 quota TUI commands", () => { command: "tokens_today", title: "Tokens", output: "ok", + document: messageDocument("ok"), dialogSize: "large", }); }); @@ -207,7 +220,10 @@ describe("V2 quota TUI commands", () => { }); describe("quota reports posted by slash commands", () => { - const metadata = { opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1 } }; + const document = messageDocument("openai 42%"); + const metadata = { + opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1, document }, + }; const report = formatQuotaReportMessage("openai 42%"); const item = (text: string, meta?: Record, type = "user") => ({ type, @@ -271,6 +287,12 @@ describe("V2 quota TUI commands", () => { inboxID: "msg_5", item: item(report, metadata), }); + // Metadata without a report document, as posted before documents were added. + emit("session.inbox.enqueued", { + sessionID: "ses_open", + inboxID: "msg_6", + item: item(report, { opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1 } }), + }); await settle(); // Only the read at start, for typed commands. @@ -301,7 +323,7 @@ describe("V2 quota TUI commands", () => { const render = context.ui.dialog.show.mock.calls[0][0] as () => { props: Record; }; - expect(render().props).toMatchObject({ title: "OpenCode Quota", message: "openai 42%" }); + expect(render().props).toMatchObject({ title: "OpenCode Quota", document }); expect(rpc.command).not.toHaveBeenCalled(); }); @@ -511,7 +533,14 @@ describe("V2 quota TUI commands", () => { type: "user", payload: { text: formatQuotaReportMessage("openai 42%"), - metadata: { opencodeQuota: { command: "quota", title: "OpenCode Quota", at: 1 } }, + metadata: { + opencodeQuota: { + command: "quota", + title: "OpenCode Quota", + at: 1, + document: messageDocument("openai 42%"), + }, + }, }, }, }); @@ -576,12 +605,37 @@ describe("V2 quota TUI commands", () => { return typeof type === "function" ? type(all) : { type, props: all }; }, }); - const output = Array.from({ length: 80 }, (_, index) => `line ${index + 1}`).join("\n"); + const lines = Array.from({ length: 80 }, (_, index) => `line ${index + 1}`); + const generatedAtMs = Date.UTC(2026, 0, 2, 3, 4); + const document: ReportDocument = { + heading: { title: "Tokens used (Today) (/tokens_today)", generatedAtMs }, + sections: [ + { + id: "models", + title: "Top Models", + blocks: [ + { + kind: "table", + headers: ["Model", "Cost"], + aligns: ["left", "right"], + rows: [ + ["gpt-5", "$1.23"], + ["claude-\u001b[31mopus", "$10.00"], + ], + }, + { kind: "kv", rows: [{ key: "enabled", value: "true" }] }, + ], + }, + { id: "empty", blocks: [{ kind: "lines", lines: [] }] }, + { id: "notes", blocks: [{ kind: "lines", lines }] }, + ], + }; rpc.command.mockResolvedValue({ state: "output", command: "quota_status", title: "Quota Status", - output, + output: "the chat text", + document, dialogSize: "xlarge", }); const { context, command, layers } = startTui(); @@ -595,9 +649,33 @@ describe("V2 quota TUI commands", () => { const scrollbox = findNode(tree, "scrollbox"); // 40 terminal rows: three quarters is 30, minus 8 rows of dialog chrome. expect(scrollbox?.props.maxHeight).toBe(22); - const text = findNode(scrollbox?.props.children, "text"); - expect(text?.props.children).toBe(output); - expect(text?.props.fg).toBe("base"); + const texts = findNodes(scrollbox?.props.children, "text").map((node) => ({ + children: node.props.children, + fg: node.props.fg, + bold: node.props.attributes === TextAttributes.BOLD, + wrapMode: node.props.wrapMode, + })); + expect(texts).toEqual([ + { + children: `Tokens used (Today) (/tokens_today) ${formatLocalCallTimestamp(generatedAtMs)}`, + fg: "base", + bold: true, + wrapMode: undefined, + }, + { children: "Top Models", fg: "base", bold: true, wrapMode: undefined }, + { children: "Model Cost", fg: "heading", bold: true, wrapMode: "none" }, + { children: "gpt-5 $1.23", fg: "base", bold: false, wrapMode: "none" }, + { children: "claude-opus $10.00", fg: "base", bold: false, wrapMode: "none" }, + { children: "- enabled: true", fg: "base", bold: false, wrapMode: undefined }, + { children: lines.join("\n"), fg: "base", bold: false, wrapMode: undefined }, + ]); + // One blank row between the heading, the sections, and the blocks of a section; a + // section title sits directly above its first block. + const body = scrollbox?.props.children; + expect(body.props.gap).toBe(1); + const section = body.props.children[1][0]; + expect(section.props.gap).toBeUndefined(); + expect(section.props.children[1].props.gap).toBe(1); const dialogLayer = layers().find((item) => item.mode === "modal")!; expect(dialogLayer.mode).toBe("modal"); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 465c4b24..cec6a4ac 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -195,7 +195,8 @@ async function setupV2Surfaces(client: ReturnType, provider // The Enter binding that runs a quota command typed in the TUI prompt. let enter: (() => unknown) | undefined; const editor = { plainText: "", clear: vi.fn() }; - // Records the title and scrollbox text of each quota output dialog the TUI shows. + // Records the title and scrollbox text of each quota output dialog the TUI shows. The + // scrollbox text is every text node in order, one per line, without the blank rows. const dialog = vi.fn((_input: { title: string; message: string }) => {}); type Node = { type: string; props: Record }; const find = (node: unknown, type: string): Node | undefined => { @@ -205,10 +206,17 @@ async function setupV2Surfaces(client: ReturnType, provider ? (node as Node) : find((node as Node).props?.children, type); }; + const texts = (node: unknown): string[] => { + if (Array.isArray(node)) return node.flatMap(texts); + if (!node || typeof node !== "object") return []; + return (node as Node).type === "text" + ? [(node as Node).props.children] + : texts((node as Node).props?.children); + }; const show = (render: () => unknown, onClose?: () => void) => { const tree = render(); const title = find(tree, "text")?.props.children; - const message = find(find(tree, "scrollbox"), "text")?.props.children; + const message = texts(find(tree, "scrollbox")).join("\n"); dialog({ title, message }); onClose?.(); }; @@ -235,6 +243,7 @@ async function setupV2Surfaces(client: ReturnType, provider surface: () => ({ text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, background: { action: { primary: { focused: "action-bg" } } }, + markdown: { heading: "heading" }, }), }, data: { From e3d32ad827aa8b87d70a84f6803cf21ad5153c09 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:15:17 +0200 Subject: [PATCH 87/97] fix(tui): don't repeat the report title in quota popups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every quota popup showed its title twice: the dialog title, then the report's own title line, e.g. "Tokens used (Last 7 Days)" and then "Tokens used (Last 7 Days) (/tokens_weekly) 16:00 29/09/2026". A report heading now has two fields: `line`, the first line of the text report, and `subtitle`, what the dialog shows in one muted line under its title instead: the facts in the line that the dialog title lacks, and the time. For example "opencode-quota v5.0.0 · 16:00 29/09/2026" for /quota_status, the date range for /tokens_between, "Percent used" for /quota with bare percent labels, or just the time. The dialog no longer draws the heading line, so the command echo is gone from the popup too. /quota and the announcements list now keep their first line in the heading instead of in a section. Chat, Web, and CLI text is unchanged: it still starts with `line`. --- src/lib/format-utils.ts | 4 - src/lib/quota-command-format.ts | 28 +-- src/lib/quota-dialog-commands.ts | 21 +- src/lib/quota-stats-format.ts | 8 +- src/lib/quota-status.ts | 6 +- src/lib/report-document.ts | 49 ++-- src/tui-v2.tsx | 40 ++-- tests/lib.quota-status.test.ts | 22 +- tests/plugin.maintainer-announcements.test.ts | 5 + tests/plugin.quota-command.test.ts | 6 + tests/plugin.tokens-session-all.test.ts | 47 +++- tests/quota-command-format.test.ts | 39 +++- tests/quota-stats-format.test.ts | 36 ++- tests/report-document.test.ts | 43 +++- tests/tui-v2-commands.test.ts | 214 ++++++++++++++++-- tests/v4-phase5-cross-surface.test.ts | 23 +- 16 files changed, 472 insertions(+), 119 deletions(-) diff --git a/src/lib/format-utils.ts b/src/lib/format-utils.ts index a267ed25..adf891cb 100644 --- a/src/lib/format-utils.ts +++ b/src/lib/format-utils.ts @@ -155,10 +155,6 @@ export function formatLocalCallTimestamp(atMs?: number): string { return `${pad2(d.getHours())}:${pad2(d.getMinutes())} ${pad2(d.getDate())}/${pad2(d.getMonth() + 1)}/${d.getFullYear()}`; } -export function renderCommandHeading(params: { title: string; generatedAtMs?: number }): string { - return `# ${params.title} ${formatLocalCallTimestamp(params.generatedAtMs)}`; -} - export function abbreviateDisplayedModelName(name: string): string { return name.replace(/antigravity/gi, "agy"); } diff --git a/src/lib/quota-command-format.ts b/src/lib/quota-command-format.ts index 58f85353..00200201 100644 --- a/src/lib/quota-command-format.ts +++ b/src/lib/quota-command-format.ts @@ -224,25 +224,17 @@ export function buildQuotaCommandDocument(params: { }); } + // With bare percent labels, only the heading tells whether the percentages are used or left. + const bare = params.percentLabelStyle === "bare"; + const time = formatLocalCallTimestamp(params.generatedAtMs); return { - sections: [ - { - id: "heading", - blocks: [ - { - kind: "lines", - lines: [ - `${ - params.percentLabelStyle === "bare" - ? formatQuotaModeHeading(params.percentDisplayMode) - : "Quota" - } (/quota) ${formatLocalCallTimestamp(params.generatedAtMs)}`, - ], - }, - ], - }, - ...sections, - ], + heading: { + line: `${bare ? formatQuotaModeHeading(params.percentDisplayMode) : "Quota"} (/quota) ${time}`, + subtitle: bare + ? `${params.percentDisplayMode === "used" ? "Percent used" : "Percent remaining"} · ${time}` + : time, + }, + sections, }; } diff --git a/src/lib/quota-dialog-commands.ts b/src/lib/quota-dialog-commands.ts index af6f9d17..b4e0e356 100644 --- a/src/lib/quota-dialog-commands.ts +++ b/src/lib/quota-dialog-commands.ts @@ -1,4 +1,5 @@ import { + formatYmd, parseQuotaBetweenArgs, startOfLocalDayMs, startOfNextLocalDayMs, @@ -54,6 +55,7 @@ import { import { buildQuotaStatsReportDocument } from "./quota-stats-format.js"; import { buildQuotaStatusReportDocument, type SessionTokenError } from "./quota-status.js"; import { + commandHeading, messageDocument, type ReportDocument, type ReportKvRow, @@ -197,6 +199,7 @@ async function kickPricingRefresh(params: { async function buildQuotaReport(params: { title: string; + titleDetail?: string; sinceMs?: number; untilMs?: number; sessionID: string; @@ -220,6 +223,7 @@ async function buildQuotaReport(params: { }); const document = buildQuotaStatsReportDocument({ title: params.title, + titleDetail: params.titleDetail, result, topModels: params.topModels, topSessions: params.topSessions, @@ -506,7 +510,10 @@ function buildPricingRefreshCommandReport(params: { } return plainTextReport({ - heading: { title: "Pricing Refresh (/pricing_refresh)", generatedAtMs: params.generatedAtMs }, + heading: commandHeading({ + title: "Pricing Refresh (/pricing_refresh)", + generatedAtMs: params.generatedAtMs, + }), sections: [ { id: "refresh", title: "refresh:", blocks: [{ kind: "kv", rows: refreshRows }] }, { @@ -524,10 +531,11 @@ function buildTokenReportUnavailableReport(params: { error: SessionNotFoundError; }): CommandReport { return plainTextReport({ - heading: { + heading: commandHeading({ title: `Token report unavailable (${params.command})`, + detail: "Token report unavailable", generatedAtMs: params.generatedAtMs, - }, + }), sections: [ { id: "session_lookup_error", @@ -584,10 +592,8 @@ async function buildQuotaAnnouncementsCommandReport( } return plainTextReport({ - sections: [ - { id: "title", blocks: [{ kind: "lines", lines: ["Maintainer announcements"] }] }, - { id: "announcements", blocks: [{ kind: "lines", lines }] }, - ], + heading: { line: "Maintainer announcements" }, + sections: [{ id: "announcements", blocks: [{ kind: "lines", lines }] }], }); } @@ -647,6 +653,7 @@ async function buildTokenReportCommandReport(params: { const rangeUntilMs = startOfNextLocalDayMs(parsed.endYmd); return await buildQuotaReport({ title: spec.titleForRange(parsed.startYmd, parsed.endYmd), + titleDetail: `${formatYmd(parsed.startYmd)} .. ${formatYmd(parsed.endYmd)}`, sinceMs, untilMs: rangeUntilMs, sessionID: sessionID ?? "", diff --git a/src/lib/quota-stats-format.ts b/src/lib/quota-stats-format.ts index ddaa0993..9abcd0df 100644 --- a/src/lib/quota-stats-format.ts +++ b/src/lib/quota-stats-format.ts @@ -2,6 +2,7 @@ import { abbreviateDisplayedModelName } from "./format-utils.js"; import type { WidthMode } from "./markdown-table.js"; import type { AggregateResult, SessionTreeNode, TokenBuckets } from "./quota-stats.js"; import { + commandHeading, type ReportDocument, type ReportSection, renderMarkdownReport, @@ -156,6 +157,8 @@ function truncateTitle(title: string | undefined): string { type QuotaStatsReportParams = { title: string; + /** Facts in the title that the TUI dialog title lacks, like the date range; see commandHeading. */ + titleDetail?: string; result: AggregateResult; topModels?: number; topSessions?: number; @@ -495,10 +498,11 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R } return { - heading: { + heading: commandHeading({ title: params.title, + detail: params.titleDetail, generatedAtMs: params.generatedAtMs, - }, + }), sections, }; } diff --git a/src/lib/quota-status.ts b/src/lib/quota-status.ts index fd856427..62525a1c 100644 --- a/src/lib/quota-status.ts +++ b/src/lib/quota-status.ts @@ -40,6 +40,7 @@ import type { QuotaProviderDefinition } from "./quota-providers.js"; import { isMaintainedQuotaProviderTuning } from "./quota-providers.js"; import { aggregateUsage } from "./quota-stats.js"; import { + commandHeading, type ReportDocument, type ReportKvRow, type ReportSection, @@ -1128,10 +1129,11 @@ export async function buildQuotaStatusReportDocument( sections.push(createKvSection("unknown_pricing", "unknown_pricing:", unknownRows)); return { - heading: { + heading: commandHeading({ title: `Quota Status (opencode-quota v${v}) (/quota_status)`, + detail: `opencode-quota v${v}`, generatedAtMs: params.generatedAtMs, - }, + }), sections, }; } diff --git a/src/lib/report-document.ts b/src/lib/report-document.ts index 4425a4f0..5b6f6917 100644 --- a/src/lib/report-document.ts +++ b/src/lib/report-document.ts @@ -1,9 +1,13 @@ -import { renderCommandHeading } from "./format-utils.js"; +import { formatLocalCallTimestamp } from "./format-utils.js"; import { renderMarkdownTable, type WidthMode } from "./markdown-table.js"; +/** + * A report's first line. The text shows `line`. The TUI dialog has its own title, so it + * shows `subtitle` instead: the facts in the line that its title lacks, and the time. + */ export type ReportHeading = { - title: string; - generatedAtMs?: number; + line: string; + subtitle?: string; }; export type ReportKvRow = { @@ -53,6 +57,23 @@ export function renderableSections(document: ReportDocument): ReportSection[] { .filter((section) => section.title || section.blocks.length > 0); } +/** + * A command report's heading: "# <time>" in text, "<detail> · <time>" in the dialog. + * The title echoes the command, e.g. "Quota Status (opencode-quota v5.0.0) (/quota_status)"; + * the detail repeats the facts in it that the dialog title lacks, e.g. "opencode-quota v5.0.0". + */ +export function commandHeading(params: { + title: string; + detail?: string; + generatedAtMs?: number; +}): ReportHeading { + const time = formatLocalCallTimestamp(params.generatedAtMs); + return { + line: `# ${params.title} ${time}`, + subtitle: params.detail ? `${params.detail} · ${time}` : time, + }; +} + /** A document of one plain message; it renders back to the same text. */ export function messageDocument(text: string): ReportDocument { return { sections: [{ id: "message", blocks: [{ kind: "lines", lines: text.split("\n") }] }] }; @@ -111,8 +132,8 @@ export function isReportDocument(value: unknown): value is ReportDocument { heading !== undefined && !( isObject(heading) && - typeof heading.title === "string" && - isOptional(heading.generatedAtMs, "number") + typeof heading.line === "string" && + isOptional(heading.subtitle, "string") ) ) { return false; @@ -177,14 +198,7 @@ function renderMarkdownBlock(block: ReportBlock): string[] { export function renderPlainTextReport(document: ReportDocument): string { const lines: string[] = []; - if (document.heading) { - lines.push( - renderCommandHeading({ - title: document.heading.title, - generatedAtMs: document.heading.generatedAtMs, - }), - ); - } + if (document.heading) lines.push(document.heading.line); for (const section of renderableSections(document)) { if (lines.length > 0) lines.push(""); @@ -205,14 +219,7 @@ export function renderPlainTextReport(document: ReportDocument): string { export function renderMarkdownReport(document: ReportDocument): string { const lines: string[] = []; - if (document.heading) { - lines.push( - renderCommandHeading({ - title: document.heading.title, - generatedAtMs: document.heading.generatedAtMs, - }), - ); - } + if (document.heading) lines.push(document.heading.line); for (const section of renderableSections(document)) { if (lines.length > 0) lines.push(""); diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 30e33277..f6c61951 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -8,7 +8,6 @@ import { createSignal, onCleanup, Show } from "solid-js"; import { loadConfig } from "./lib/config.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; -import { formatLocalCallTimestamp } from "./lib/format-utils.js"; import { padTableColumns } from "./lib/markdown-table.js"; import { parseQuotaSlashCommand, @@ -331,21 +330,14 @@ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX } /** - * Draws a report document with the same spacing as its plain text: one blank row between - * the heading, the sections, and the blocks of a section, and a section title directly - * above its first block. The heading and section titles are bold. + * Draws a report document's sections with the same spacing as its plain text: one blank row + * between the sections and between the blocks of a section, and a section title directly + * above its first block. Section titles are bold. The dialog shows the heading's subtitle + * under its own title, so the heading line is left out. */ function ReportDocumentView(props: { document: ReportDocument; theme: DialogTheme }): JSX.Element { - const heading = props.document.heading; return ( <box flexDirection="column" gap={1}> - {heading ? ( - <text attributes={TextAttributes.BOLD} fg={props.theme.text.base}> - {sanitizeDisplayText( - `${heading.title} ${formatLocalCallTimestamp(heading.generatedAtMs)}`, - )} - </text> - ) : null} {renderableSections(props.document).map((section) => ( <box flexDirection="column"> {section.title ? ( @@ -377,9 +369,12 @@ function QuotaOutputDialog(props: { }): JSX.Element { const theme = () => props.context.theme.surface("dialog"); const dimensions = useTerminalDimensions(); + const subtitle = props.document.heading?.subtitle; // The host dialog starts a quarter of the way down the terminal. The remaining - // 8 rows cover the title, ok button, paddings, gaps, and one spare row. - const maxHeight = () => Math.max(1, Math.floor(dimensions().height * 0.75) - 8); + // 8 rows cover the title, ok button, paddings, gaps, and one spare row; the subtitle + // takes one more. + const maxHeight = () => + Math.max(1, Math.floor(dimensions().height * 0.75) - 8 - (subtitle ? 1 : 0)); let scroll: ScrollBoxRenderable | undefined; const close = () => props.context.ui.dialog.clear(); @@ -413,13 +408,16 @@ function QuotaOutputDialog(props: { return ( <box paddingLeft={2} paddingRight={2} gap={1}> - <box flexDirection="row" justifyContent="space-between"> - <text attributes={TextAttributes.BOLD} fg={theme().text.base}> - {props.title} - </text> - <text fg={theme().text.muted} onMouseUp={close}> - esc - </text> + <box> + <box flexDirection="row" justifyContent="space-between"> + <text attributes={TextAttributes.BOLD} fg={theme().text.base}> + {props.title} + </text> + <text fg={theme().text.muted} onMouseUp={close}> + esc + </text> + </box> + {subtitle ? <text fg={theme().text.muted}>{sanitizeDisplayText(subtitle)}</text> : null} </box> <box paddingBottom={1}> <scrollbox diff --git a/tests/lib.quota-status.test.ts b/tests/lib.quota-status.test.ts index bbd62f14..08e890e3 100644 --- a/tests/lib.quota-status.test.ts +++ b/tests/lib.quota-status.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - +import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; import { aggregateUsage } from "../src/lib/quota-stats.js"; - import { buildProviderStatusReport, buildQuotaStatusReportForTest, + DEFAULT_QUOTA_STATUS_REPORT_GENERATED_AT_MS, expectReportSection, getReportSection, makeProviderAvailability, @@ -12,6 +12,7 @@ import { makeProviderProbe, makeProviderSafeFailureProbe, makeProviderSuccessProbe, + makeQuotaStatusReportParams, makeStatusDetails, } from "./helpers/quota-status-test-harness.js"; @@ -172,6 +173,23 @@ describe("buildQuotaStatusReport", () => { vi.clearAllMocks(); }); + it("keeps the title line in text and gives the dialog the version and time", async () => { + const { buildQuotaStatusReportDocument } = await import("../src/lib/quota-status.js"); + const time = formatLocalCallTimestamp(DEFAULT_QUOTA_STATUS_REPORT_GENERATED_AT_MS); + + const report = await buildQuotaStatusReportForTest(); + const document = await buildQuotaStatusReportDocument(makeQuotaStatusReportParams()); + + expect(report.split("\n").slice(0, 2)).toEqual([ + `# Quota Status (opencode-quota v1.2.3) (/quota_status) ${time}`, + "", + ]); + expect(document.heading).toEqual({ + line: `# Quota Status (opencode-quota v1.2.3) (/quota_status) ${time}`, + subtitle: `opencode-quota v1.2.3 · ${time}`, + }); + }); + it("uses a Unicode ellipsis for truncated pricing diagnostic lists", async () => { const tokens = { input: 1, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 }; const unpriced = Array.from({ length: 7 }, (_, index) => ({ diff --git a/tests/plugin.maintainer-announcements.test.ts b/tests/plugin.maintainer-announcements.test.ts index 9653929e..1afe8e8c 100644 --- a/tests/plugin.maintainer-announcements.test.ts +++ b/tests/plugin.maintainer-announcements.test.ts @@ -206,6 +206,11 @@ async function buildAnnouncementsDialogOutput(params: { if (result.state !== "output") return ""; // Every command here renders its document as plain text. expect(renderPlainTextReport(result.document)).toBe(result.output); + // The list's heading has no time and the dialog title names it, so the dialog shows no + // subtitle. Invalid arguments give a plain message without a heading. + expect(result.document.heading).toEqual( + params.arguments?.trim() ? undefined : { line: "Maintainer announcements" }, + ); return result.output; } diff --git a/tests/plugin.quota-command.test.ts b/tests/plugin.quota-command.test.ts index de128063..958e8f88 100644 --- a/tests/plugin.quota-command.test.ts +++ b/tests/plugin.quota-command.test.ts @@ -33,6 +33,8 @@ async function buildDialogOutput(params: { sessionID: string; arguments?: string; generatedAtMs?: number; + /** The line the TUI dialog shows under its title, in place of the report's title line. */ + subtitle?: string; }) { const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); // V2 CLI obtains host provider IDs from its location cache, not V1 plugin bootstrap. @@ -65,6 +67,9 @@ async function buildDialogOutput(params: { if (result.state !== "output") return ""; // Every command here renders its document as plain text. expect(renderPlainTextReport(result.document)).toBe(result.output); + if (params.subtitle !== undefined) { + expect(result.document.heading?.subtitle).toBe(params.subtitle); + } return result.output; } @@ -884,6 +889,7 @@ describe("/quota command behavior", () => { client, sessionID: "session-pricing-refresh", generatedAtMs, + subtitle: formatLocalCallTimestamp(generatedAtMs), }); expect(mocks.maybeRefreshPricingSnapshot).toHaveBeenCalledWith({ diff --git a/tests/plugin.tokens-session-all.test.ts b/tests/plugin.tokens-session-all.test.ts index f1a74c01..cd617c5f 100644 --- a/tests/plugin.tokens-session-all.test.ts +++ b/tests/plugin.tokens-session-all.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; +import type { TokenReportCommandId } from "../src/lib/quota-dialog-command-specs.js"; import { type ReportDocument, renderMarkdownReport, @@ -71,7 +72,8 @@ vi.mock("../src/lib/quota-stats-format.js", () => ({ })); async function buildTokenDialogOutput(params: { - command: "tokens_session" | "tokens_session_all"; + command: TokenReportCommandId; + arguments?: string; client: ReturnType<typeof createClient>; sessionID: string; generatedAtMs?: number; @@ -81,6 +83,7 @@ async function buildTokenDialogOutput(params: { const { buildQuotaDialogCommandOutput } = await import("../src/lib/quota-dialog-commands.js"); const result = await buildQuotaDialogCommandOutput({ command: params.command, + arguments: params.arguments, client: params.client, roots: { workspaceRoot: process.cwd(), @@ -92,14 +95,14 @@ async function buildTokenDialogOutput(params: { }); expect(params.client.session.prompt).not.toHaveBeenCalled(); expect(result.state).toBe("output"); - if (result.state !== "output") return ""; + if (result.state !== "output") throw new Error("expected report output"); expect(params.render(result.document)).toBe(result.output); - return result.output; + return { output: result.output, document: result.document }; } // A titled section, so the markdown and plain-text renderers give different text. const TOKEN_REPORT_DOCUMENT: ReportDocument = { - heading: { title: "Tokens used", generatedAtMs: 0 }, + heading: { line: "# Tokens used 00:00 01/01/1970", subtitle: "00:00 01/01/1970" }, sections: [ { id: "summary", @@ -170,7 +173,7 @@ describe("/tokens_session_all command", () => { it("aggregates the current session tree for /tokens_session_all", async () => { const client = createClient(); - const output = await buildTokenDialogOutput({ + const { output } = await buildTokenDialogOutput({ command: "tokens_session_all", client, sessionID: "ses_parent", @@ -250,7 +253,7 @@ describe("/tokens_session_all command", () => { const client = createClient(); const generatedAtMs = Date.UTC(2026, 0, 2, 3, 4); - const injected = await buildTokenDialogOutput({ + const { output: injected, document } = await buildTokenDialogOutput({ command: "tokens_session_all", client, sessionID: "ses_missing", @@ -267,6 +270,11 @@ describe("/tokens_session_all command", () => { "- checked_path: /tmp/opencode.db", ].join("\n"), ); + // The dialog title is the report's name, so the dialog shows why and when instead. + expect(document.heading).toEqual({ + line: injected.split("\n")[0], + subtitle: `Token report unavailable · ${formatLocalCallTimestamp(generatedAtMs)}`, + }); }); it("returns a dialog session lookup error for /tokens_session", async () => { @@ -276,7 +284,7 @@ describe("/tokens_session_all command", () => { const client = createClient(); - const injected = await buildTokenDialogOutput({ + const { output: injected, document } = await buildTokenDialogOutput({ command: "tokens_session", client, sessionID: "ses_parent", @@ -285,5 +293,30 @@ describe("/tokens_session_all command", () => { expect(injected).toContain("Token report unavailable (/tokens_session)"); expect(injected).toContain("- session_id: ses_parent"); expect(injected).toContain("- checked_path: /tmp/opencode.db"); + expect(document.heading?.subtitle).toMatch(/^Token report unavailable · /); + }); + + it("titles every token report; only /tokens_between has facts the dialog title lacks", async () => { + const { TOKEN_REPORT_COMMANDS } = await import("../src/lib/quota-dialog-command-specs.js"); + for (const spec of TOKEN_REPORT_COMMANDS) { + mocks.buildQuotaStatsReportDocument.mockClear(); + await buildTokenDialogOutput({ + command: spec.id, + arguments: spec.kind === "between" ? "2026-01-01 2026-01-15" : undefined, + client: createClient(), + sessionID: "ses_parent", + render: renderMarkdownReport, + }); + expect(mocks.buildQuotaStatsReportDocument).toHaveBeenCalledWith( + expect.objectContaining( + spec.kind === "between" + ? { + title: "Tokens used (2026-01-01 .. 2026-01-15) (/tokens_between)", + titleDetail: "2026-01-01 .. 2026-01-15", + } + : { title: spec.title, titleDetail: undefined }, + ), + ); + } }); }); diff --git a/tests/quota-command-format.test.ts b/tests/quota-command-format.test.ts index e9b5fa71..d4592881 100644 --- a/tests/quota-command-format.test.ts +++ b/tests/quota-command-format.test.ts @@ -1,6 +1,11 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { formatQuotaCommand, QUOTA_COMMAND_BAR_WIDTH } from "../src/lib/quota-command-format.js"; +import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; +import { + buildQuotaCommandDocument, + formatQuotaCommand, + QUOTA_COMMAND_BAR_WIDTH, +} from "../src/lib/quota-command-format.js"; function accounting( resultType: "quota" | "rate_limit" | "usage" | "spend" | "budget" | "balance" | "status", @@ -516,4 +521,36 @@ describe("formatQuotaCommand", () => { expect(out).not.toContain("19% used"); expect(out).toContain("reset 3d 5h 14m"); }); + + it("keeps the /quota title line in text and gives the dialog the time and bare-label mode", () => { + const generatedAtMs = Date.UTC(2026, 8, 29, 14, 0); + const time = formatLocalCallTimestamp(generatedAtMs); + const data = { + entries: [{ accounting: accounting("quota"), name: "Copilot", percentRemaining: 81 }], + errors: [], + generatedAtMs, + }; + const cases = [ + { options: {}, line: `Quota (/quota) ${time}`, subtitle: time }, + { + options: { percentLabelStyle: "bare", percentDisplayMode: "used" }, + line: `Quota [Used] (/quota) ${time}`, + subtitle: `Percent used · ${time}`, + }, + { + options: { percentLabelStyle: "bare", percentDisplayMode: "remaining" }, + line: `Quota [Remaining] (/quota) ${time}`, + subtitle: `Percent remaining · ${time}`, + }, + ] as const; + + for (const { options, line, subtitle } of cases) { + const text = formatQuotaCommand({ ...data, ...options }); + const document = buildQuotaCommandDocument({ ...data, ...options }); + expect(text.split("\n").slice(0, 2)).toEqual([line, ""]); + expect(document.heading).toEqual({ line, subtitle }); + // The sections, which the dialog draws, start with the first provider group. + expect(document.sections[0]?.title).toBe("→ [Copilot]"); + } + }); }); diff --git a/tests/quota-stats-format.test.ts b/tests/quota-stats-format.test.ts index 80859178..17b33a21 100644 --- a/tests/quota-stats-format.test.ts +++ b/tests/quota-stats-format.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; +import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; import type { AggregateResult } from "../src/lib/quota-stats.js"; -import { formatQuotaStatsReport } from "../src/lib/quota-stats-format.js"; +import { + buildQuotaStatsReportDocument, + formatQuotaStatsReport, +} from "../src/lib/quota-stats-format.js"; function makeEmptyResult(overrides?: Partial<AggregateResult>): AggregateResult { return { @@ -626,4 +630,34 @@ describe("formatQuotaStatsReport (markdown)", () => { Run /quota_status to see the full pricing diagnostics report." `); }); + + it("keeps the title line in text and gives the dialog only the facts its title lacks", () => { + const generatedAtMs = Date.UTC(2026, 8, 29, 14, 0); + const time = formatLocalCallTimestamp(generatedAtMs); + const weekly = { + title: "Tokens used (Last 7 Days) (/tokens_weekly)", + result: makeEmptyResult(), + generatedAtMs, + }; + const between = { + title: "Tokens used (2026-01-01 .. 2026-01-15) (/tokens_between)", + titleDetail: "2026-01-01 .. 2026-01-15", + result: makeEmptyResult(), + generatedAtMs, + }; + + expect(formatQuotaStatsReport(weekly).split("\n")[0]).toBe( + `# Tokens used (Last 7 Days) (/tokens_weekly) ${time}`, + ); + expect(buildQuotaStatsReportDocument(weekly).heading?.subtitle).toBe(time); + expect(formatQuotaStatsReport(between).split("\n")[0]).toBe( + `# Tokens used (2026-01-01 .. 2026-01-15) (/tokens_between) ${time}`, + ); + expect(buildQuotaStatsReportDocument(between).heading?.subtitle).toBe( + `2026-01-01 .. 2026-01-15 · ${time}`, + ); + // The sections, which the dialog draws, never repeat the title. + const sectionsText = formatQuotaStatsReport(weekly).split("\n").slice(1).join("\n"); + expect(sectionsText).not.toContain("Tokens used"); + }); }); diff --git a/tests/report-document.test.ts b/tests/report-document.test.ts index 00c4a0f8..6edf620b 100644 --- a/tests/report-document.test.ts +++ b/tests/report-document.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from "vitest"; +import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; import { padTableColumns } from "../src/lib/markdown-table.js"; import { + commandHeading, isReportDocument, messageDocument, type ReportDocument, @@ -99,6 +101,38 @@ describe("report-document", () => { `); }); + it("builds a command heading: the full line for text, the facts and time for the dialog", () => { + const generatedAtMs = Date.UTC(2026, 8, 29, 14, 0); + const time = formatLocalCallTimestamp(generatedAtMs); + + expect( + commandHeading({ + title: "Quota Status (opencode-quota v5.0.0) (/quota_status)", + detail: "opencode-quota v5.0.0", + generatedAtMs, + }), + ).toEqual({ + line: `# Quota Status (opencode-quota v5.0.0) (/quota_status) ${time}`, + subtitle: `opencode-quota v5.0.0 · ${time}`, + }); + expect( + commandHeading({ title: "Tokens used (Last 7 Days) (/tokens_weekly)", generatedAtMs }), + ).toEqual({ + line: `# Tokens used (Last 7 Days) (/tokens_weekly) ${time}`, + subtitle: time, + }); + }); + + it("puts the heading line first in both text renderers", () => { + const document: ReportDocument = { + heading: { line: "# Report 16:00 29/09/2026", subtitle: "16:00 29/09/2026" }, + sections: [{ id: "notes", title: "Notes", blocks: [{ kind: "lines", lines: ["note"] }] }], + }; + + expect(renderPlainTextReport(document)).toBe("# Report 16:00 29/09/2026\n\nNotes\nnote"); + expect(renderMarkdownReport(document)).toBe("# Report 16:00 29/09/2026\n\n## Notes\n\nnote"); + }); + it("pads table columns to their widths without pipes or escaping", () => { expect( padTableColumns({ @@ -163,7 +197,7 @@ describe("report-document", () => { it("accepts well-formed documents and rejects malformed ones", () => { const document: ReportDocument = { - heading: { title: "Report", generatedAtMs: 1 }, + heading: { line: "# Report 16:00 29/09/2026", subtitle: "16:00 29/09/2026" }, sections: [ { id: "all", @@ -185,6 +219,9 @@ describe("report-document", () => { expect(isReportDocument(document)).toBe(true); expect(isReportDocument(messageDocument("hi"))).toBe(true); + expect(isReportDocument({ heading: { line: "Maintainer announcements" }, sections: [] })).toBe( + true, + ); expect(isReportDocument(JSON.parse(JSON.stringify(document)))).toBe(true); for (const value of [ undefined, @@ -193,7 +230,9 @@ describe("report-document", () => { [], {}, { sections: {} }, - { heading: { title: 1 }, sections: [] }, + { heading: { line: 1 }, sections: [] }, + { heading: { title: "Report", generatedAtMs: 1 }, sections: [] }, + { heading: { line: "# Report", subtitle: 1 }, sections: [] }, { sections: [{ blocks: [] }] }, { sections: [{ id: "s", blocks: [{ kind: "html", lines: [] }] }] }, { sections: [{ id: "s", blocks: [{ kind: "lines", lines: [1] }] }] }, diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index c6235d66..dd198fc4 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -15,9 +15,20 @@ vi.mock("../src/lib/config.js", async (importOriginal) => { }); import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; -import { parseQuotaSlashCommand } from "../src/lib/quota-dialog-command-specs.js"; +import { buildQuotaCommandDocument } from "../src/lib/quota-command-format.js"; +import { + parseQuotaSlashCommand, + QUOTA_DIALOG_COMMANDS, + type QuotaDialogCommandId, + TOKEN_REPORT_COMMANDS, +} from "../src/lib/quota-dialog-command-specs.js"; import { formatQuotaReportMessage } from "../src/lib/quota-report-message.js"; -import { messageDocument, type ReportDocument } from "../src/lib/report-document.js"; +import { buildQuotaStatsReportDocument } from "../src/lib/quota-stats-format.js"; +import { + commandHeading, + messageDocument, + type ReportDocument, +} from "../src/lib/report-document.js"; import plugin from "../src/tui-v2.tsx"; // The server plugin's quota RPC runs palette commands; the TUI shows their output. @@ -47,6 +58,32 @@ function findNode(node: unknown, type: string): Node | undefined { return findNode(element.props?.children, type); } +/** Makes JSX build a plain tree of { type, props } nodes, running components. */ +function stubRenderingReact() { + vi.stubGlobal("React", { + createElement: ( + type: unknown, + props: Record<string, unknown> | null, + ...children: unknown[] + ) => { + const all = { ...props, children: children.length > 1 ? children : children[0] }; + return typeof type === "function" ? type(all) : { type, props: all }; + }, + }); +} + +/** The quota output dialog's title and the subtitle text under it (null when there is none). */ +function dialogHeader(tree: unknown): { + title: string; + subtitle: { children: string; fg: string } | null; +} { + const [titleRow, subtitle] = (tree as Node).props.children[0].props.children; + return { + title: findNode(titleRow, "text")?.props.children, + subtitle: subtitle && { children: subtitle.props.children, fg: subtitle.props.fg }, + }; +} + type RegisteredCommand = { id: string; slash?: unknown; @@ -595,20 +632,11 @@ describe("V2 quota TUI commands", () => { }); it("shows command output in a scrollable dialog that fits the terminal", async () => { - vi.stubGlobal("React", { - createElement: ( - type: unknown, - props: Record<string, unknown> | null, - ...children: unknown[] - ) => { - const all = { ...props, children: children.length > 1 ? children : children[0] }; - return typeof type === "function" ? type(all) : { type, props: all }; - }, - }); + stubRenderingReact(); const lines = Array.from({ length: 80 }, (_, index) => `line ${index + 1}`); const generatedAtMs = Date.UTC(2026, 0, 2, 3, 4); const document: ReportDocument = { - heading: { title: "Tokens used (Today) (/tokens_today)", generatedAtMs }, + heading: commandHeading({ title: "Tokens used (Today) (/tokens_today)", generatedAtMs }), sections: [ { id: "models", @@ -647,8 +675,13 @@ describe("V2 quota TUI commands", () => { const render = context.ui.dialog.show.mock.calls[0][0] as () => unknown; const tree = render(); const scrollbox = findNode(tree, "scrollbox"); - // 40 terminal rows: three quarters is 30, minus 8 rows of dialog chrome. - expect(scrollbox?.props.maxHeight).toBe(22); + // 40 terminal rows: three quarters is 30, minus 8 rows of dialog chrome and the subtitle. + expect(scrollbox?.props.maxHeight).toBe(21); + // The subtitle sits directly under the title, muted; the report's title line is left out. + expect(dialogHeader(tree)).toEqual({ + title: "Quota Status", + subtitle: { children: formatLocalCallTimestamp(generatedAtMs), fg: "muted" }, + }); const texts = findNodes(scrollbox?.props.children, "text").map((node) => ({ children: node.props.children, fg: node.props.fg, @@ -656,12 +689,6 @@ describe("V2 quota TUI commands", () => { wrapMode: node.props.wrapMode, })); expect(texts).toEqual([ - { - children: `Tokens used (Today) (/tokens_today) ${formatLocalCallTimestamp(generatedAtMs)}`, - fg: "base", - bold: true, - wrapMode: undefined, - }, { children: "Top Models", fg: "base", bold: true, wrapMode: undefined }, { children: "Model Cost", fg: "heading", bold: true, wrapMode: "none" }, { children: "gpt-5 $1.23", fg: "base", bold: false, wrapMode: "none" }, @@ -669,11 +696,11 @@ describe("V2 quota TUI commands", () => { { children: "- enabled: true", fg: "base", bold: false, wrapMode: undefined }, { children: lines.join("\n"), fg: "base", bold: false, wrapMode: undefined }, ]); - // One blank row between the heading, the sections, and the blocks of a section; a - // section title sits directly above its first block. + // One blank row between the sections and between the blocks of a section; a section + // title sits directly above its first block. const body = scrollbox?.props.children; expect(body.props.gap).toBe(1); - const section = body.props.children[1][0]; + const section = body.props.children[0]; expect(section.props.gap).toBeUndefined(); expect(section.props.children[1].props.gap).toBe(1); @@ -694,9 +721,146 @@ describe("V2 quota TUI commands", () => { run("pagedown"); run("up"); run("end"); - expect(scroll.scrollBy.mock.calls).toEqual([[22], [-1]]); + expect(scroll.scrollBy.mock.calls).toEqual([[21], [-1]]); expect(scroll.scrollTo).toHaveBeenCalledWith(80); run("return"); expect(context.ui.dialog.clear).toHaveBeenCalledOnce(); }); + + it("shows every command's report title once, as the dialog title, with the subtitle under it", async () => { + stubRenderingReact(); + const generatedAtMs = Date.UTC(2026, 8, 29, 14, 0); + const time = formatLocalCallTimestamp(generatedAtMs); + const tokenResult = { + window: { sinceMs: 0, untilMs: 1 }, + totals: { + priced: { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 }, + unknown: { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 }, + unpriced: { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 }, + costUsd: 0, + messageCount: 0, + sessionCount: 0, + }, + bySourceProvider: [], + bySourceModel: [], + byModel: [], + bySession: [], + unknown: [], + unpriced: [], + }; + const accounting = { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + } as const; + // Each document has the heading its server builder gives it. + const reports = new Map<QuotaDialogCommandId, { document: ReportDocument; subtitle?: string }>([ + [ + "quota", + { + document: buildQuotaCommandDocument({ + entries: [{ accounting, name: "Copilot", percentRemaining: 81 }], + errors: [], + generatedAtMs, + }), + subtitle: time, + }, + ], + [ + "quota_status", + { + document: { + heading: commandHeading({ + title: "Quota Status (opencode-quota v5.0.0) (/quota_status)", + detail: "opencode-quota v5.0.0", + generatedAtMs, + }), + sections: [{ id: "toast", title: "toast:", blocks: [{ kind: "lines", lines: ["-"] }] }], + }, + subtitle: `opencode-quota v5.0.0 · ${time}`, + }, + ], + [ + "quota_announcements", + { + document: { + heading: { line: "Maintainer announcements" }, + sections: [ + { id: "list", blocks: [{ kind: "lines", lines: ["No current announcements."] }] }, + ], + }, + }, + ], + [ + "pricing_refresh", + { + document: { + heading: commandHeading({ title: "Pricing Refresh (/pricing_refresh)", generatedAtMs }), + sections: [ + { id: "refresh", title: "refresh:", blocks: [{ kind: "lines", lines: ["-"] }] }, + ], + }, + subtitle: time, + }, + ], + ...TOKEN_REPORT_COMMANDS.map( + (spec): [QuotaDialogCommandId, { document: ReportDocument; subtitle: string }] => + spec.kind === "between" + ? [ + spec.id, + { + document: buildQuotaStatsReportDocument({ + title: "Tokens used (2026-01-01 .. 2026-01-15) (/tokens_between)", + titleDetail: "2026-01-01 .. 2026-01-15", + result: tokenResult, + generatedAtMs, + }), + subtitle: `2026-01-01 .. 2026-01-15 · ${time}`, + }, + ] + : [ + spec.id, + { + document: buildQuotaStatsReportDocument({ + title: spec.title, + result: tokenResult, + generatedAtMs, + }), + subtitle: time, + }, + ], + ), + ]); + expect([...reports.keys()].sort()).toEqual(QUOTA_DIALOG_COMMANDS.map((spec) => spec.id).sort()); + + for (const spec of QUOTA_DIALOG_COMMANDS) { + const report = reports.get(spec.id)!; + rpc.command.mockResolvedValueOnce({ + state: "output", + command: spec.id, + title: spec.title, + output: "the chat text", + document: report.document, + dialogSize: spec.dialogSize, + }); + const { context, command } = startTui(); + context.ui.dialog.prompt.mockResolvedValue("2026-01-01 2026-01-15"); + + await command(spec.id).run(); + + const tree = (context.ui.dialog.show.mock.calls[0][0] as () => unknown)(); + expect(dialogHeader(tree)).toEqual({ + title: spec.title, + subtitle: report.subtitle === undefined ? null : { children: report.subtitle, fg: "muted" }, + }); + const body = findNodes(findNode(tree, "scrollbox")?.props.children, "text") + .map((node) => node.props.children) + .join("\n"); + expect(body).not.toBe(""); + expect(body).not.toContain(report.document.heading!.line); + expect(body).not.toContain(spec.title); + expect(body).not.toContain(`(/${spec.id})`); + } + }); }); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index cec6a4ac..34dd8b1c 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -195,9 +195,10 @@ async function setupV2Surfaces(client: ReturnType<typeof createClient>, provider // The Enter binding that runs a quota command typed in the TUI prompt. let enter: (() => unknown) | undefined; const editor = { plainText: "", clear: vi.fn() }; - // Records the title and scrollbox text of each quota output dialog the TUI shows. The - // scrollbox text is every text node in order, one per line, without the blank rows. - const dialog = vi.fn((_input: { title: string; message: string }) => {}); + // Records the title, subtitle, and scrollbox text of each quota output dialog the TUI + // shows. The scrollbox text is every text node in order, one per line, without the blank + // rows. + const dialog = vi.fn((_input: { title: string; subtitle?: string; message: string }) => {}); type Node = { type: string; props: Record<string, any> }; const find = (node: unknown, type: string): Node | undefined => { if (Array.isArray(node)) return node.map((child) => find(child, type)).find(Boolean); @@ -216,8 +217,9 @@ async function setupV2Surfaces(client: ReturnType<typeof createClient>, provider const show = (render: () => unknown, onClose?: () => void) => { const tree = render(); const title = find(tree, "text")?.props.children; + const subtitle = (tree as Node).props.children[0].props.children[1]?.props.children; const message = texts(find(tree, "scrollbox")).join("\n"); - dialog({ title, message }); + dialog({ title, subtitle, message }); onClose?.(); }; const toast = vi.fn(); @@ -574,11 +576,20 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(v2.dialog).toHaveBeenCalledOnce(); expect(client.session.prompt).not.toHaveBeenCalled(); const serverOutput = v2.dialog.mock.calls[0][0].message; - expect(serverOutput).toMatch(/^Quota \(\/quota\)/); + // The dialog shows the title once and the time under it, not the report's title line. + const quotaDialog = v2.dialog.mock.calls[0][0]; + expect(quotaDialog.title).toBe("OpenCode Quota"); + expect(quotaDialog.subtitle).toMatch(/^\d{2}:\d{2} \d{2}\/\d{2}\/\d{4}$/); + expect(serverOutput).toMatch(/^→ /); + expect(serverOutput).not.toContain("(/quota)"); // /quota typed in the TUI prompt opens the same report in the dialog and posts nothing. expect(v2.typeCommand("/quota")).toBeUndefined(); await vi.waitFor(() => expect(v2.dialog).toHaveBeenCalledTimes(2)); - expect(v2.dialog.mock.calls[1][0].message).toMatch(/^Quota \(\/quota\)/); + expect(v2.dialog.mock.calls[1][0]).toMatchObject({ + title: "OpenCode Quota", + subtitle: quotaDialog.subtitle, + }); + expect(v2.dialog.mock.calls[1][0].message).toMatch(/^→ /); expect(client.session.prompt).not.toHaveBeenCalled(); expect(serverOutput).not.toContain("```"); expect(serverOutput).not.toMatch(/^#{1,6} /mu); From 60dbe1012edc0755dd780defabee99ce7214b8d4 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:16:29 +0200 Subject: [PATCH 88/97] docs(readme): remove the 5.0 beta note 5.0.0 ships as the stable release, so the plain install command gets 5.x and the @next instructions no longer apply. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 7db04bd3..cb0052a7 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,6 @@ npx @slkiser/opencode-quota init Upgrading from 4.x? Read [what changed in 5.0](#breaking-changes-in-500). -> [!NOTE] -> **Try the 5.0 beta.** Until 5.0.0 ships, the plain command installs 4.x. Run `npx @slkiser/opencode-quota@next init`, or put `"plugin": ["@slkiser/opencode-quota@next"]` in `opencode.json`. `npx @slkiser/opencode-quota@next update` keeps you on `@next`. - After installation: 1. Restart OpenCode. From f8a8d19bffd04ed476f071dfadeb68c8b9263193 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:54:43 +0200 Subject: [PATCH 89/97] fix(tui): use OpenCode theme colors for quota text --- src/tui-v2.tsx | 55 ++++++--- tests/plugin.command-handled-boundary.test.ts | 1 + tests/tui-v2-commands.test.ts | 18 ++- tests/tui-v2-format-style.test.ts | 1 + tests/tui-v2-refresh.test.ts | 1 + tests/tui-v2-theme-colors.test.ts | 115 ++++++++++++++++++ tests/v4-phase5-cross-surface.test.ts | 2 +- 7 files changed, 169 insertions(+), 24 deletions(-) create mode 100644 tests/tui-v2-theme-colors.test.ts diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index f6c61951..2fef7cb8 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -1,7 +1,7 @@ /** @jsxImportSource @opentui/solid */ import { Plugin } from "@opencode/plugin/tui"; -import { RGBA, type ScrollBoxRenderable, TextAttributes } from "@opentui/core"; +import { type RGBA, type ScrollBoxRenderable, TextAttributes } from "@opentui/core"; import { type JSX, useTerminalDimensions } from "@opentui/solid"; import { createSignal, onCleanup, Show } from "solid-js"; @@ -34,7 +34,6 @@ import { type QuotaRpcWriteExportOutput, } from "./rpc.js"; -const terminalForeground = RGBA.defaultForeground(); const REFRESH_INTERVAL_MS = 60_000; const RPC_TIMEOUT_MS = 60_000; @@ -57,7 +56,6 @@ type KeymapCommand = { type DialogTheme = { text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA } } }; background: { action: { primary: { focused: RGBA } } }; - markdown: { heading: RGBA }; }; type QuotaRpcCallOptions = { location: { directory: string }; signal: AbortSignal }; type QuotaRpcClient = { @@ -87,7 +85,7 @@ type TuiContext = { inbox: { cancel: (input: { sessionID: string; inboxID: string }) => Promise<void> }; }; }; - theme: { surface: (name: "dialog") => DialogTheme }; + theme: { text: { base: RGBA; muted: RGBA }; surface: (name: "dialog") => DialogTheme }; data: { on: (event: string, handler: (event: TuiEvent) => void) => () => void; session: { @@ -274,7 +272,7 @@ function QuotaFooter(props: { <Show when={lines().length}> <box flexDirection="column"> {lines().map((line) => ( - <text fg={terminalForeground}>{line}</text> + <text fg={props.context.theme.text.muted}>{line}</text> ))} </box> </Show> @@ -296,18 +294,27 @@ function reportFailure(error: unknown): void { /** * One block of a report. Lines and key-value rows wrap at words. A table keeps each row on * one line: its cells are padded to their column widths and joined by two spaces, with the - * header row bold in the markdown heading color. + * header row bold and muted. Like OpenCode's sidebar, labels are muted and values are base. */ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX.Element { const block = props.block; if (block.kind === "lines") { - return <text fg={props.theme.text.base}>{sanitizeDisplayText(block.lines.join("\n"))}</text>; + return <text fg={props.theme.text.muted}>{sanitizeDisplayText(block.lines.join("\n"))}</text>; } if (block.kind === "kv") { return ( - <text fg={props.theme.text.base}> - {sanitizeDisplayText(block.rows.map(renderKvRow).join("\n"))} - </text> + <box flexDirection="column"> + {block.rows.map(({ value, ...key }) => + value === undefined ? ( + <text fg={props.theme.text.muted}>{sanitizeDisplayText(renderKvRow(key))}</text> + ) : ( + <text fg={props.theme.text.muted}> + {sanitizeDisplayText(renderKvRow({ ...key, trailingColon: true }))} + <span style={{ fg: props.theme.text.base }}> {sanitizeDisplayText(value)}</span> + </text> + ), + )} + </box> ); } const table = padTableColumns({ @@ -317,7 +324,7 @@ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX }); return ( <box flexDirection="column"> - <text attributes={TextAttributes.BOLD} fg={props.theme.markdown.heading} wrapMode="none"> + <text attributes={TextAttributes.BOLD} fg={props.theme.text.muted} wrapMode="none"> {table.header.join(" ")} </text> {table.rows.map((row) => ( @@ -631,22 +638,32 @@ function SidebarQuotaView(props: { context: TuiContext; sessionID: string }): JS onMouseDown={() => expandable() && setOpen((value) => !value)} > <Show when={expandable()}> - <text fg={terminalForeground}>{open() ? "▼" : "▶"}</text> + <text fg={props.context.theme.text.base}>{open() ? "▼" : "▶"}</text> </Show> - <text fg={terminalForeground}> + <text fg={props.context.theme.text.base}> <b>Quota</b> <Show when={expandable() && !open() && quota()?.activeProviderCount}> {(count: () => number) => ` (${count()} active)`} </Show> </text> </box> - <Show when={quota()} fallback={<text fg={terminalForeground}>No quota data available</text>}> + <Show + when={quota()} + fallback={<text fg={props.context.theme.text.muted}>No quota data available</text>} + > <Show when={!expandable() || open()}> - {lines().map((line) => ( - <text fg={terminalForeground} wrapMode="none"> - {line || " "} - </text> - ))} + {lines().map((line) => + // Provider headers such as "[Copilot] (individual)" are bold, like section titles. + line.startsWith("[") ? ( + <text fg={props.context.theme.text.base} wrapMode="none"> + <b>{line}</b> + </text> + ) : ( + <text fg={props.context.theme.text.muted} wrapMode="none"> + {line || " "} + </text> + ), + )} </Show> </Show> </box> diff --git a/tests/plugin.command-handled-boundary.test.ts b/tests/plugin.command-handled-boundary.test.ts index 436d81b9..0a976c16 100644 --- a/tests/plugin.command-handled-boundary.test.ts +++ b/tests/plugin.command-handled-boundary.test.ts @@ -41,6 +41,7 @@ function startTui() { const context = { location: { directory: process.cwd() }, client: { rpc: vi.fn(() => rpc) }, + theme: { text: { base: "base", muted: "muted" } }, data: { session: { get: vi.fn() }, on: vi.fn((name: string, callback: (event: { data?: Record<string, unknown> }) => void) => { diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index dd198fc4..ee323535 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -113,10 +113,10 @@ function startTui( session: { inbox: { cancel: vi.fn().mockResolvedValue(undefined) } }, }, theme: { + text: { base: "base", muted: "muted" }, surface: vi.fn(() => ({ text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, background: { action: { primary: { focused: "action-bg" } } }, - markdown: { heading: "heading" }, })), }, data: { @@ -688,13 +688,23 @@ describe("V2 quota TUI commands", () => { bold: node.props.attributes === TextAttributes.BOLD, wrapMode: node.props.wrapMode, })); + // Section titles are bold base, table headers bold muted, and values base. A key-value + // row is a muted "- key:" with a base value; plain lines are muted. expect(texts).toEqual([ { children: "Top Models", fg: "base", bold: true, wrapMode: undefined }, - { children: "Model Cost", fg: "heading", bold: true, wrapMode: "none" }, + { children: "Model Cost", fg: "muted", bold: true, wrapMode: "none" }, { children: "gpt-5 $1.23", fg: "base", bold: false, wrapMode: "none" }, { children: "claude-opus $10.00", fg: "base", bold: false, wrapMode: "none" }, - { children: "- enabled: true", fg: "base", bold: false, wrapMode: undefined }, - { children: lines.join("\n"), fg: "base", bold: false, wrapMode: undefined }, + { + children: [ + "- enabled:", + { type: "span", props: { style: { fg: "base" }, children: [" ", "true"] } }, + ], + fg: "muted", + bold: false, + wrapMode: undefined, + }, + { children: lines.join("\n"), fg: "muted", bold: false, wrapMode: undefined }, ]); // One blank row between the sections and between the blocks of a section; a section // title sits directly above its first block. diff --git a/tests/tui-v2-format-style.test.ts b/tests/tui-v2-format-style.test.ts index 1a410cf5..5f7ce8f6 100644 --- a/tests/tui-v2-format-style.test.ts +++ b/tests/tui-v2-format-style.test.ts @@ -24,6 +24,7 @@ describe("V2 sidebar format style", () => { let sidebarRender: ((props: { sessionID: string }) => unknown) | undefined; plugin.setup({ client: { rpc: () => rpc }, + theme: { text: { base: "base", muted: "muted" } }, location: { directory: "/work/project" }, data: { on: vi.fn(() => vi.fn()) }, keymap: { layer: vi.fn() }, diff --git a/tests/tui-v2-refresh.test.ts b/tests/tui-v2-refresh.test.ts index 387d743e..74d812cb 100644 --- a/tests/tui-v2-refresh.test.ts +++ b/tests/tui-v2-refresh.test.ts @@ -24,6 +24,7 @@ function setupFooterSlots( const renderers = new Map<string, (props?: any) => unknown>(); plugin.setup({ client, + theme: { text: { base: "base", muted: "muted" } }, data: { location: { default: () => ({ directory: "/work/default" }) }, on: vi.fn((event: string, handler: (event: unknown) => void) => { diff --git a/tests/tui-v2-theme-colors.test.ts b/tests/tui-v2-theme-colors.test.ts new file mode 100644 index 00000000..1c8aa5dd --- /dev/null +++ b/tests/tui-v2-theme-colors.test.ts @@ -0,0 +1,115 @@ +import { createRoot } from "solid-js"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Seeds the views' Solid signals in creation order, so one render shows loaded quota. +const seeds = vi.hoisted(() => [] as unknown[]); +vi.mock("solid-js", async (importOriginal) => { + const actual = await importOriginal<typeof import("solid-js")>(); + return { + ...actual, + createSignal: ((value?: unknown) => + actual.createSignal(seeds.length ? seeds.shift() : value)) as typeof actual.createSignal, + }; +}); + +import plugin from "../src/tui-v2.tsx"; + +type Node = { type: string; props: Record<string, any> }; + +function findNodes(node: unknown, type: string): Node[] { + if (Array.isArray(node)) return node.flatMap((child) => findNodes(child, type)); + if (!node || typeof node !== "object") return []; + const element = node as Node; + if (element.type === type) return [element]; + return findNodes(element.props?.children, type); +} + +/** A text node's words, with bold (<b>) children marked. */ +function describeText(node: Node): { text: string; fg: string; bold: boolean } { + const children = [node.props.children].flat(Number.POSITIVE_INFINITY); + return { + text: children + .map((child) => (typeof child === "string" ? child : (child?.props?.children ?? ""))) + .join(""), + fg: node.props.fg, + bold: children.some((child) => child?.type === "b"), + }; +} + +function renderSlot(append: string, props?: { sessionID: string }): unknown { + vi.stubGlobal("React", { + createElement: ( + type: unknown, + elementProps: Record<string, unknown> | null, + ...children: unknown[] + ) => { + const all = { ...elementProps, children: children.length > 1 ? children : children[0] }; + return typeof type === "function" ? type(all) : { type, props: all }; + }, + }); + // The RPC never answers: the seeded signals hold what the view shows. + const pending = () => new Promise(() => {}); + let render: ((props?: { sessionID: string }) => unknown) | undefined; + plugin.setup({ + client: { rpc: () => ({ surface: pending, footer: pending, writeExport: pending }) }, + location: { directory: "/work/project" }, + theme: { text: { base: "base", muted: "muted" } }, + data: { on: vi.fn(() => vi.fn()) }, + keymap: { layer: vi.fn() }, + ui: { + slot: vi.fn((claim) => { + if (claim.append === append) render = claim.render; + return vi.fn(); + }), + }, + } as any); + return createRoot((dispose) => { + const tree = render?.(props); + dispose(); + return tree; + }); +} + +describe("V2 TUI theme colors", () => { + afterEach(() => { + seeds.length = 0; + }); + + it("draws the sidebar like OpenCode's Context section: bold base titles, muted body", () => { + const message = ["[Copilot] (individual)*", "Premium 5h 80%", "", "Session tokens"].join( + "\n", + ); + seeds.push(true, { message, duration: 5000, activeProviderCount: 1 }); + + const texts = findNodes(renderSlot("sidebar.content", { sessionID: "ses_1" }), "text"); + + expect(texts.map(describeText)).toEqual([ + { text: "▼", fg: "base", bold: false }, + { text: "Quota", fg: "base", bold: true }, + { text: "[Copilot] (individual)*", fg: "base", bold: true }, + { text: "Premium 5h 80%", fg: "muted", bold: false }, + { text: " ", fg: "muted", bold: false }, + { text: "Session tokens", fg: "muted", bold: false }, + ]); + }); + + it("draws the sidebar's empty state muted", () => { + const texts = findNodes(renderSlot("sidebar.content", { sessionID: "ses_1" }), "text"); + + expect(texts.map(describeText)).toEqual([ + { text: "Quota", fg: "base", bold: true }, + { text: "No quota data available", fg: "muted", bold: false }, + ]); + }); + + it.each([ + ["prompt.footer", { sessionID: "ses_1" }], + ["home.footer.status", undefined], + ])("draws the %s line muted", (append, props) => { + seeds.push(["Copilot 50%"]); + + const texts = findNodes(renderSlot(append, props), "text"); + + expect(texts.map(describeText)).toEqual([{ text: "Copilot 50%", fg: "muted", bold: false }]); + }); +}); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 34dd8b1c..c28bcf68 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -242,10 +242,10 @@ async function setupV2Surfaces(client: ReturnType<typeof createClient>, provider location: { directory: process.cwd() }, renderer: { currentFocusedEditor: editor }, theme: { + text: { base: "base", muted: "muted" }, surface: () => ({ text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, background: { action: { primary: { focused: "action-bg" } } }, - markdown: { heading: "heading" }, }), }, data: { From f5794cd020f67f66fae0970df3e07a794a10b2db Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:13:34 +0200 Subject: [PATCH 90/97] fix(tui): color popup table headers with OpenCode's primary accent --- src/tui-v2.tsx | 11 ++++++++--- tests/tui-v2-commands.test.ts | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 2fef7cb8..ae25dc7a 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -54,7 +54,7 @@ type KeymapCommand = { run: () => void | false | Promise<void>; }; type DialogTheme = { - text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA } } }; + text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA; selected: RGBA } } }; background: { action: { primary: { focused: RGBA } } }; }; type QuotaRpcCallOptions = { location: { directory: string }; signal: AbortSignal }; @@ -294,7 +294,8 @@ function reportFailure(error: unknown): void { /** * One block of a report. Lines and key-value rows wrap at words. A table keeps each row on * one line: its cells are padded to their column widths and joined by two spaces, with the - * header row bold and muted. Like OpenCode's sidebar, labels are muted and values are base. + * header row bold in OpenCode's primary accent (its text color for a selected tab). Like + * OpenCode's sidebar, labels are muted and values are base. */ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX.Element { const block = props.block; @@ -324,7 +325,11 @@ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX }); return ( <box flexDirection="column"> - <text attributes={TextAttributes.BOLD} fg={props.theme.text.muted} wrapMode="none"> + <text + attributes={TextAttributes.BOLD} + fg={props.theme.text.action.primary.selected} + wrapMode="none" + > {table.header.join(" ")} </text> {table.rows.map((row) => ( diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index ee323535..f3a336c6 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -115,7 +115,11 @@ function startTui( theme: { text: { base: "base", muted: "muted" }, surface: vi.fn(() => ({ - text: { base: "base", muted: "muted", action: { primary: { focused: "action" } } }, + text: { + base: "base", + muted: "muted", + action: { primary: { focused: "action", selected: "accent" } }, + }, background: { action: { primary: { focused: "action-bg" } } }, })), }, @@ -688,11 +692,11 @@ describe("V2 quota TUI commands", () => { bold: node.props.attributes === TextAttributes.BOLD, wrapMode: node.props.wrapMode, })); - // Section titles are bold base, table headers bold muted, and values base. A key-value + // Section titles are bold base, table headers bold accent, and values base. A key-value // row is a muted "- key:" with a base value; plain lines are muted. expect(texts).toEqual([ { children: "Top Models", fg: "base", bold: true, wrapMode: undefined }, - { children: "Model Cost", fg: "muted", bold: true, wrapMode: "none" }, + { children: "Model Cost", fg: "accent", bold: true, wrapMode: "none" }, { children: "gpt-5 $1.23", fg: "base", bold: false, wrapMode: "none" }, { children: "claude-opus $10.00", fg: "base", bold: false, wrapMode: "none" }, { From 4a5ee34c20f59d46012d31885e21b070f866e5ec Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:28:00 +0200 Subject: [PATCH 91/97] fix(reports): use clearer section titles in token reports --- src/lib/quota-stats-format.ts | 14 +++++++------- tests/quota-stats-format.test.ts | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/lib/quota-stats-format.ts b/src/lib/quota-stats-format.ts index 9abcd0df..fbfe4f0a 100644 --- a/src/lib/quota-stats-format.ts +++ b/src/lib/quota-stats-format.ts @@ -163,7 +163,7 @@ type QuotaStatsReportParams = { topModels?: number; topSessions?: number; focusSessionID?: string; - /** When true, hides Window/Sessions columns and Top Sessions section (for session-only reports) */ + /** When true, hides Window/Sessions columns and the Top sessions section (for session-only reports) */ sessionOnly?: boolean; reportKind?: QuotaStatsReportKind; sessionTree?: { @@ -327,7 +327,7 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R if (rows.length > 0) { sections.push({ id: "models", - title: "Models", + title: "Model breakdown", blocks: [ { kind: "table", @@ -357,7 +357,7 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R sections.push({ id: "session-tree", - title: "Session Tree", + title: "Session tree", blocks: [ { kind: "table", @@ -372,7 +372,7 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R }); } - // Skip Top Sessions for session-scoped reports (e.g., /tokens_session, /tokens_session_all). + // Skip Top sessions for session-scoped reports (e.g., /tokens_session, /tokens_session_all). if (reportKind === "standard") { const sessionRows: string[][] = []; const visibleSessions = r.bySession.filter(hasRenderableSessionUsage); @@ -407,7 +407,7 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R sections.push({ id: "top-sessions", - title: "Top Sessions", + title: "Top sessions", blocks: sessionRows.length > 0 ? [ @@ -428,7 +428,7 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R if (r.unpriced.length > 0) { sections.push({ id: "unpriced-models", - title: "Unpriced Models", + title: "Models with no token prices", blocks: [ { kind: "table", @@ -456,7 +456,7 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R if (r.unknown.length > 0) { sections.push({ id: "unknown-pricing", - title: "Unknown Pricing", + title: "Models without pricing", blocks: [ { kind: "table", diff --git a/tests/quota-stats-format.test.ts b/tests/quota-stats-format.test.ts index 17b33a21..e4285cf8 100644 --- a/tests/quota-stats-format.test.ts +++ b/tests/quota-stats-format.test.ts @@ -77,7 +77,7 @@ describe("formatQuotaStatsReport (markdown)", () => { expect(out).toMatch( /^# Tokens used \(Last 24 Hours\) \(\/tokens_daily\) \d{2}:\d{2} \d{2}\/\d{2}\/\d{4}\n\n/, ); - expect(out).toContain("## Models"); + expect(out).toContain("## Model breakdown"); expect(out).toContain("| Source"); // blank separator row between sources expect(out).toContain("| |"); @@ -229,7 +229,7 @@ describe("formatQuotaStatsReport (markdown)", () => { expect(out).not.toContain("Reasoning"); }); - it("sessionOnly mode hides Window/Sessions columns and Top Sessions section", () => { + it("sessionOnly mode hides Window/Sessions columns and Top sessions section", () => { const r = makeEmptyResult({ totals: { priced: { input: 100, output: 200, reasoning: 0, cache_read: 0, cache_write: 0 }, @@ -279,8 +279,8 @@ describe("formatQuotaStatsReport (markdown)", () => { expect(out).toContain("Tokens"); expect(out).toContain("Cost"); - // Top Sessions section should NOT be present - expect(out).not.toContain("## Top Sessions"); + // Top sessions section should NOT be present + expect(out).not.toContain("## Top sessions"); }); it("session_tree mode renders a session breakdown and counts zero-usage descendants", () => { @@ -346,17 +346,17 @@ describe("formatQuotaStatsReport (markdown)", () => { expect(out).toContain("| Messages"); expect(out).toContain("| Sessions"); - expect(out).toContain("## Session Tree"); + expect(out).toContain("## Session tree"); expect(out).toContain("current"); expect(out).toContain("child"); expect(out).toContain("grandchild"); expect(out).toContain("ses_parent"); expect(out).toContain("ses_grandchild"); expect(out).toContain("$0.00"); - expect(out).not.toContain("## Top Sessions"); + expect(out).not.toContain("## Top sessions"); }); - it("standard mode includes Window/Sessions columns and Top Sessions section", () => { + it("standard mode includes Window/Sessions columns and Top sessions section", () => { const r = makeEmptyResult({ totals: { priced: { input: 100, output: 200, reasoning: 0, cache_read: 0, cache_write: 0 }, @@ -396,8 +396,8 @@ describe("formatQuotaStatsReport (markdown)", () => { expect(out).toContain("Window"); expect(out).toContain("Sessions"); - // Top Sessions section SHOULD be present - expect(out).toContain("## Top Sessions"); + // Top sessions section SHOULD be present + expect(out).toContain("## Top sessions"); // Marker column should be named and not render as an empty header expect(out).toContain("| Current"); expect(out).toContain("| Session"); @@ -603,25 +603,25 @@ describe("formatQuotaStatsReport (markdown)", () => { | -------- | -------: | -------: | -----: | ----: | | all time | 6 | 2 | 3.1K | $1.23 | - ## Models + ## Model breakdown | Source | Model | Input | Output | C.Read | C.Write | Total | Cost | | -------- | -------------------- | ----: | -----: | -----: | ------: | ----: | ----: | | OpenCode | claude-opus-4-5-high | 1.0K | 2.0K | 0 | 0 | 3.0K | $1.23 | - ## Top Sessions + ## Top sessions | Current | Session | Cost | Tokens | Msgs | Title | | ------- | ------- | ----: | -----: | ---: | ------------ | | | ses_123 | $0.50 | 300 | 3 | Test Session | - ## Unpriced Models + ## Models with no token prices | Source | Model | Mapped | Reason | Tokens | Msgs | | ------ | --------- | ---------------- | ---------------------- | -----: | ---: | | Cursor | foo-model | openai/foo-model | snapshot missing model | 70 | 1 | - ## Unknown Pricing + ## Models without pricing | Source | Model | Mapped | Tokens | Msgs | | -------- | --------- | ----------------------------------------------- | -----: | ---: | From 01e07a96b5078bead4bf75b531355636594d7691 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:34:23 +0200 Subject: [PATCH 92/97] feat(tui): stretch popup tables to the dialog width --- src/lib/markdown-table.ts | 40 ++++++++++++++++ src/lib/quota-stats-format.ts | 77 ++++++++++++++++++++---------- src/lib/report-document.ts | 5 ++ src/tui-v2.tsx | 66 ++++++++++++++++++-------- tests/quota-stats-format.test.ts | 51 ++++++++++++++------ tests/report-document.test.ts | 71 +++++++++++++++++++++++++++- tests/tui-v2-commands.test.ts | 81 ++++++++++++++++++++++++++++---- 7 files changed, 322 insertions(+), 69 deletions(-) diff --git a/src/lib/markdown-table.ts b/src/lib/markdown-table.ts index 1c92477a..feab55ea 100644 --- a/src/lib/markdown-table.ts +++ b/src/lib/markdown-table.ts @@ -172,3 +172,43 @@ export function padTableColumns(params: { cells.map((cell, i) => padCell(cell, widths[i], params.aligns[i] ?? "left", "raw")); return { header: padRow(params.headers), rows: rows.map(padRow) }; } + +/** + * Lays out a table drawn without pipes to span `width` columns, for the TUI dialog. The + * header uses the full labels when the table fits with them, else the compact ones. The + * spare width is spread over the gaps between columns (at least two spaces each, the + * leftmost gaps taking any remainder), so cells keep their alignment inside their columns. + * A table too wide even with the compact labels keeps its natural layout: compact labels + * and two-space gaps. + */ +export function fitTableToWidth(params: { + headers: string[]; + fullHeaders?: string[]; + rows: string[][]; + aligns: Array<"left" | "right">; + width: number; +}): { header: string; rows: string[] } { + const naturalWidth = (table: { header: string[] }) => + table.header.reduce((sum, cell) => sum + measureWidth(cell), 0) + 2 * (table.header.length - 1); + const compact = padTableColumns(params); + const full = params.fullHeaders + ? padTableColumns({ ...params, headers: params.fullHeaders }) + : undefined; + const fitting = [full, compact].find( + (table) => table !== undefined && naturalWidth(table) <= params.width, + ); + + const gapCount = params.headers.length - 1; + const gaps = new Array<number>(Math.max(0, gapCount)).fill(2); + if (fitting && gapCount > 0) { + const spare = params.width - naturalWidth(fitting); + for (let i = 0; i < gapCount; i++) { + gaps[i] += Math.floor(spare / gapCount) + (i < spare % gapCount ? 1 : 0); + } + } + + const table = fitting ?? compact; + const joinRow = (cells: string[]) => + cells.map((cell, i) => (i < gapCount ? cell + " ".repeat(gaps[i]) : cell)).join(""); + return { header: joinRow(table.header), rows: table.rows.map(joinRow) }; +} diff --git a/src/lib/quota-stats-format.ts b/src/lib/quota-stats-format.ts index fbfe4f0a..7e1c65a8 100644 --- a/src/lib/quota-stats-format.ts +++ b/src/lib/quota-stats-format.ts @@ -25,6 +25,18 @@ type QuotaStatsReportTableOptions = { modelNameMaxWidth?: number; }; +/** + * A table's header labels: the compact ones when asked for, else the full ones. The full + * labels also ride along, so the TUI dialog can show them when the table fits with them. + */ +function tableHeaders( + options: QuotaStatsReportTableOptions, + compact: string[], + full: string[], +): { headers: string[]; fullHeaders: string[] } { + return { headers: options.compactHeaders ? compact : full, fullHeaders: full }; +} + function hasRenderableSessionUsage(row: SessionReportRow): boolean { return totalTokenBuckets(row.tokens) > 0 || row.costUsd > 0; } @@ -204,9 +216,7 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R blocks: [ { kind: "table", - headers: tableOptions.compactHeaders - ? ["Msgs", "Tok", "Cost"] - : ["Messages", "Tokens", "Cost"], + ...tableHeaders(tableOptions, ["Msgs", "Tok", "Cost"], ["Messages", "Tokens", "Cost"]), aligns: ["right", "right", "right"], widthMode: TABLE_WIDTH_MODE, rows: [ @@ -225,9 +235,11 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R blocks: [ { kind: "table", - headers: tableOptions.compactHeaders - ? ["Msgs", "Sess", "Tok", "Cost"] - : ["Messages", "Sessions", "Tokens", "Cost"], + ...tableHeaders( + tableOptions, + ["Msgs", "Sess", "Tok", "Cost"], + ["Messages", "Sessions", "Tokens", "Cost"], + ), aligns: ["right", "right", "right", "right"], widthMode: TABLE_WIDTH_MODE, rows: [ @@ -247,9 +259,11 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R blocks: [ { kind: "table", - headers: tableOptions.compactHeaders - ? ["Window", "Msgs", "Sess", "Tok", "Cost"] - : ["Window", "Messages", "Sessions", "Tokens", "Cost"], + ...tableHeaders( + tableOptions, + ["Window", "Msgs", "Sess", "Tok", "Cost"], + ["Window", "Messages", "Sessions", "Tokens", "Cost"], + ), aligns: ["left", "right", "right", "right", "right"], widthMode: TABLE_WIDTH_MODE, rows: [ @@ -271,16 +285,18 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R r.totals.unknown.reasoning > 0 || r.totals.unpriced.reasoning > 0; - const headers = tableOptions.compactHeaders - ? ["Source", "Model", "In", "Out", "C.Rd", "C.Wr"] - : ["Source", "Model", "Input", "Output", "C.Read", "C.Write"]; + const compactLabels = ["Source", "Model", "In", "Out", "C.Rd", "C.Wr"]; + const fullLabels = ["Source", "Model", "Input", "Output", "Cache read", "Cache write"]; const aligns: Array<"left" | "right"> = ["left", "left", "right", "right", "right", "right"]; if (hasAnyReasoning) { - headers.push(tableOptions.compactHeaders ? "Rsn" : "Reasoning"); + compactLabels.push("Rsn"); + fullLabels.push("Reasoning"); aligns.push("right"); } - headers.push(tableOptions.compactHeaders ? "Tok" : "Total", "Cost"); + compactLabels.push("Tok", "Cost"); + fullLabels.push("Tokens", "Cost"); aligns.push("right", "right"); + const { headers, fullHeaders } = tableHeaders(tableOptions, compactLabels, fullLabels); const rows: string[][] = []; const grouped = new Map<string, AggregateResult["bySourceModel"]>(); @@ -332,6 +348,7 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R { kind: "table", headers, + fullHeaders, rows, aligns, widthMode: TABLE_WIDTH_MODE, @@ -361,9 +378,11 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R blocks: [ { kind: "table", - headers: tableOptions.compactHeaders - ? ["Rel", "Parent", "Session", "Cost", "Tok", "Msgs", "Title"] - : ["Relation", "Parent", "Session", "Cost", "Tokens", "Msgs", "Title"], + ...tableHeaders( + tableOptions, + ["Rel", "Parent", "Session", "Cost", "Tok", "Msgs", "Title"], + ["Relation", "Parent", "Session", "Cost", "Tokens", "Messages", "Title"], + ), aligns: ["left", "left", "left", "right", "right", "right", "left"], widthMode: TABLE_WIDTH_MODE, rows: sessionTreeRows, @@ -413,9 +432,11 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R ? [ { kind: "table", - headers: tableOptions.compactHeaders - ? ["Cur", "Session", "Cost", "Tok", "Msgs", "Title"] - : ["Current", "Session", "Cost", "Tokens", "Msgs", "Title"], + ...tableHeaders( + tableOptions, + ["Cur", "Session", "Cost", "Tok", "Msgs", "Title"], + ["Current", "Session", "Cost", "Tokens", "Messages", "Title"], + ), aligns: ["left", "left", "right", "right", "right", "left"], widthMode: TABLE_WIDTH_MODE, rows: sessionRows, @@ -432,9 +453,11 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R blocks: [ { kind: "table", - headers: tableOptions.compactHeaders - ? ["Source", "Model", "Map", "Reason", "Tok", "Msgs"] - : ["Source", "Model", "Mapped", "Reason", "Tokens", "Msgs"], + ...tableHeaders( + tableOptions, + ["Source", "Model", "Map", "Reason", "Tok", "Msgs"], + ["Source", "Model", "Mapped", "Reason", "Tokens", "Messages"], + ), aligns: ["left", "left", "left", "left", "right", "right"], widthMode: TABLE_WIDTH_MODE, rows: r.unpriced.slice(0, 20).map((u) => { @@ -460,9 +483,11 @@ export function buildQuotaStatsReportDocument(params: QuotaStatsReportParams): R blocks: [ { kind: "table", - headers: tableOptions.compactHeaders - ? ["Source", "Model", "Map", "Tok", "Msgs"] - : ["Source", "Model", "Mapped", "Tokens", "Msgs"], + ...tableHeaders( + tableOptions, + ["Source", "Model", "Map", "Tok", "Msgs"], + ["Source", "Model", "Mapped", "Tokens", "Messages"], + ), aligns: ["left", "left", "left", "right", "right"], widthMode: TABLE_WIDTH_MODE, rows: r.unknown.slice(0, 20).map((u) => { diff --git a/src/lib/report-document.ts b/src/lib/report-document.ts index 5b6f6917..d79ec88d 100644 --- a/src/lib/report-document.ts +++ b/src/lib/report-document.ts @@ -23,6 +23,8 @@ export type ReportBlock = | { kind: "table"; headers: string[]; + /** Longer labels, one per column, that the TUI dialog shows when the table fits with them. */ + fullHeaders?: string[]; rows: string[][]; aligns: Array<"left" | "right">; widthMode?: WidthMode; @@ -111,6 +113,9 @@ function isReportBlock(value: unknown): boolean { case "table": return ( isStringArray(value.headers) && + (value.fullHeaders === undefined || + (isStringArray(value.fullHeaders) && + value.fullHeaders.length === value.headers.length)) && Array.isArray(value.rows) && value.rows.every(isStringArray) && Array.isArray(value.aligns) && diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index ae25dc7a..24377a8c 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -8,7 +8,7 @@ import { createSignal, onCleanup, Show } from "solid-js"; import { loadConfig } from "./lib/config.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; -import { padTableColumns } from "./lib/markdown-table.js"; +import { fitTableToWidth } from "./lib/markdown-table.js"; import { parseQuotaSlashCommand, QUOTA_DIALOG_COMMANDS, @@ -53,6 +53,7 @@ type KeymapCommand = { /** Returning false lets the key continue to the next layer. */ run: () => void | false | Promise<void>; }; +type DialogSize = "medium" | "large" | "xlarge"; type DialogTheme = { text: { base: RGBA; muted: RGBA; action: { primary: { focused: RGBA; selected: RGBA } } }; background: { action: { primary: { focused: RGBA } } }; @@ -118,7 +119,7 @@ type TuiContext = { show: (render: () => JSX.Element, onClose?: () => void) => void; clear: () => void; prompt: (params: { title: string; placeholder?: string }) => Promise<string | undefined>; - set: (params: { size: "medium" | "large" | "xlarge" }) => void; + set: (params: { size: DialogSize }) => void; }; }; }; @@ -291,13 +292,20 @@ function reportFailure(error: unknown): void { console.warn(`[opencode-quota] failed to load quota: ${rpcErrorMessage(error)}${hint}`); } +/** OpenCode's dialog widths (dialogWidth in its ui/dialog.tsx). */ +const DIALOG_WIDTHS: Record<DialogSize, number> = { medium: 60, large: 88, xlarge: 116 }; + /** * One block of a report. Lines and key-value rows wrap at words. A table keeps each row on - * one line: its cells are padded to their column widths and joined by two spaces, with the - * header row bold in OpenCode's primary accent (its text color for a selected tab). Like - * OpenCode's sidebar, labels are muted and values are base. + * one line and spans `width` columns when it fits (see fitTableToWidth), with the header + * row bold in OpenCode's primary accent (its text color for a selected tab). Like OpenCode's + * sidebar, labels are muted and values are base. */ -function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX.Element { +function ReportBlockView(props: { + block: ReportBlock; + theme: DialogTheme; + width: number; +}): JSX.Element { const block = props.block; if (block.kind === "lines") { return <text fg={props.theme.text.muted}>{sanitizeDisplayText(block.lines.join("\n"))}</text>; @@ -318,11 +326,15 @@ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX </box> ); } - const table = padTableColumns({ - headers: block.headers.map(sanitizeDisplayText), - rows: block.rows.map((row) => row.map(sanitizeDisplayText)), - aligns: block.aligns, - }); + // Read in the JSX, so the table is laid out again when the terminal is resized. + const table = () => + fitTableToWidth({ + headers: block.headers.map(sanitizeDisplayText), + fullHeaders: block.fullHeaders?.map(sanitizeDisplayText), + rows: block.rows.map((row) => row.map(sanitizeDisplayText)), + aligns: block.aligns, + width: props.width, + }); return ( <box flexDirection="column"> <text @@ -330,11 +342,11 @@ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX fg={props.theme.text.action.primary.selected} wrapMode="none" > - {table.header.join(" ")} + {table().header} </text> - {table.rows.map((row) => ( + {table().rows.map((row) => ( <text fg={props.theme.text.base} wrapMode="none"> - {row.join(" ")} + {row} </text> ))} </box> @@ -347,7 +359,11 @@ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme }): JSX * above its first block. Section titles are bold. The dialog shows the heading's subtitle * under its own title, so the heading line is left out. */ -function ReportDocumentView(props: { document: ReportDocument; theme: DialogTheme }): JSX.Element { +function ReportDocumentView(props: { + document: ReportDocument; + theme: DialogTheme; + width: number; +}): JSX.Element { return ( <box flexDirection="column" gap={1}> {renderableSections(props.document).map((section) => ( @@ -359,7 +375,7 @@ function ReportDocumentView(props: { document: ReportDocument; theme: DialogThem ) : null} <box flexDirection="column" gap={1}> {section.blocks.map((block) => ( - <ReportBlockView block={block} theme={props.theme} /> + <ReportBlockView block={block} theme={props.theme} width={props.width} /> ))} </box> </box> @@ -378,9 +394,14 @@ function QuotaOutputDialog(props: { context: TuiContext; title: string; document: ReportDocument; + size: DialogSize; }): JSX.Element { const theme = () => props.context.theme.surface("dialog"); const dimensions = useTerminalDimensions(); + // OpenCode's dialog is its size's width, at most the terminal width minus 2. Inside it + // come this box's paddings (2 + 2) and one column for the scrollbox's scrollbar, which + // shows when the report is taller than the box. + const tableWidth = () => Math.min(DIALOG_WIDTHS[props.size], dimensions().width - 2) - 4 - 1; const subtitle = props.document.heading?.subtitle; // The host dialog starts a quarter of the way down the terminal. The remaining // 8 rows cover the title, ok button, paddings, gaps, and one spare row; the subtitle @@ -438,7 +459,7 @@ function QuotaOutputDialog(props: { }} maxHeight={maxHeight()} > - <ReportDocumentView document={props.document} theme={theme()} /> + <ReportDocumentView document={props.document} theme={theme()} width={tableWidth()} /> </scrollbox> </box> <box flexDirection="row" justifyContent="flex-end" paddingBottom={1}> @@ -457,11 +478,18 @@ function QuotaOutputDialog(props: { function showQuotaOutputDialog( context: TuiContext, - output: { title: string; document: ReportDocument; dialogSize: "medium" | "large" | "xlarge" }, + output: { title: string; document: ReportDocument; dialogSize: DialogSize }, ): Promise<void> { return new Promise<void>((resolve) => { context.ui.dialog.show( - () => <QuotaOutputDialog context={context} title={output.title} document={output.document} />, + () => ( + <QuotaOutputDialog + context={context} + title={output.title} + document={output.document} + size={output.dialogSize} + /> + ), resolve, ); context.ui.dialog.set({ size: output.dialogSize }); diff --git a/tests/quota-stats-format.test.ts b/tests/quota-stats-format.test.ts index e4285cf8..2b727d97 100644 --- a/tests/quota-stats-format.test.ts +++ b/tests/quota-stats-format.test.ts @@ -114,7 +114,7 @@ describe("formatQuotaStatsReport (markdown)", () => { expect(standard).toContain(longModel); expect(standard).toContain("Input"); expect(standard).toContain("Output"); - expect(standard).toContain("C.Read"); + expect(standard).toContain("Cache read"); const compact = formatQuotaStatsReport({ title: "Tokens used (Last 24 Hours) (/tokens_daily)", @@ -136,8 +136,29 @@ describe("formatQuotaStatsReport (markdown)", () => { expect(compact).toContain("C.Wr"); expect(compact).not.toContain("Input"); expect(compact).not.toContain("Output"); - expect(compact).not.toContain("C.Read"); - expect(compact).not.toContain("C.Write"); + expect(compact).not.toContain("Cache read"); + expect(compact).not.toContain("Cache write"); + + // The document keeps each table's full labels next to the compact ones, for the TUI dialog. + const document = buildQuotaStatsReportDocument({ + title: "Tokens used (Last 24 Hours) (/tokens_daily)", + result: r, + tableOptions: { compactHeaders: true }, + }); + const models = document.sections.find((section) => section.id === "models")?.blocks[0]; + expect(models).toMatchObject({ + headers: ["Source", "Model", "In", "Out", "C.Rd", "C.Wr", "Tok", "Cost"], + fullHeaders: [ + "Source", + "Model", + "Input", + "Output", + "Cache read", + "Cache write", + "Tokens", + "Cost", + ], + }); }); it("abbreviates antigravity model names before width enforcement", () => { @@ -605,27 +626,27 @@ describe("formatQuotaStatsReport (markdown)", () => { ## Model breakdown - | Source | Model | Input | Output | C.Read | C.Write | Total | Cost | - | -------- | -------------------- | ----: | -----: | -----: | ------: | ----: | ----: | - | OpenCode | claude-opus-4-5-high | 1.0K | 2.0K | 0 | 0 | 3.0K | $1.23 | + | Source | Model | Input | Output | Cache read | Cache write | Tokens | Cost | + | -------- | -------------------- | ----: | -----: | ---------: | ----------: | -----: | ----: | + | OpenCode | claude-opus-4-5-high | 1.0K | 2.0K | 0 | 0 | 3.0K | $1.23 | ## Top sessions - | Current | Session | Cost | Tokens | Msgs | Title | - | ------- | ------- | ----: | -----: | ---: | ------------ | - | | ses_123 | $0.50 | 300 | 3 | Test Session | + | Current | Session | Cost | Tokens | Messages | Title | + | ------- | ------- | ----: | -----: | -------: | ------------ | + | | ses_123 | $0.50 | 300 | 3 | Test Session | ## Models with no token prices - | Source | Model | Mapped | Reason | Tokens | Msgs | - | ------ | --------- | ---------------- | ---------------------- | -----: | ---: | - | Cursor | foo-model | openai/foo-model | snapshot missing model | 70 | 1 | + | Source | Model | Mapped | Reason | Tokens | Messages | + | ------ | --------- | ---------------- | ---------------------- | -----: | -------: | + | Cursor | foo-model | openai/foo-model | snapshot missing model | 70 | 1 | ## Models without pricing - | Source | Model | Mapped | Tokens | Msgs | - | -------- | --------- | ----------------------------------------------- | -----: | ---: | - | OpenCode | bar-model | openai/bar-model (candidates: openai,anthropic) | 30 | 1 | + | Source | Model | Mapped | Tokens | Messages | + | -------- | --------- | ----------------------------------------------- | -----: | -------: | + | OpenCode | bar-model | openai/bar-model (candidates: openai,anthropic) | 30 | 1 | Run /quota_status to see the full pricing diagnostics report." `); diff --git a/tests/report-document.test.ts b/tests/report-document.test.ts index 6edf620b..7bd3013e 100644 --- a/tests/report-document.test.ts +++ b/tests/report-document.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { formatLocalCallTimestamp } from "../src/lib/format-utils.js"; -import { padTableColumns } from "../src/lib/markdown-table.js"; +import { fitTableToWidth, padTableColumns } from "../src/lib/markdown-table.js"; import { commandHeading, isReportDocument, @@ -150,6 +150,64 @@ describe("report-document", () => { }); }); + describe("fitTableToWidth", () => { + const table = { + headers: ["Model", "Tok", "Cost"], + fullHeaders: ["Model", "Tokens", "Cost"], + rows: [ + ["gpt-5", "1.0K", "$1.23"], + ["claude-opus", "20K", "$10.00"], + ], + aligns: ["left", "right", "right"] as Array<"left" | "right">, + }; + // Natural widths: full labels 11 + 2 + 6 + 2 + 6 = 27, compact labels 11 + 2 + 4 + 2 + 6 = 25. + + it("uses the full labels and spreads the spare width over the gaps", () => { + const out = fitTableToWidth({ ...table, width: 40 }); + // 13 spare columns over 2 gaps: the left gap takes the odd one. + expect(out).toEqual({ + header: `Model ${" ".repeat(9)}Tokens${" ".repeat(8)} Cost`, + rows: [ + `gpt-5 ${" ".repeat(9)} 1.0K${" ".repeat(8)} $1.23`, + `claude-opus${" ".repeat(9)} 20K${" ".repeat(8)}$10.00`, + ], + }); + for (const line of [out.header, ...out.rows]) expect(line).toHaveLength(40); + }); + + it("uses the full labels at exactly their natural width", () => { + const out = fitTableToWidth({ ...table, width: 27 }); + expect(out.header).toBe("Model Tokens Cost"); + expect(out.rows).toEqual(["gpt-5 1.0K $1.23", "claude-opus 20K $10.00"]); + }); + + it("falls back to the compact labels when the full ones do not fit", () => { + const out = fitTableToWidth({ ...table, width: 26 }); + expect(out).toEqual({ + header: "Model Tok Cost", + rows: ["gpt-5 1.0K $1.23", "claude-opus 20K $10.00"], + }); + for (const line of [out.header, ...out.rows]) expect(line).toHaveLength(26); + }); + + it("keeps the natural compact layout when even the compact labels do not fit", () => { + const natural = padTableColumns(table); + expect(fitTableToWidth({ ...table, width: 24 })).toEqual({ + header: natural.header.join(" "), + rows: natural.rows.map((row) => row.join(" ")), + }); + }); + + it("stretches a table without full labels using its labels", () => { + const out = fitTableToWidth({ ...table, fullHeaders: undefined, width: 30 }); + expect(out.header).toBe("Model Tok Cost"); + expect(out.rows).toEqual([ + "gpt-5 1.0K $1.23", + "claude-opus 20K $10.00", + ]); + }); + }); + it("keeps only sections with a title or a block that has content", () => { const document: ReportDocument = { sections: [ @@ -212,6 +270,7 @@ describe("report-document", () => { aligns: ["right"], widthMode: "markdown-conceal", }, + { kind: "table", headers: ["Tok"], fullHeaders: ["Tokens"], rows: [], aligns: [] }, ], }, ], @@ -245,6 +304,16 @@ describe("report-document", () => { }, ], }, + { + sections: [ + { + id: "s", + blocks: [ + { kind: "table", headers: ["A"], fullHeaders: ["A", "B"], rows: [], aligns: [] }, + ], + }, + ], + }, ]) { expect(isReportDocument(value)).toBe(false); } diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index f3a336c6..e2ff5720 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -4,8 +4,9 @@ import { join } from "node:path"; import { TextAttributes } from "@opentui/core"; import { beforeEach, describe, expect, it, vi } from "vitest"; +const terminal = vi.hoisted(() => ({ width: 120, height: 40 })); vi.mock("@opentui/solid", () => ({ - useTerminalDimensions: () => () => ({ width: 120, height: 40 }), + useTerminalDimensions: () => () => ({ ...terminal }), })); const loadConfig = vi.hoisted(() => vi.fn()); vi.mock("../src/lib/config.js", async (importOriginal) => { @@ -166,6 +167,7 @@ function startTui( describe("V2 quota TUI commands", () => { beforeEach(() => { + terminal.width = 120; rpc.command.mockReset().mockResolvedValue({ state: "output", command: "tokens_today", @@ -648,11 +650,12 @@ describe("V2 quota TUI commands", () => { blocks: [ { kind: "table", - headers: ["Model", "Cost"], - aligns: ["left", "right"], + headers: ["Model", "Tok", "Cost"], + fullHeaders: ["Model", "Tokens", "Cost"], + aligns: ["left", "right", "right"], rows: [ - ["gpt-5", "$1.23"], - ["claude-\u001b[31mopus", "$10.00"], + ["gpt-5", "1.0K", "$1.23"], + ["claude-\u001b[31mopus", "20K", "$10.00"], ], }, { kind: "kv", rows: [{ key: "enabled", value: "true" }] }, @@ -694,11 +697,30 @@ describe("V2 quota TUI commands", () => { })); // Section titles are bold base, table headers bold accent, and values base. A key-value // row is a muted "- key:" with a base value; plain lines are muted. + // A 120-column terminal fits the whole 116-column dialog; minus its paddings and the + // scrollbar, a table spans 111 columns. It fits with its full labels (27 columns), so its + // two gaps share the 84 spare columns. + const gap = " ".repeat(44); expect(texts).toEqual([ { children: "Top Models", fg: "base", bold: true, wrapMode: undefined }, - { children: "Model Cost", fg: "accent", bold: true, wrapMode: "none" }, - { children: "gpt-5 $1.23", fg: "base", bold: false, wrapMode: "none" }, - { children: "claude-opus $10.00", fg: "base", bold: false, wrapMode: "none" }, + { + children: `Model ${gap}Tokens${gap} Cost`, + fg: "accent", + bold: true, + wrapMode: "none", + }, + { + children: `gpt-5 ${gap} 1.0K${gap} $1.23`, + fg: "base", + bold: false, + wrapMode: "none", + }, + { + children: `claude-opus${gap} 20K${gap}$10.00`, + fg: "base", + bold: false, + wrapMode: "none", + }, { children: [ "- enabled:", @@ -741,6 +763,49 @@ describe("V2 quota TUI commands", () => { expect(context.ui.dialog.clear).toHaveBeenCalledOnce(); }); + it("lays popup tables out again for the terminal width, with compact labels when narrow", async () => { + stubRenderingReact(); + rpc.command.mockResolvedValue({ + state: "output", + command: "tokens_today", + title: "Tokens", + output: "the chat text", + document: { + sections: [ + { + id: "models", + blocks: [ + { + kind: "table", + headers: ["Model", "Tok", "Cost"], + fullHeaders: ["Model", "Tokens", "Cost"], + aligns: ["left", "right", "right"], + rows: [["claude-opus", "20K", "$10.00"]], + }, + ], + }, + ], + }, + dialogSize: "xlarge", + }); + const { context, command } = startTui(); + await command("tokens_today").run(); + const render = context.ui.dialog.show.mock.calls[0][0] as () => unknown; + const tableLines = () => + findNodes(findNode(render(), "scrollbox")?.props.children, "text").map( + (node) => node.props.children as string, + ); + + // 33 columns: the dialog is 31 wide and a table 26; the full labels (27) do not fit, + // the compact ones (24) do, and the two gaps share the 2 spare columns. + terminal.width = 33; + expect(tableLines()).toEqual(["Model Tok Cost", "claude-opus 20K $10.00"]); + // 30 columns: a table gets 23; even the compact labels do not fit, so it keeps its + // natural layout. + terminal.width = 30; + expect(tableLines()).toEqual(["Model Tok Cost", "claude-opus 20K $10.00"]); + }); + it("shows every command's report title once, as the dialog title, with the subtitle under it", async () => { stubRenderingReact(); const generatedAtMs = Date.UTC(2026, 8, 29, 14, 0); From 37b1695998bfcd522f0ff810c8cc5ca8ea7072dd Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:58:17 +0200 Subject: [PATCH 93/97] feat(tui): lay out the /quota popup in aligned columns --- src/lib/quota-columns.ts | 150 ++++++++++++++++++++++++++ src/lib/quota-command-format.ts | 42 ++++++-- src/lib/report-document.ts | 41 +++++++ src/tui-v2.tsx | 83 ++++++++++++-- tests/quota-columns.test.ts | 140 ++++++++++++++++++++++++ tests/quota-command-format.test.ts | 47 ++++++++ tests/report-document.test.ts | 16 +++ tests/tui-v2-commands.test.ts | 84 +++++++++++++++ tests/v4-phase5-cross-surface.test.ts | 19 ++-- 9 files changed, 597 insertions(+), 25 deletions(-) create mode 100644 src/lib/quota-columns.ts create mode 100644 tests/quota-columns.test.ts diff --git a/src/lib/quota-columns.ts b/src/lib/quota-columns.ts new file mode 100644 index 00000000..7294d4af --- /dev/null +++ b/src/lib/quota-columns.ts @@ -0,0 +1,150 @@ +/** + * The TUI dialog's column layout for /quota rows. The chat text keeps its own layout + * (see buildQuotaCommandDocument); this one only draws the same rows in the dialog. + */ + +import { bar, wrapDisplayText } from "./format-utils.js"; +import { QUOTA_COMMAND_BAR_WIDTH } from "./quota-command-format.js"; +import type { ReportQuotaRow } from "./report-document.js"; + +/** + * The widest the rows get: about the width of OpenCode's large dialog (88), an easy line + * to read. A typical full row (a 15-column label, a 24-cell bar, "100% left", "30.4/200", + * "reset 23d 23h 59m") fits it, and the xlarge dialog keeps the rest as even margins. + */ +export const QUOTA_COLUMNS_MAX_WIDTH = 84; +/** The longest bar; a longer one only stretches the eye between a label and its percent. */ +export const QUOTA_COLUMNS_BAR_MAX = 24; +/** The rows sit this far in from their provider title, as in the chat text. */ +const ROW_INDENT = 2; +const GAP = 2; + +/** A run of a laid-out line. Labels, reset times, and notes are muted; the rest is base. */ +export type QuotaLineSegment = { text: string; muted: boolean }; + +type Column = { id: "label" | "bar" | "value" | "usage" | "reset"; start: number; width: number }; + +export type QuotaColumns = { + /** Blank columns left of the rows, which center them in the dialog. */ + indent: number; + /** The rows' width; each row ends at this column. */ + width: number; + columns: Column[]; +}; + +/** + * Fits the columns of every /quota row in a report into `availableWidth`: label, bar, + * percent, used/limit, and reset. The rows take at most QUOTA_COLUMNS_MAX_WIDTH columns, + * centered. The bar grows up to QUOTA_COLUMNS_BAR_MAX cells and the spare width is spread + * over the gaps between columns (at least two spaces each, the leftmost gaps taking any + * remainder). A report without percent rows has no bar or percent columns; its value + * column takes the free width instead. Returns undefined when the bar (or that value + * column) would be shorter than the chat's bar, so the dialog keeps the chat layout. + */ +export function fitQuotaColumns( + rows: ReportQuotaRow[], + availableWidth: number, +): QuotaColumns | undefined { + const width = Math.min(QUOTA_COLUMNS_MAX_WIDTH, availableWidth); + const widest = (texts: Array<string | undefined>) => + Math.max(0, ...texts.map((text) => text?.length ?? 0)); + const percentRows = rows.filter((row) => row.barPercent !== undefined); + // The column that takes the free width: the bar, or the value when no row has a bar. + const flexId: Column["id"] = percentRows.length > 0 ? "bar" : "value"; + const candidates: Array<Omit<Column, "start">> = [ + { id: "label", width: widest(rows.map((row) => row.label)) }, + { id: flexId, width: 0 }, + ...(flexId === "bar" + ? [{ id: "value" as const, width: widest(percentRows.map((row) => row.value)) }] + : []), + { id: "usage", width: widest(rows.map((row) => row.usage)) }, + { id: "reset", width: widest(rows.map((row) => row.reset)) }, + ]; + const sized = candidates.filter((column) => column.id === flexId || column.width > 0); + + const gapCount = sized.length - 1; + const used = ROW_INDENT + sized.reduce((sum, column) => sum + column.width, 0) + GAP * gapCount; + const flexWidth = flexId === "bar" ? Math.min(QUOTA_COLUMNS_BAR_MAX, width - used) : width - used; + if (flexWidth < QUOTA_COMMAND_BAR_WIDTH) return undefined; + + const spare = width - used - flexWidth; + let start = ROW_INDENT; + const columns = sized.map((column, i) => { + const placed = { ...column, start, width: column.id === flexId ? flexWidth : column.width }; + start += placed.width + GAP + Math.floor(spare / gapCount) + (i < spare % gapCount ? 1 : 0); + return placed; + }); + return { indent: Math.floor((availableWidth - width) / 2), width, columns }; +} + +/** + * Lays out one row in the columns: the label left, then the bar, the percent, used/limit, + * and reset in their columns, numbers right-aligned. A value row's text is right-aligned in + * the columns from the bar's up to the next part the row shows (to the rows' right edge when + * there is none), and wraps there when it is too long. The notes follow, muted, from the + * bar's column, wrapped at the rows' right edge. + */ +export function layoutQuotaRow(row: ReportQuotaRow, fit: QuotaColumns): QuotaLineSegment[][] { + const flexStart = fit.columns[1].start; + const cellText = (column: Column): string | undefined => { + switch (column.id) { + case "label": + return row.label; + case "bar": + return row.barPercent === undefined ? undefined : bar(row.barPercent, column.width); + case "value": + return row.barPercent === undefined ? undefined : row.value; + case "usage": + case "reset": + return row[column.id]; + } + }; + const parts: Array<{ start: number; text: string; muted: boolean }> = []; + let valueEnd = fit.width; + for (const [index, column] of fit.columns.entries()) { + const text = cellText(column); + if (!text) continue; + if (index > 0 && parts.length === 1) { + valueEnd = fit.columns[index - 1].start + fit.columns[index - 1].width; + } + const pad = column.id === "value" || column.id === "usage" ? column.width - text.length : 0; + parts.push({ + start: column.start + pad, + text, + muted: column.id === "label" || column.id === "reset", + }); + } + + const valueLines = + row.barPercent !== undefined + ? [] + : row.value.length <= valueEnd - flexStart + ? [row.value] + : wrapDisplayText(row.value, valueEnd - flexStart); + if (valueLines.length > 0) { + parts.push({ start: valueEnd - valueLines[0].length, text: valueLines[0], muted: false }); + } + const lines: QuotaLineSegment[][] = [toSegments(parts)]; + for (const line of valueLines.slice(1)) { + lines.push([{ text: " ".repeat(valueEnd - line.length) + line, muted: false }]); + } + for (const note of row.notes) { + for (const line of wrapDisplayText(note, fit.width - flexStart)) { + lines.push([{ text: " ".repeat(flexStart) + line, muted: true }]); + } + } + return lines; +} + +/** Joins placed parts into one line, filling the columns between them with spaces. */ +function toSegments( + parts: Array<{ start: number; text: string; muted: boolean }>, +): QuotaLineSegment[] { + const segments: QuotaLineSegment[] = []; + let at = 0; + for (const part of [...parts].sort((a, b) => a.start - b.start)) { + segments.push({ text: " ".repeat(part.start - at) + part.text, muted: part.muted }); + at = part.start + part.text.length; + } + return segments; +} diff --git a/src/lib/quota-command-format.ts b/src/lib/quota-command-format.ts index 00200201..08034e45 100644 --- a/src/lib/quota-command-format.ts +++ b/src/lib/quota-command-format.ts @@ -27,6 +27,7 @@ import { classifyQuotaWindowText, type QuotaWindowKind } from "./quota-entry-dis import { formatQuotaRunway } from "./quota-exhaustion-projection.js"; import { type ReportDocument, + type ReportQuotaRow, type ReportSection, renderPlainTextReport, } from "./report-document.js"; @@ -114,15 +115,13 @@ function formatCommandDetails( return ` | ${details.join(" | ")}`; } -function getCommandBasisLines(basis: AccountingRowInterpretation["basis"]): string[] { +function getCommandBasisDetails(basis: AccountingRowInterpretation["basis"]): string[] { if (!basis) return []; - const details = - basis.kind === "detailed" - ? basis.facts.map((fact) => fact.text) - : basis.text - ? [basis.text] - : []; - return details.map((detail) => ` ${detail}`); + return basis.kind === "detailed" + ? basis.facts.map((fact) => fact.text) + : basis.text + ? [basis.text] + : []; } export function buildQuotaCommandDocument(params: { @@ -139,6 +138,7 @@ export function buildQuotaCommandDocument(params: { const sections: ReportSection[] = groups.map((group, index) => { const lines: string[] = []; + const rows: ReportQuotaRow[] = []; const interpretedRows = group.entries.map((entry) => ({ entry, interpretation: interpretAccountingRow(entry, { @@ -162,11 +162,22 @@ export function buildQuotaCommandDocument(params: { ), ); for (const { entry: row, interpretation } of interpretedRows) { - const label = padRight(getCommandMetricLabel(row, interpretation.label), labelWidth); + const metricLabel = getCommandMetricLabel(row, interpretation.label); + const label = padRight(metricLabel, labelWidth); const details = formatCommandDetails(row, rightWidth, params.resetTimeSpaced); + // The RPC output must be plain JSON, so absent parts are left out, never undefined. + const usage = row.right?.trim(); + const reset = formatCommandReset(row.resetTimeIso, params.resetTimeSpaced); + const usageAndReset = { ...(usage ? { usage } : {}), ...(reset ? { reset } : {}) }; if (interpretation.display.kind === "value") { lines.push(` ${label} ${interpretation.display.text}${details}`); + rows.push({ + label: metricLabel, + value: interpretation.display.text, + ...usageAndReset, + notes: [], + }); continue; } @@ -182,12 +193,21 @@ export function buildQuotaCommandDocument(params: { lines.push( ` ${label} ${bar(displayedPercent, QUOTA_COMMAND_BAR_WIDTH)} ${padLeft(pctLabel, Math.max(9, pctLabel.length))}${details}`, ); - lines.push(...getCommandBasisLines(interpretation.basis)); + const basisDetails = getCommandBasisDetails(interpretation.basis); + lines.push(...basisDetails.map((detail) => ` ${detail}`)); + const runway = isPercentEntry(row) ? formatQuotaRunway(row.runway) : ""; + rows.push({ + label: metricLabel, + barPercent: displayedPercent, + value: pctLabel, + ...usageAndReset, + notes: [...(runway ? [`Runs out ${runway}`] : []), ...basisDetails], + }); } return { id: `group-${index}`, title: `→ ${formatGroupedHeader(group.group)}`, - blocks: [{ kind: "lines", lines }], + blocks: [{ kind: "quota", lines, rows }], }; }); diff --git a/src/lib/report-document.ts b/src/lib/report-document.ts index d79ec88d..e85d1630 100644 --- a/src/lib/report-document.ts +++ b/src/lib/report-document.ts @@ -17,8 +17,28 @@ export type ReportKvRow = { trailingColon?: boolean; }; +/** + * One /quota row, split into the parts the TUI dialog lays out in columns. A percent row has + * `barPercent` and its percent label as `value`; a value row has only its text as `value`. + */ +export type ReportQuotaRow = { + label: string; + /** The displayed percent the bar fills, 0-100 (a percent row only). */ + barPercent?: number; + /** The percent label, e.g. "85% left", or a value row's text, e.g. "USD 0.00". */ + value: string; + /** Used over limit, e.g. "30.4/200". */ + usage?: string; + /** The reset countdown, e.g. "reset 1d 8h 18m". */ + reset?: string; + /** Lines under the row: the run-out projection and the accounting basis. */ + notes: string[]; +}; + export type ReportBlock = | { kind: "lines"; lines: string[] } + /** /quota rows: the text shows `lines`, the TUI dialog lays out `rows` in columns. */ + | { kind: "quota"; lines: string[]; rows: ReportQuotaRow[] } | { kind: "kv"; rows: ReportKvRow[] } | { kind: "table"; @@ -44,6 +64,7 @@ export type ReportDocument = { function hasBlockContent(block: ReportBlock): boolean { switch (block.kind) { case "lines": + case "quota": return block.lines.length > 0; case "kv": return block.rows.length > 0; @@ -103,11 +124,29 @@ function isReportKvRow(value: unknown): boolean { ); } +function isReportQuotaRow(value: unknown): boolean { + return ( + isObject(value) && + typeof value.label === "string" && + isOptional(value.barPercent, "number") && + typeof value.value === "string" && + isOptional(value.usage, "string") && + isOptional(value.reset, "string") && + isStringArray(value.notes) + ); +} + function isReportBlock(value: unknown): boolean { if (!isObject(value)) return false; switch (value.kind) { case "lines": return isStringArray(value.lines); + case "quota": + return ( + isStringArray(value.lines) && + Array.isArray(value.rows) && + value.rows.every(isReportQuotaRow) + ); case "kv": return Array.isArray(value.rows) && value.rows.every(isReportKvRow); case "table": @@ -167,6 +206,7 @@ export function renderKvRow(row: ReportKvRow): string { function renderPlainTextBlock(block: ReportBlock): string[] { switch (block.kind) { case "lines": + case "quota": return block.lines; case "kv": return block.rows.map(renderKvRow); @@ -185,6 +225,7 @@ function renderPlainTextBlock(block: ReportBlock): string[] { function renderMarkdownBlock(block: ReportBlock): string[] { switch (block.kind) { case "lines": + case "quota": return block.lines; case "kv": return block.rows.map(renderKvRow); diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 24377a8c..75e67bc6 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -9,6 +9,7 @@ import { loadConfig } from "./lib/config.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { fitTableToWidth } from "./lib/markdown-table.js"; +import { fitQuotaColumns, layoutQuotaRow, type QuotaColumns } from "./lib/quota-columns.js"; import { parseQuotaSlashCommand, QUOTA_DIALOG_COMMANDS, @@ -18,6 +19,7 @@ import { readQuotaReportMetadata } from "./lib/quota-report-message.js"; import { type ReportBlock, type ReportDocument, + type ReportQuotaRow, renderableSections, renderKvRow, } from "./lib/report-document.js"; @@ -295,21 +297,63 @@ function reportFailure(error: unknown): void { /** OpenCode's dialog widths (dialogWidth in its ui/dialog.tsx). */ const DIALOG_WIDTHS: Record<DialogSize, number> = { medium: 60, large: 88, xlarge: 116 }; +function sanitizeQuotaRow(row: ReportQuotaRow): ReportQuotaRow { + return { + ...row, + label: sanitizeDisplayText(row.label), + value: sanitizeDisplayText(row.value), + usage: row.usage === undefined ? undefined : sanitizeDisplayText(row.usage), + reset: row.reset === undefined ? undefined : sanitizeDisplayText(row.reset), + notes: row.notes.map(sanitizeDisplayText), + }; +} + +/** The /quota rows of a report, sanitized, for fitQuotaColumns and layoutQuotaRow. */ +function quotaRows(block: ReportBlock): ReportQuotaRow[] { + return block.kind === "quota" ? block.rows.map(sanitizeQuotaRow) : []; +} + /** * One block of a report. Lines and key-value rows wrap at words. A table keeps each row on * one line and spans `width` columns when it fits (see fitTableToWidth), with the header - * row bold in OpenCode's primary accent (its text color for a selected tab). Like OpenCode's - * sidebar, labels are muted and values are base. + * row bold in OpenCode's primary accent (its text color for a selected tab). /quota rows + * sit in `quotaColumns` when the report's rows fit them, else they show their chat lines. + * Like OpenCode's sidebar, labels are muted and values are base. */ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme; width: number; + quotaColumns: QuotaColumns | undefined; }): JSX.Element { const block = props.block; if (block.kind === "lines") { return <text fg={props.theme.text.muted}>{sanitizeDisplayText(block.lines.join("\n"))}</text>; } + if (block.kind === "quota") { + // Read in the JSX, so the rows are laid out again when the terminal is resized. + return ( + <box flexDirection="column"> + {props.quotaColumns ? ( + quotaRows(block) + .flatMap((row) => layoutQuotaRow(row, props.quotaColumns!)) + .map((line) => ( + <text fg={props.theme.text.base} wrapMode="none"> + {line.map((segment) => ( + <span + style={{ fg: segment.muted ? props.theme.text.muted : props.theme.text.base }} + > + {segment.text} + </span> + ))} + </text> + )) + ) : ( + <text fg={props.theme.text.muted}>{sanitizeDisplayText(block.lines.join("\n"))}</text> + )} + </box> + ); + } if (block.kind === "kv") { return ( <box flexDirection="column"> @@ -356,26 +400,49 @@ function ReportBlockView(props: { /** * Draws a report document's sections with the same spacing as its plain text: one blank row * between the sections and between the blocks of a section, and a section title directly - * above its first block. Section titles are bold. The dialog shows the heading's subtitle - * under its own title, so the heading line is left out. + * above its first block. Section titles are bold; a /quota provider title is in OpenCode's + * primary accent. When the /quota rows fit their columns (see fitQuotaColumns), the whole + * report takes the columns' width, centered, so its other lines wrap there too. The dialog + * shows the heading's subtitle under its own title, so the heading line is left out. */ function ReportDocumentView(props: { document: ReportDocument; theme: DialogTheme; width: number; }): JSX.Element { + const sections = renderableSections(props.document); + const rows = sections.flatMap((section) => section.blocks.flatMap(quotaRows)); + // Read in the JSX, so the columns are fitted again when the terminal is resized. + const columns = () => (rows.length > 0 ? fitQuotaColumns(rows, props.width) : undefined); return ( - <box flexDirection="column" gap={1}> - {renderableSections(props.document).map((section) => ( + <box + flexDirection="column" + gap={1} + paddingLeft={columns()?.indent} + width={columns() ? columns()!.indent + columns()!.width : undefined} + > + {sections.map((section) => ( <box flexDirection="column"> {section.title ? ( - <text attributes={TextAttributes.BOLD} fg={props.theme.text.base}> + <text + attributes={TextAttributes.BOLD} + fg={ + section.blocks.some((block) => block.kind === "quota") + ? props.theme.text.action.primary.selected + : props.theme.text.base + } + > {sanitizeDisplayText(section.title)} </text> ) : null} <box flexDirection="column" gap={1}> {section.blocks.map((block) => ( - <ReportBlockView block={block} theme={props.theme} width={props.width} /> + <ReportBlockView + block={block} + theme={props.theme} + width={props.width} + quotaColumns={columns()} + /> ))} </box> </box> diff --git a/tests/quota-columns.test.ts b/tests/quota-columns.test.ts new file mode 100644 index 00000000..04632606 --- /dev/null +++ b/tests/quota-columns.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from "vitest"; + +import { + fitQuotaColumns, + layoutQuotaRow, + QUOTA_COLUMNS_BAR_MAX, + QUOTA_COLUMNS_MAX_WIDTH, + type QuotaColumns, +} from "../src/lib/quota-columns.js"; +import type { ReportQuotaRow } from "../src/lib/report-document.js"; + +const copilot: ReportQuotaRow = { + label: "Quota", + barPercent: 85, + value: "85% left", + usage: "30.4/200", + reset: "reset 1d 8h 18m", + notes: [], +}; +const fiveHour: ReportQuotaRow = { + label: "5h quota", + barPercent: 100, + value: "100% left", + reset: "reset 5h 0m", + notes: [], +}; +const week: ReportQuotaRow = { + label: "Week quota", + barPercent: 50, + value: "50% left", + reset: "reset 3d 18h 21m", + notes: ["Runs out ≈ 2d 3h"], +}; +const spend: ReportQuotaRow = { label: "Monthly spend", value: "USD 0.00", notes: [] }; + +/** A row's lines as plain text, each with the columns its muted runs cover. */ +function draw(row: ReportQuotaRow, fit: QuotaColumns): string[] { + return layoutQuotaRow(row, fit).map((line) => line.map((segment) => segment.text).join("")); +} + +describe("quota columns", () => { + it("caps the rows' width and centers them in a wider dialog", () => { + const fit = fitQuotaColumns([copilot, fiveHour, week, spend], 111)!; + + expect(QUOTA_COLUMNS_MAX_WIDTH).toBe(84); + expect(fit.width).toBe(84); + // 111 - 84 = 27 spare columns: 13 on the left, 14 on the right. + expect(fit.indent).toBe(13); + }); + + it("caps the bar and spreads the spare width over the gaps", () => { + const fit = fitQuotaColumns([fiveHour, week], 111)!; + + // A 2-column row indent, label 10, percent 9, reset 16, and three 2-column gaps leave + // 41 of the 84 columns: the bar takes its 24 and the three gaps share the other 17. + expect(fit.columns).toEqual([ + { id: "label", start: 2, width: 10 }, + { id: "bar", start: 20, width: QUOTA_COLUMNS_BAR_MAX }, + { id: "value", start: 52, width: 9 }, + { id: "reset", start: 68, width: 16 }, + ]); + expect(draw(fiveHour, fit)).toEqual([ + ` 5h quota ${"█".repeat(24)} 100% left reset 5h 0m`, + ]); + }); + + it("aligns the columns: label left, numbers right, reset left, notes under the bar", () => { + const fit = fitQuotaColumns([copilot, fiveHour, week, spend], 111)!; + const lines = [copilot, fiveHour, week].flatMap((row) => draw(row, fit)); + + expect(lines).toEqual([ + ` Quota ${"█".repeat(20)}${"░".repeat(4)} 85% left 30.4/200 reset 1d 8h 18m`, + ` 5h quota ${"█".repeat(24)} 100% left reset 5h 0m`, + ` Week quota ${"█".repeat(12)}${"░".repeat(12)} 50% left reset 3d 18h 21m`, + " Runs out ≈ 2d 3h", + ]); + // Labels, reset times, and notes are muted; the bar, percent, and usage are base. + expect(layoutQuotaRow(copilot, fit)[0].map((segment) => segment.muted)).toEqual([ + true, + false, + false, + false, + true, + ]); + expect(layoutQuotaRow(week, fit)[1]).toEqual([ + { text: " Runs out ≈ 2d 3h", muted: true }, + ]); + }); + + it("right-aligns a value row's text at the rows' right edge, or before its reset", () => { + const withReset = { ...spend, label: "Plan", reset: "reset 3d 0h" }; + const fit = fitQuotaColumns([copilot, spend, withReset], 111)!; + + expect(draw(spend, fit)).toEqual([` Monthly spend${" ".repeat(61)}USD 0.00`]); + expect(draw(spend, fit)[0]).toHaveLength(fit.width); + // It ends where the usage column ends, like the percent rows' usage. + expect(draw(withReset, fit)).toEqual([` Plan${" ".repeat(52)}USD 0.00 reset 3d 0h`]); + expect(layoutQuotaRow(spend, fit)[0]).toEqual([ + { text: " Monthly spend", muted: true }, + { text: `${" ".repeat(61)}USD 0.00`, muted: false }, + ]); + }); + + it("wraps a long value row's text inside its columns", () => { + const plan: ReportQuotaRow = { + label: "Plan", + value: "Pro | quota details unavailable for this organization right now", + notes: [], + }; + const fit = fitQuotaColumns([fiveHour, plan], 60)!; + + expect(draw(plan, fit)).toEqual([ + " Plan Pro | quota details unavailable for this", + " organization right now", + ]); + }); + + it("gives a report without percent rows a value column that takes the free width", () => { + const balance = { ...spend, label: "Current balance" }; + const fit = fitQuotaColumns([spend, balance], 111)!; + + expect(fit.columns).toEqual([ + { id: "label", start: 2, width: 15 }, + { id: "value", start: 19, width: 65 }, + ]); + expect(draw(balance, fit)).toEqual([` Current balance${" ".repeat(59)}USD 0.00`]); + }); + + it("shrinks the bar on a narrower dialog and gives up below the chat's bar width", () => { + // Everything but the bar takes 2 + 5 + 8 + 8 + 15 + 4 gaps of 2 = 46 columns. + expect(fitQuotaColumns([copilot], 60)?.columns[1]).toEqual({ + id: "bar", + start: 9, + width: 14, + }); + expect(fitQuotaColumns([copilot], 56)?.columns[1].width).toBe(10); + expect(fitQuotaColumns([copilot], 55)).toBeUndefined(); + expect(fitQuotaColumns([spend], 24)).toBeUndefined(); + }); +}); diff --git a/tests/quota-command-format.test.ts b/tests/quota-command-format.test.ts index d4592881..c78d7913 100644 --- a/tests/quota-command-format.test.ts +++ b/tests/quota-command-format.test.ts @@ -553,4 +553,51 @@ describe("formatQuotaCommand", () => { expect(document.sections[0]?.title).toBe("→ [Copilot]"); } }); + + it("gives the dialog each row's parts beside the unchanged chat lines", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-15T12:00:00.000Z")); + const params = { + entries: [ + { + accounting: accounting("quota"), + name: "Copilot", + label: "Quota:", + right: "42/300", + percentRemaining: 86, + resetTimeIso: "2026-01-16T00:00:00.000Z", + runway: { kind: "lasts_past_reset" as const }, + }, + { + accounting: accounting("usage"), + name: "Copilot", + label: "Usage:", + kind: "value" as const, + value: "9 used", + }, + ], + errors: [], + }; + + const document = buildQuotaCommandDocument(params); + + const block = document.sections[0]?.blocks[0]; + expect(block?.kind).toBe("quota"); + if (block?.kind !== "quota") return; + // The text shows the lines, exactly as formatQuotaCommand prints them. + expect(formatQuotaCommand(params).split("\n").slice(3)).toEqual(block.lines); + expect(block.rows).toEqual([ + { + label: "Quota", + barPercent: 86, + value: "86% left", + usage: "42/300", + reset: "reset 12h0m", + notes: ["Runs out lasts past reset"], + }, + { label: "Usage", value: "9 used", notes: [] }, + ]); + // The RPC output must be plain JSON: no part is undefined. + expect(JSON.parse(JSON.stringify(block.rows))).toStrictEqual(block.rows); + }); }); diff --git a/tests/report-document.test.ts b/tests/report-document.test.ts index 7bd3013e..0c923c15 100644 --- a/tests/report-document.test.ts +++ b/tests/report-document.test.ts @@ -271,6 +271,14 @@ describe("report-document", () => { widthMode: "markdown-conceal", }, { kind: "table", headers: ["Tok"], fullHeaders: ["Tokens"], rows: [], aligns: [] }, + { + kind: "quota", + lines: [" Quota █░ 50% left"], + rows: [ + { label: "Quota", barPercent: 50, value: "50% left", notes: [] }, + { label: "Spend", value: "USD 0.00", usage: "1/2", reset: "reset 1h", notes: [] }, + ], + }, ], }, ], @@ -314,6 +322,14 @@ describe("report-document", () => { }, ], }, + { + sections: [ + { + id: "s", + blocks: [{ kind: "quota", lines: [], rows: [{ label: "Quota", value: "1%" }] }], + }, + ], + }, ]) { expect(isReportDocument(value)).toBe(false); } diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index e2ff5720..869d4a96 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -806,6 +806,90 @@ describe("V2 quota TUI commands", () => { expect(tableLines()).toEqual(["Model Tok Cost", "claude-opus 20K $10.00"]); }); + it("lays /quota rows out in centered columns, and falls back to the chat lines when narrow", async () => { + stubRenderingReact(); + const accounting = { + resultType: "quota", + acquisitionMethod: "remote_api", + ownership: "maintained", + authority: "provider_reported", + } as const; + const document = buildQuotaCommandDocument({ + entries: [ + { accounting, name: "Copilot", label: "Quota:", right: "30/200", percentRemaining: 85 }, + { + accounting: { ...accounting, resultType: "spend" }, + name: "OpenCode Zen", + label: "Monthly spend:", + kind: "value", + value: "USD 0.00", + }, + ], + errors: [{ label: "Z.ai", message: "Authentication expired" }], + }); + rpc.command.mockResolvedValue({ + state: "output", + command: "quota", + title: "Quota", + output: "the chat text", + document, + dialogSize: "xlarge", + }); + const { context, command } = startTui(); + await command("quota").run(); + const render = context.ui.dialog.show.mock.calls[0][0] as () => unknown; + const body = () => findNode(render(), "scrollbox")?.props.children; + const texts = () => + findNodes(body(), "text").map((node) => { + const children = node.props.children; + return { + text: Array.isArray(children) + ? children.map((span: Node) => span.props.children).join("") + : children, + fg: Array.isArray(children) + ? children.map((span: Node) => span.props.style.fg) + : node.props.fg, + bold: node.props.attributes === TextAttributes.BOLD, + }; + }); + + // 120 columns: the report gets 111; the rows take 84 of them, 13 blank columns left. + expect(body().props.paddingLeft).toBe(13); + expect(body().props.width).toBe(97); + // Provider titles are bold accent; labels muted, bar, percent, and values base. With + // no reset column, the three gaps share the spare 27 columns. + const bar = `${"█".repeat(20)}${"░".repeat(4)}`; + expect(texts()).toEqual([ + { text: "→ [Copilot]", fg: "accent", bold: true }, + { + text: ` Quota${" ".repeat(17)}${bar}${" ".repeat(11)}85% left${" ".repeat(11)}30/200`, + fg: ["muted", "base", "base", "base"], + bold: false, + }, + { text: "→ [OpenCode Zen]", fg: "accent", bold: true }, + { + text: ` Month spend${" ".repeat(63)}USD 0.00`, + fg: ["muted", "base"], + bold: false, + }, + { text: "Partial failures", fg: "base", bold: true }, + { text: " Z.ai: Authentication expired", fg: "muted", bold: false }, + ]); + + // 45 columns: the report gets 38, which leaves the bar 5 of the 10 cells it needs. + terminal.width = 45; + expect(body().props.paddingLeft).toBeUndefined(); + expect(body().props.width).toBeUndefined(); + expect(texts().slice(0, 2)).toEqual([ + { text: "→ [Copilot]", fg: "accent", bold: true }, + { + text: " Quota █████████░ 85% left | 30/200", + fg: "muted", + bold: false, + }, + ]); + }); + it("shows every command's report title once, as the dialog title, with the subtitle under it", async () => { stubRenderingReact(); const generatedAtMs = Date.UTC(2026, 8, 29, 14, 0); diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index c28bcf68..91115f8d 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -197,7 +197,7 @@ async function setupV2Surfaces(client: ReturnType<typeof createClient>, provider const editor = { plainText: "", clear: vi.fn() }; // Records the title, subtitle, and scrollbox text of each quota output dialog the TUI // shows. The scrollbox text is every text node in order, one per line, without the blank - // rows. + // rows; a text node made of spans is their text joined. const dialog = vi.fn((_input: { title: string; subtitle?: string; message: string }) => {}); type Node = { type: string; props: Record<string, any> }; const find = (node: unknown, type: string): Node | undefined => { @@ -210,9 +210,13 @@ async function setupV2Surfaces(client: ReturnType<typeof createClient>, provider const texts = (node: unknown): string[] => { if (Array.isArray(node)) return node.flatMap(texts); if (!node || typeof node !== "object") return []; - return (node as Node).type === "text" - ? [(node as Node).props.children] - : texts((node as Node).props?.children); + if ((node as Node).type !== "text") return texts((node as Node).props?.children); + const children = (node as Node).props.children; + return [ + Array.isArray(children) + ? children.map((span: Node) => span.props.children).join("") + : children, + ]; }; const show = (render: () => unknown, onClose?: () => void) => { const tree = render(); @@ -593,11 +597,14 @@ describe("v4 Phase 5 cross-surface release evidence", () => { expect(client.session.prompt).not.toHaveBeenCalled(); expect(serverOutput).not.toContain("```"); expect(serverOutput).not.toMatch(/^#{1,6} /mu); + // The dialog lays the rows out in columns, centered: a 120-column terminal leaves them + // 84 of 111 columns, 13 blank on the left. Every bar has the same length. expect(serverOutput).toMatch(/→ \[Team Accounting\]\n {2}Month quota/u); const serverBars = serverOutput.match(/[█░]+/gu) ?? []; expect(serverBars.length).toBeGreaterThan(0); - expect(serverBars.every((bar) => Array.from(bar).length === 10)).toBe(true); - expect(serverOutput).toMatch(/Month quota\s+[█░]{10}\s+64% left \| 64\/100 \| reset /); + expect(new Set(serverBars.map((bar) => Array.from(bar).length)).size).toBe(1); + expect(serverOutput).not.toMatch(/Month quota[^\n]* \| /u); + expect(serverOutput).toMatch(/Month quota\s+[█░]{10,24}\s+64% left\s+64\/100\s+reset /); expect(serverOutput).toMatch(/Balance\s+\$12\.34/); assertFixtureContent(serverOutput); assertTreeSessionTokenTotals(serverOutput); From f833e8fb301beea5365b1a516cca0d14a6b65813 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:01:17 +0200 Subject: [PATCH 94/97] fix(quota): only mark the active login when a provider has several --- README.md | 2 +- docs/readme/providers.md | 2 +- src/lib/opencode-auth.ts | 7 ++-- tests/lib.opencode-auth.test.ts | 39 ++++++++++++------- tests/providers.anthropic.test.ts | 10 ++--- tests/providers.copilot.surfaces.test.ts | 4 +- tests/providers.copilot.test.ts | 4 +- tests/providers.glm-coding-plan.test.ts | 2 +- tests/providers.google-db-credentials.test.ts | 5 ++- tests/providers.google-gemini-cli.test.ts | 2 +- tests/providers.kimi-code.test.ts | 2 +- tests/providers.minimax-coding-plan.test.ts | 2 +- tests/providers.openai.test.ts | 18 ++++----- tests/providers.opencode-go.test.ts | 15 ++++--- tests/providers.xai.surfaces.test.ts | 4 +- tests/providers.xai.test.ts | 2 +- tests/tui-v2-theme-colors.test.ts | 4 +- 17 files changed, 72 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index cb0052a7..856889d7 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ The updater always shows a preview first. `--yes` applies only the safe edits it More ways to use it: - **Terminal:** run `npx @slkiser/opencode-quota show` (works with OpenCode closed). In Web and Desktop, slash commands post the report in the chat, and the AI can call the `quota_status` tool. -- **Several logins:** each login for a provider gets its own rows; `*` marks the active one. +- **Several logins:** each login for a provider gets its own rows; `(active)` marks the one OpenCode uses. - **Scripts and CI:** JSON output and optional OpenTelemetry metrics. See [External integration](docs/readme/external-integration.md). - **Display:** a quota bar under the prompt ([`tuiPromptBar.enabled`](docs/readme/configuration.md#tui-settings)), OpenCode Go's collapsed-sidebar row ([`tuiSidebarPanel.opencodeGoPreferredWindow`](docs/readme/configuration.md#tui-settings)), reset countdown style ([`resetTimeSpaced`](docs/readme/configuration.md#common-changes), [`resetTimeDecimals`](docs/readme/configuration.md#common-changes)), bare `81%` labels ([`percentLabelStyle`](docs/readme/configuration.md#common-changes)), and extra accounting rows ([`accountingDetail`](docs/readme/configuration.md#show-accounting-detail)). - **Runs-out estimate:** [`quotaProjection: "runway"`](docs/readme/configuration.md#estimate-when-fixed-quota-runs-out) shows **Runs out ≈ 1h 50m** for supported fixed windows. Off by default; JSON is unchanged. diff --git a/docs/readme/providers.md b/docs/readme/providers.md index 79d7c276..57ddd408 100644 --- a/docs/readme/providers.md +++ b/docs/readme/providers.md @@ -12,7 +12,7 @@ ## Pre-configured providers -Most providers work automatically. `Automatic` means OpenCode Quota reuses the credential saved through OpenCode's `/connect`, read through OpenCode 2's plugin API. If you have several logins for one provider, each one gets its own rows and `*` marks the active one. If a provider has a “Needs setup” link, open that setup note only if you use that provider. Providers can appear in both audience sections when the vendor supports both. +Most providers work automatically. `Automatic` means OpenCode Quota reuses the credential saved through OpenCode's `/connect`, read through OpenCode 2's plugin API. If you have several logins for one provider, each one gets its own rows and `(active)` marks the one OpenCode uses. If a provider has a “Needs setup” link, open that setup note only if you use that provider. Providers can appear in both audience sections when the vendor supports both. ### American providers diff --git a/src/lib/opencode-auth.ts b/src/lib/opencode-auth.ts index 2343e093..59fa5095 100644 --- a/src/lib/opencode-auth.ts +++ b/src/lib/opencode-auth.ts @@ -143,8 +143,9 @@ export function formatCredentialDisplayNames( const numberedAlias = duplicate === 1 ? aliasKey : `${aliasKey} ${duplicate}`; const base = `[${providerName}${numberedAlias ? ` ${numberedAlias}` : ""}]`; const category = fallbackCategory ? ` (${fallbackCategory})` : ""; - const active = row.active || (credentials.length === 1 && !credentials[0]!.row.active); - return `${base}${category}${active ? "*" : ""}`; + // Only a provider with several logins marks the one OpenCode uses. + const active = credentials.length > 1 && row.active; + return `${base}${category}${active ? " (active)" : ""}`; }); } @@ -399,7 +400,7 @@ export function createIntegrationCredentialSource( const rows = await Promise.all( wanted.map((slot) => credentialRowForSlot(integration, slot, methods)), ); - // The `*` goes to the first row of the first requested id that has rows. + // The active mark goes to the first row of the first requested id that has rows. return rows.map((row, index) => ({ ...row, active: index === 0 })); }, }; diff --git a/tests/lib.opencode-auth.test.ts b/tests/lib.opencode-auth.test.ts index 308eaa1a..00ab549f 100644 --- a/tests/lib.opencode-auth.test.ts +++ b/tests/lib.opencode-auth.test.ts @@ -86,18 +86,18 @@ function resolvedIds(integration: ReturnType<typeof createFakeIntegration>): str describe("OpenCode auth reader", () => { it.each([ - ["OpenAI", "OpenAI (Pro)", "default", true, "[OpenAI] (Pro)*"], - ["OpenAI", "OpenAI (Pro)", "SEPD", true, "[OpenAI SEPD] (Pro)*"], - ["Copilot", "Copilot (business)", "sita", true, "[Copilot sita] (business)*"], - ["xAI", "xAI SuperGrok", "personal", true, "[xAI personal] (SuperGrok)*"], - ["OpenAI", "OpenAI (Pro)", " ", true, "[OpenAI] (Pro)*"], - ["OpenAI", "OpenAI (Pro)", "openai", true, "[OpenAI] (Pro)*"], + ["OpenAI", "OpenAI (Pro)", "default", true, "[OpenAI] (Pro)"], + ["OpenAI", "OpenAI (Pro)", "SEPD", true, "[OpenAI SEPD] (Pro)"], + ["Copilot", "Copilot (business)", "sita", true, "[Copilot sita] (business)"], + ["xAI", "xAI SuperGrok", "personal", true, "[xAI personal] (SuperGrok)"], + ["OpenAI", "OpenAI (Pro)", " ", true, "[OpenAI] (Pro)"], + ["OpenAI", "OpenAI (Pro)", "openai", true, "[OpenAI] (Pro)"], // OpenCode 2 labels credentials imported from auth.json "OAuth" or "API key". - ["OpenAI", "OpenAI (Pro)", "OAuth", true, "[OpenAI] (Pro)*"], - ["Copilot", "Copilot (individual)", "OAuth", true, "[Copilot] (individual)*"], - ["Z.ai", "Z.ai", "API key", true, "[Z.ai]*"], - ["MiniMax", "MiniMax", "api key", true, "[MiniMax]*"], - ["Kimi Code", "Kimi Code", "API key", true, "[Kimi Code]*"], + ["OpenAI", "OpenAI (Pro)", "OAuth", true, "[OpenAI] (Pro)"], + ["Copilot", "Copilot (individual)", "OAuth", true, "[Copilot] (individual)"], + ["Z.ai", "Z.ai", "API key", true, "[Z.ai]"], + ["MiniMax", "MiniMax", "api key", true, "[MiniMax]"], + ["Kimi Code", "Kimi Code", "API key", true, "[Kimi Code]"], ])("formats %s credential aliases as %s", (providerName, fallbackName, label, active, expected) => { expect( formatCredentialDisplayNames(providerName, [ @@ -109,6 +109,19 @@ describe("OpenCode auth reader", () => { ).toEqual([expected]); }); + it("marks no login when the provider has only one, active or not", () => { + for (const active of [true, false]) { + expect( + formatCredentialDisplayNames("OpenAI", [ + { + row: { id: "a", integrationId: "openai", label: "Work", active, value: {} }, + fallbackName: "OpenAI (Pro)", + }, + ]), + ).toEqual(["[OpenAI Work] (Pro)"]); + } + }); + it("numbers duplicate custom aliases without numbering default credentials", () => { expect( formatCredentialDisplayNames("OpenAI", [ @@ -125,7 +138,7 @@ describe("OpenCode auth reader", () => { fallbackName: "OpenAI (Pro)", }, ]), - ).toEqual(["[OpenAI SEPD] (Pro)*", "[OpenAI SEPD 2] (Pro)", "[OpenAI] (Pro)"]); + ).toEqual(["[OpenAI SEPD] (Pro) (active)", "[OpenAI SEPD 2] (Pro)", "[OpenAI] (Pro)"]); }); it("keeps a named connection next to an imported generic one", () => { @@ -140,7 +153,7 @@ describe("OpenCode auth reader", () => { fallbackName: "Z.ai", }, ]), - ).toEqual(["[Z.ai Work]*", "[Z.ai]"]); + ).toEqual(["[Z.ai Work] (active)", "[Z.ai]"]); }); it("reports the credential database path and honors OPENCODE_DB", () => { diff --git a/tests/providers.anthropic.test.ts b/tests/providers.anthropic.test.ts index 4c729c12..3736fc1f 100644 --- a/tests/providers.anthropic.test.ts +++ b/tests/providers.anthropic.test.ts @@ -66,10 +66,10 @@ describe("anthropic provider", () => { expect(queryAnthropicQuotaWithOAuth).toHaveBeenCalledWith("valid", undefined); expect(queryAnthropicQuota).not.toHaveBeenCalled(); expect(out.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ - ["[Claude Work]*", "valid-inactive"], - ["[Claude Work]*", "valid-inactive"], - ["[Claude Work]* Usage Credits", "valid-inactive"], - ["[Claude Work]*", "valid-inactive"], + ["[Claude Work]", "valid-inactive"], + ["[Claude Work]", "valid-inactive"], + ["[Claude Work] Usage Credits", "valid-inactive"], + ["[Claude Work]", "valid-inactive"], ]); }); @@ -114,7 +114,7 @@ describe("anthropic provider", () => { expect(queryAnthropicQuota).not.toHaveBeenCalled(); expect(out.errors).toEqual([ { - label: "[Claude]*", + label: "[Claude] (active)", message: "Anthropic sign-in could not be read: refresh_failed: HTTP 500. Run `opencode auth login anthropic`.", }, diff --git a/tests/providers.copilot.surfaces.test.ts b/tests/providers.copilot.surfaces.test.ts index 35f97cb4..bd098896 100644 --- a/tests/providers.copilot.surfaces.test.ts +++ b/tests/providers.copilot.surfaces.test.ts @@ -28,8 +28,8 @@ const data: QuotaRenderData = { describe("Copilot usage-only four-surface formatting", () => { it.each([ - ["[Copilot] (business)*"], - ["[Copilot sita] (business)*"], + ["[Copilot] (business) (active)"], + ["[Copilot sita] (business) (active)"], ])("preserves the credential group %s on command, toast, and sidebar", (group) => { const credentialData: QuotaRenderData = { ...data, diff --git a/tests/providers.copilot.test.ts b/tests/providers.copilot.test.ts index 870a173e..48806632 100644 --- a/tests/providers.copilot.test.ts +++ b/tests/providers.copilot.test.ts @@ -171,7 +171,7 @@ describe("copilot provider", () => { const out = await copilotProvider.fetch({} as any); - expect(visibleEntries(out.entries, "copilot")[0]?.group).toBe("[Copilot alice] (enterprise)*"); + expect(visibleEntries(out.entries, "copilot")[0]?.group).toBe("[Copilot alice] (enterprise)"); expect(out.statusDetails).toContainEqual({ key: "billing_scope", value: "user" }); }); @@ -225,7 +225,7 @@ describe("copilot provider", () => { { "github-copilot": { type: "oauth", resolveError: "refresh_failed: HTTP 401" } }, { "github-copilot": { type: "oauth", access: "bob-token" } }, ]); - expect(out.errors).toEqual([{ label: "[Copilot alice]*", message: signInError }]); + expect(out.errors).toEqual([{ label: "[Copilot alice] (active)", message: signInError }]); expect(visibleEntries(out.entries, "copilot")[0]?.group).toBe("[Copilot bob] (enterprise)"); }); diff --git a/tests/providers.glm-coding-plan.test.ts b/tests/providers.glm-coding-plan.test.ts index 0eb7a024..199bdaf9 100644 --- a/tests/providers.glm-coding-plan.test.ts +++ b/tests/providers.glm-coding-plan.test.ts @@ -243,7 +243,7 @@ describe("GLM database credential rows", () => { }); expect(out.errors).toEqual([ { - label: "[Z.ai Work]*", + label: "[Z.ai Work] (active)", message: "OpenCode could not read this login: refresh_failed: HTTP 401", }, ]); diff --git a/tests/providers.google-db-credentials.test.ts b/tests/providers.google-db-credentials.test.ts index 06cd8ebf..16eae847 100644 --- a/tests/providers.google-db-credentials.test.ts +++ b/tests/providers.google-db-credentials.test.ts @@ -107,7 +107,10 @@ describe.each([ expect(query).toHaveBeenCalledTimes(2); expect(output.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ - [`[${provider.id === "google-agy" ? "Google AGY" : "Gemini CLI"} Work]*`, "active-id"], + [ + `[${provider.id === "google-agy" ? "Google AGY" : "Gemini CLI"} Work] (active)`, + "active-id", + ], [`[${provider.id === "google-agy" ? "Google AGY" : "Gemini CLI"} Work 2]`, "other-id"], ]); }); diff --git a/tests/providers.google-gemini-cli.test.ts b/tests/providers.google-gemini-cli.test.ts index 8be92d23..199d1f85 100644 --- a/tests/providers.google-gemini-cli.test.ts +++ b/tests/providers.google-gemini-cli.test.ts @@ -225,7 +225,7 @@ describe("google gemini cli provider", () => { expect(out.attempted).toBe(true); expect(out.errors).toEqual([ { - label: "[Gemini CLI work@example.com]*", + label: "[Gemini CLI work@example.com] (active)", message: "Gemini CLI sign-in could not be refreshed: refresh_failed: HTTP 400. Run `opencode auth login google`.", }, diff --git a/tests/providers.kimi-code.test.ts b/tests/providers.kimi-code.test.ts index 7d68a6cb..a8399eb0 100644 --- a/tests/providers.kimi-code.test.ts +++ b/tests/providers.kimi-code.test.ts @@ -375,7 +375,7 @@ describe("Kimi regional providers", () => { expect(out.entries).toEqual([]); expect(out.errors).toEqual([ { - label: "[Kimi Code (CN) Work]*", + label: "[Kimi Code (CN) Work]", message: "OpenCode could not read this login: resolve_empty: no value", }, ]); diff --git a/tests/providers.minimax-coding-plan.test.ts b/tests/providers.minimax-coding-plan.test.ts index 6a4bbbc6..559be04b 100644 --- a/tests/providers.minimax-coding-plan.test.ts +++ b/tests/providers.minimax-coding-plan.test.ts @@ -206,7 +206,7 @@ describe("minimax-coding-plan provider", () => { methods: ["key"], }); expect(out.errors).toContainEqual({ - label: "[MiniMax Token Plan shared]*", + label: "[MiniMax Token Plan shared] (active)", message: "empty key", }); expect(out.entries).toContainEqual( diff --git a/tests/providers.openai.test.ts b/tests/providers.openai.test.ts index 76164972..d79a23ee 100644 --- a/tests/providers.openai.test.ts +++ b/tests/providers.openai.test.ts @@ -153,10 +153,10 @@ describe("openai provider", () => { "other-id", ]); expect(out.entries.map((entry) => [entry.group, entry.accounting.sourceId])).toEqual([ - ["[OpenAI Work] (Business)*", "active-id"], - ["[OpenAI Work] (Business)*", "active-id"], - ["[OpenAI Work] (Business)*", "active-id"], - ["[OpenAI Work] (Business)*", "active-id"], + ["[OpenAI Work] (Business) (active)", "active-id"], + ["[OpenAI Work] (Business) (active)", "active-id"], + ["[OpenAI Work] (Business) (active)", "active-id"], + ["[OpenAI Work] (Business) (active)", "active-id"], ["[OpenAI Work 2] (Business)", "other-id"], ["[OpenAI Work 2] (Business)", "other-id"], ["[OpenAI Work 2] (Business)", "other-id"], @@ -165,9 +165,9 @@ describe("openai provider", () => { }); it.each([ - ["OpenAI", "[OpenAI] (Business)*"], - ["SEPD", "[OpenAI SEPD] (Business)*"], - ["default", "[OpenAI] (Business)*"], + ["OpenAI", "[OpenAI] (Business)"], + ["SEPD", "[OpenAI SEPD] (Business)"], + ["default", "[OpenAI] (Business)"], ])("renders DB alias %s literally on sidebar, CLI, and toast", async (label, expected) => { const { readCredentialRows } = await import("../src/lib/opencode-auth.js"); const { queryOpenAIQuota, resolveOpenAIOAuth } = await import("../src/lib/openai.js"); @@ -204,7 +204,7 @@ describe("openai provider", () => { for (const output of outputs) { expect(output).toContain(expected); - expect(output).not.toContain("[OpenAI (OpenAI)*]"); + expect(output).not.toContain("[OpenAI (OpenAI)]"); } }); @@ -248,7 +248,7 @@ describe("openai provider", () => { }); expect(out.errors).toEqual([ { - label: "[OpenAI Work]*", + label: "[OpenAI Work] (active)", message: "OpenAI sign-in could not be refreshed: refresh_failed: HTTP 400. Run `opencode auth login openai`.", }, diff --git a/tests/providers.opencode-go.test.ts b/tests/providers.opencode-go.test.ts index ec03d17a..fb2e4dff 100644 --- a/tests/providers.opencode-go.test.ts +++ b/tests/providers.opencode-go.test.ts @@ -427,7 +427,10 @@ describe("opencode-go provider", () => { ); const out = await runFetch(); - expect(out.errors).toContainEqual({ label: "[OpenCode Go shared]*", message: "empty key" }); + expect(out.errors).toContainEqual({ + label: "[OpenCode Go shared] (active)", + message: "empty key", + }); expect(out.entries).toContainEqual( expect.objectContaining({ group: "[OpenCode Go shared 2]", @@ -461,7 +464,7 @@ describe("opencode-go provider", () => { const message = "OpenCode could not read this login: active_failed: database is locked"; expect(out.errors).toEqual([ - { label: "[OpenCode Go Personal]*", message }, + { label: "[OpenCode Go Personal] (active)", message }, { label: "[OpenCode Go Work]", message }, ]); expect(mocks.queryOpenCodeGoQuota).not.toHaveBeenCalled(); @@ -492,8 +495,8 @@ describe("opencode-go provider", () => { expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); expect(visibleEntries(out.entries, "opencode-go").map((entry) => entry.group)).toEqual([ - "[OpenCode Go]*", - "[OpenCode Go]*", + "[OpenCode Go]", + "[OpenCode Go]", ]); for (const entry of out.entries) { expect(entry.accounting).toMatchObject({ sourceId: "go-row" }); @@ -625,8 +628,8 @@ describe("opencode-go provider", () => { expect(mocks.queryOpenCodeGoQuota).toHaveBeenCalledOnce(); expect(visibleEntries(out.entries, "opencode-go").map((entry) => entry.group)).toEqual([ - "[OpenCode Go]*", - "[OpenCode Go]*", + "[OpenCode Go]", + "[OpenCode Go]", ]); for (const entry of out.entries) { expect(entry.accounting).toMatchObject({ sourceId: "alias-row" }); diff --git a/tests/providers.xai.surfaces.test.ts b/tests/providers.xai.surfaces.test.ts index cfeed28c..b0147952 100644 --- a/tests/providers.xai.surfaces.test.ts +++ b/tests/providers.xai.surfaces.test.ts @@ -30,8 +30,8 @@ function renderDataForLabel(label: (typeof xaiLabels)[number]): QuotaRenderData describe("xAI four-surface formatting", () => { it.each([ - ["[xAI] (SuperGrok)*"], - ["[xAI personal] (SuperGrok)*"], + ["[xAI] (SuperGrok) (active)"], + ["[xAI personal] (SuperGrok) (active)"], ])("preserves the credential group %s on command, toast, and sidebar", (group) => { const data: QuotaRenderData = { entries: [ diff --git a/tests/providers.xai.test.ts b/tests/providers.xai.test.ts index 57aa3c71..19903de9 100644 --- a/tests/providers.xai.test.ts +++ b/tests/providers.xai.test.ts @@ -146,7 +146,7 @@ describe("xai provider", () => { expect(output.entries).toEqual([]); expect(output.errors).toEqual([ { - label: "[xAI]*", + label: "[xAI]", message: "xAI sign-in could not be refreshed: refresh_failed: HTTP 401. Run `opencode auth login xai`.", }, diff --git a/tests/tui-v2-theme-colors.test.ts b/tests/tui-v2-theme-colors.test.ts index 1c8aa5dd..bfbd93cd 100644 --- a/tests/tui-v2-theme-colors.test.ts +++ b/tests/tui-v2-theme-colors.test.ts @@ -76,7 +76,7 @@ describe("V2 TUI theme colors", () => { }); it("draws the sidebar like OpenCode's Context section: bold base titles, muted body", () => { - const message = ["[Copilot] (individual)*", "Premium 5h 80%", "", "Session tokens"].join( + const message = ["[Copilot] (individual)", "Premium 5h 80%", "", "Session tokens"].join( "\n", ); seeds.push(true, { message, duration: 5000, activeProviderCount: 1 }); @@ -86,7 +86,7 @@ describe("V2 TUI theme colors", () => { expect(texts.map(describeText)).toEqual([ { text: "▼", fg: "base", bold: false }, { text: "Quota", fg: "base", bold: true }, - { text: "[Copilot] (individual)*", fg: "base", bold: true }, + { text: "[Copilot] (individual)", fg: "base", bold: true }, { text: "Premium 5h 80%", fg: "muted", bold: false }, { text: " ", fg: "muted", bold: false }, { text: "Session tokens", fg: "muted", bold: false }, From 62b66bea0d77f5efac49d88fa6cc2d38c6db5de8 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:11:58 +0200 Subject: [PATCH 95/97] fix(tui): start /quota popup values in one left-aligned column --- src/lib/quota-columns.ts | 23 ++++++++++---------- tests/quota-columns.test.ts | 41 ++++++++++++++++++++++++----------- tests/tui-v2-commands.test.ts | 7 ++++-- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/lib/quota-columns.ts b/src/lib/quota-columns.ts index 7294d4af..95e52e78 100644 --- a/src/lib/quota-columns.ts +++ b/src/lib/quota-columns.ts @@ -78,14 +78,16 @@ export function fitQuotaColumns( } /** - * Lays out one row in the columns: the label left, then the bar, the percent, used/limit, - * and reset in their columns, numbers right-aligned. A value row's text is right-aligned in - * the columns from the bar's up to the next part the row shows (to the rows' right edge when - * there is none), and wraps there when it is too long. The notes follow, muted, from the - * bar's column, wrapped at the rows' right edge. + * Lays out one row in the columns: the label, then the bar, the percent, used/limit, and + * reset, each left-aligned at its column's start. A value row's text starts in the percent + * column (the value column), runs up to the next part the row shows (to the rows' right + * edge when there is none), and wraps there when it is too long. The notes follow, muted, + * from the bar's column, wrapped at the rows' right edge. */ export function layoutQuotaRow(row: ReportQuotaRow, fit: QuotaColumns): QuotaLineSegment[][] { const flexStart = fit.columns[1].start; + // Every fit has a value column: the percent rows' labels, or the flex column without them. + const valueStart = fit.columns.find((column) => column.id === "value")!.start; const cellText = (column: Column): string | undefined => { switch (column.id) { case "label": @@ -107,9 +109,8 @@ export function layoutQuotaRow(row: ReportQuotaRow, fit: QuotaColumns): QuotaLin if (index > 0 && parts.length === 1) { valueEnd = fit.columns[index - 1].start + fit.columns[index - 1].width; } - const pad = column.id === "value" || column.id === "usage" ? column.width - text.length : 0; parts.push({ - start: column.start + pad, + start: column.start, text, muted: column.id === "label" || column.id === "reset", }); @@ -118,15 +119,15 @@ export function layoutQuotaRow(row: ReportQuotaRow, fit: QuotaColumns): QuotaLin const valueLines = row.barPercent !== undefined ? [] - : row.value.length <= valueEnd - flexStart + : row.value.length <= valueEnd - valueStart ? [row.value] - : wrapDisplayText(row.value, valueEnd - flexStart); + : wrapDisplayText(row.value, valueEnd - valueStart); if (valueLines.length > 0) { - parts.push({ start: valueEnd - valueLines[0].length, text: valueLines[0], muted: false }); + parts.push({ start: valueStart, text: valueLines[0], muted: false }); } const lines: QuotaLineSegment[][] = [toSegments(parts)]; for (const line of valueLines.slice(1)) { - lines.push([{ text: " ".repeat(valueEnd - line.length) + line, muted: false }]); + lines.push([{ text: " ".repeat(valueStart) + line, muted: false }]); } for (const note of row.notes) { for (const line of wrapDisplayText(note, fit.width - flexStart)) { diff --git a/tests/quota-columns.test.ts b/tests/quota-columns.test.ts index 04632606..1382d8cf 100644 --- a/tests/quota-columns.test.ts +++ b/tests/quota-columns.test.ts @@ -64,16 +64,23 @@ describe("quota columns", () => { ]); }); - it("aligns the columns: label left, numbers right, reset left, notes under the bar", () => { + it("left-aligns every column: label, percent, used/limit, reset, notes under the bar", () => { const fit = fitQuotaColumns([copilot, fiveHour, week, spend], 111)!; const lines = [copilot, fiveHour, week].flatMap((row) => draw(row, fit)); expect(lines).toEqual([ - ` Quota ${"█".repeat(20)}${"░".repeat(4)} 85% left 30.4/200 reset 1d 8h 18m`, + ` Quota ${"█".repeat(20)}${"░".repeat(4)} 85% left 30.4/200 reset 1d 8h 18m`, ` 5h quota ${"█".repeat(24)} 100% left reset 5h 0m`, - ` Week quota ${"█".repeat(12)}${"░".repeat(12)} 50% left reset 3d 18h 21m`, + ` Week quota ${"█".repeat(12)}${"░".repeat(12)} 50% left reset 3d 18h 21m`, " Runs out ≈ 2d 3h", ]); + // Every percent starts at the value column. + const valueStart = fit.columns.find((column) => column.id === "value")!.start; + expect(lines.slice(0, 3).map((line) => line.search(/\d+% left/))).toEqual([ + valueStart, + valueStart, + valueStart, + ]); // Labels, reset times, and notes are muted; the bar, percent, and usage are base. expect(layoutQuotaRow(copilot, fit)[0].map((segment) => segment.muted)).toEqual([ true, @@ -87,21 +94,26 @@ describe("quota columns", () => { ]); }); - it("right-aligns a value row's text at the rows' right edge, or before its reset", () => { + it("starts a value row's text in the percent column, before its reset", () => { const withReset = { ...spend, label: "Plan", reset: "reset 3d 0h" }; const fit = fitQuotaColumns([copilot, spend, withReset], 111)!; - expect(draw(spend, fit)).toEqual([` Monthly spend${" ".repeat(61)}USD 0.00`]); - expect(draw(spend, fit)[0]).toHaveLength(fit.width); - // It ends where the usage column ends, like the percent rows' usage. - expect(draw(withReset, fit)).toEqual([` Plan${" ".repeat(52)}USD 0.00 reset 3d 0h`]); + expect(draw(spend, fit)).toEqual([` Monthly spend${" ".repeat(32)}USD 0.00`]); + expect(draw(withReset, fit)).toEqual([ + ` Plan${" ".repeat(41)}USD 0.00${" ".repeat(14)}reset 3d 0h`, + ]); + // The value starts where the percent starts, and the reset where the percent row's does. + const percentStart = draw(copilot, fit)[0].indexOf("85% left"); + expect(draw(spend, fit)[0].indexOf("USD 0.00")).toBe(percentStart); + expect(draw(withReset, fit)[0].indexOf("USD 0.00")).toBe(percentStart); + expect(draw(withReset, fit)[0].indexOf("reset")).toBe(draw(copilot, fit)[0].indexOf("reset")); expect(layoutQuotaRow(spend, fit)[0]).toEqual([ { text: " Monthly spend", muted: true }, - { text: `${" ".repeat(61)}USD 0.00`, muted: false }, + { text: `${" ".repeat(32)}USD 0.00`, muted: false }, ]); }); - it("wraps a long value row's text inside its columns", () => { + it("wraps a long value row's text inside its columns, from the percent column", () => { const plan: ReportQuotaRow = { label: "Plan", value: "Pro | quota details unavailable for this organization right now", @@ -109,9 +121,11 @@ describe("quota columns", () => { }; const fit = fitQuotaColumns([fiveHour, plan], 60)!; + expect(draw(fiveHour, fit)[0].indexOf("100% left")).toBe(38); expect(draw(plan, fit)).toEqual([ - " Plan Pro | quota details unavailable for this", - " organization right now", + ` Plan${" ".repeat(32)}Pro | quota details`, + `${" ".repeat(38)}unavailable for this`, + `${" ".repeat(38)}organization right now`, ]); }); @@ -123,7 +137,8 @@ describe("quota columns", () => { { id: "label", start: 2, width: 15 }, { id: "value", start: 19, width: 65 }, ]); - expect(draw(balance, fit)).toEqual([` Current balance${" ".repeat(59)}USD 0.00`]); + expect(draw(spend, fit)).toEqual([" Monthly spend USD 0.00"]); + expect(draw(balance, fit)).toEqual([" Current balance USD 0.00"]); }); it("shrinks the bar on a narrower dialog and gives up below the chat's bar width", () => { diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index 869d4a96..d287191d 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -857,7 +857,8 @@ describe("V2 quota TUI commands", () => { expect(body().props.paddingLeft).toBe(13); expect(body().props.width).toBe(97); // Provider titles are bold accent; labels muted, bar, percent, and values base. With - // no reset column, the three gaps share the spare 27 columns. + // no reset column, the three gaps share the spare 27 columns. The percent, used/limit, + // and the value row's text start at their columns' left edges. const bar = `${"█".repeat(20)}${"░".repeat(4)}`; expect(texts()).toEqual([ { text: "→ [Copilot]", fg: "accent", bold: true }, @@ -868,13 +869,15 @@ describe("V2 quota TUI commands", () => { }, { text: "→ [OpenCode Zen]", fg: "accent", bold: true }, { - text: ` Month spend${" ".repeat(63)}USD 0.00`, + text: ` Month spend${" ".repeat(46)}USD 0.00`, fg: ["muted", "base"], bold: false, }, { text: "Partial failures", fg: "base", bold: true }, { text: " Z.ai: Authentication expired", fg: "muted", bold: false }, ]); + // The value starts where the percent starts, across providers. + expect(texts()[3].text.indexOf("USD 0.00")).toBe(texts()[1].text.indexOf("85% left")); // 45 columns: the report gets 38, which leaves the bar 5 of the 10 cells it needs. terminal.width = 45; From 29081daf82fe33db351de75c860d612341e48802 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:45:23 +0200 Subject: [PATCH 96/97] feat(tui): group the /quota popup into quota and spending tables --- src/lib/markdown-table.ts | 23 +- src/lib/quota-columns.ts | 271 +++++++++++---------- src/lib/quota-command-format.ts | 71 +++++- src/lib/report-document.ts | 26 +- src/tui-v2.tsx | 160 +++++++------ tests/quota-columns.test.ts | 326 +++++++++++++++++--------- tests/quota-command-format.test.ts | 66 +++++- tests/report-document.test.ts | 26 +- tests/tui-v2-commands.test.ts | 78 ++++-- tests/v4-phase5-cross-surface.test.ts | 34 +-- 10 files changed, 711 insertions(+), 370 deletions(-) diff --git a/src/lib/markdown-table.ts b/src/lib/markdown-table.ts index feab55ea..13daef20 100644 --- a/src/lib/markdown-table.ts +++ b/src/lib/markdown-table.ts @@ -199,16 +199,25 @@ export function fitTableToWidth(params: { ); const gapCount = params.headers.length - 1; - const gaps = new Array<number>(Math.max(0, gapCount)).fill(2); - if (fitting && gapCount > 0) { - const spare = params.width - naturalWidth(fitting); - for (let i = 0; i < gapCount; i++) { - gaps[i] += Math.floor(spare / gapCount) + (i < spare % gapCount ? 1 : 0); - } - } + const gaps = fitting + ? spreadTableGaps(gapCount, params.width - naturalWidth(fitting)) + : new Array<number>(Math.max(0, gapCount)).fill(2); const table = fitting ?? compact; const joinRow = (cells: string[]) => cells.map((cell, i) => (i < gapCount ? cell + " ".repeat(gaps[i]) : cell)).join(""); return { header: joinRow(table.header), rows: table.rows.map(joinRow) }; } + +/** + * The gaps between a table's columns when it spans `spare` more columns than its natural + * width: at least two spaces each, the spare width spread over them, the leftmost gaps + * taking any remainder. + */ +export function spreadTableGaps(gapCount: number, spare: number): number[] { + const gaps = new Array<number>(Math.max(0, gapCount)).fill(2); + for (let i = 0; i < gapCount; i++) { + gaps[i] += Math.floor(spare / gapCount) + (i < spare % gapCount ? 1 : 0); + } + return gaps; +} diff --git a/src/lib/quota-columns.ts b/src/lib/quota-columns.ts index 95e52e78..2e52304f 100644 --- a/src/lib/quota-columns.ts +++ b/src/lib/quota-columns.ts @@ -1,151 +1,164 @@ /** - * The TUI dialog's column layout for /quota rows. The chat text keeps its own layout - * (see buildQuotaCommandDocument); this one only draws the same rows in the dialog. + * The TUI dialog's tables for /quota rows. The chat text keeps its own layout (see + * buildQuotaCommandDocument); these only draw the same rows in the dialog. */ -import { bar, wrapDisplayText } from "./format-utils.js"; +import { bar, padLeft, wrapDisplayText } from "./format-utils.js"; +import { spreadTableGaps } from "./markdown-table.js"; import { QUOTA_COMMAND_BAR_WIDTH } from "./quota-command-format.js"; -import type { ReportQuotaRow } from "./report-document.js"; +import type { ReportBlock, ReportQuotaRow } from "./report-document.js"; -/** - * The widest the rows get: about the width of OpenCode's large dialog (88), an easy line - * to read. A typical full row (a 15-column label, a 24-cell bar, "100% left", "30.4/200", - * "reset 23d 23h 59m") fits it, and the xlarge dialog keeps the rest as even margins. - */ -export const QUOTA_COLUMNS_MAX_WIDTH = 84; -/** The longest bar; a longer one only stretches the eye between a label and its percent. */ -export const QUOTA_COLUMNS_BAR_MAX = 24; -/** The rows sit this far in from their provider title, as in the chat text. */ -const ROW_INDENT = 2; -const GAP = 2; +/** The bar's length; a longer one only stretches the eye between a label and its percent. */ +export const QUOTA_TABLE_BAR_WIDTH = 24; +/** The rows sit this far in from their provider, as in the chat text. */ +const ROW_INDENT = " "; + +export type QuotaBlock = Extract<ReportBlock, { kind: "quota" }>; /** A run of a laid-out line. Labels, reset times, and notes are muted; the rest is base. */ export type QuotaLineSegment = { text: string; muted: boolean }; -type Column = { id: "label" | "bar" | "value" | "usage" | "reset"; start: number; width: number }; +/** A table line: the header (bold, accent), a provider (bold), or a row or its note. */ +export type QuotaTableLine = { + style: "header" | "provider" | "row"; + segments: QuotaLineSegment[]; +}; + +export type QuotaTable = { title: string; lines: QuotaTableLine[] }; + +type ColumnId = "label" | "bar" | "value" | "usage" | "reset"; +type ColumnTexts = Partial<Record<ColumnId, string>>; -export type QuotaColumns = { - /** Blank columns left of the rows, which center them in the dialog. */ - indent: number; - /** The rows' width; each row ends at this column. */ - width: number; - columns: Column[]; +type TableGroup = { + title: string; + /** The header labels, full and compact. A column the group leaves empty has none. */ + headers: Record<"full" | "compact", ColumnTexts>; + providers: Array<{ provider: string; rows: ReportQuotaRow[] }>; }; /** - * Fits the columns of every /quota row in a report into `availableWidth`: label, bar, - * percent, used/limit, and reset. The rows take at most QUOTA_COLUMNS_MAX_WIDTH columns, - * centered. The bar grows up to QUOTA_COLUMNS_BAR_MAX cells and the spare width is spread - * over the gaps between columns (at least two spaces each, the leftmost gaps taking any - * remainder). A report without percent rows has no bar or percent columns; its value - * column takes the free width instead. Returns undefined when the bar (or that value - * column) would be shorter than the chat's bar, so the dialog keeps the chat layout. + * Lays out a /quota report's rows as the dialog's two tables, which span `width` columns: + * "Quota limits" holds the rows with a bar, "Spending & balances" the rows with a value. A + * provider shows in each table it has rows in, in the report's order; a table without rows + * is left out. Both tables share one set of columns, so the amounts start where the percents + * do: the label, the bar, the percent (or amount), used/limit, and the reset time, the last + * two only when a row has one. As in the token tables, the spare width is spread over the + * gaps between the columns, and the headers turn compact when the full ones do not fit. + * The bar keeps QUOTA_TABLE_BAR_WIDTH cells and shrinks only when the columns need the + * room. Returns undefined when they do not fit even with a chat-length bar, compact + * headers, and two-space gaps, so the dialog shows the chat lines. */ -export function fitQuotaColumns( - rows: ReportQuotaRow[], - availableWidth: number, -): QuotaColumns | undefined { - const width = Math.min(QUOTA_COLUMNS_MAX_WIDTH, availableWidth); - const widest = (texts: Array<string | undefined>) => - Math.max(0, ...texts.map((text) => text?.length ?? 0)); - const percentRows = rows.filter((row) => row.barPercent !== undefined); - // The column that takes the free width: the bar, or the value when no row has a bar. - const flexId: Column["id"] = percentRows.length > 0 ? "bar" : "value"; - const candidates: Array<Omit<Column, "start">> = [ - { id: "label", width: widest(rows.map((row) => row.label)) }, - { id: flexId, width: 0 }, - ...(flexId === "bar" - ? [{ id: "value" as const, width: widest(percentRows.map((row) => row.value)) }] - : []), - { id: "usage", width: widest(rows.map((row) => row.usage)) }, - { id: "reset", width: widest(rows.map((row) => row.reset)) }, - ]; - const sized = candidates.filter((column) => column.id === flexId || column.width > 0); +export function layoutQuotaTables(blocks: QuotaBlock[], width: number): QuotaTable[] | undefined { + const used = blocks[0]?.percentMode === "used"; + const allRows = blocks.flatMap((block) => block.rows); + const percentRows = allRows.filter((row) => row.barPercent !== undefined); + const percentHeader = used ? "Used %" : "Left"; + // The percents are right-aligned under their header. + const percentWidth = Math.max( + percentHeader.length, + ...percentRows.map((row) => row.value.length), + ); - const gapCount = sized.length - 1; - const used = ROW_INDENT + sized.reduce((sum, column) => sum + column.width, 0) + GAP * gapCount; - const flexWidth = flexId === "bar" ? Math.min(QUOTA_COLUMNS_BAR_MAX, width - used) : width - used; - if (flexWidth < QUOTA_COMMAND_BAR_WIDTH) return undefined; + const group = (title: string, item: string, percent: boolean, headers: ColumnTexts) => { + const providers = blocks + .map((block) => ({ + provider: block.provider, + rows: block.rows.filter((row) => (row.barPercent !== undefined) === percent), + })) + .filter((entry) => entry.rows.length > 0); + const rows = providers.flatMap((entry) => entry.rows); + const usage = rows.some((row) => row.usage); + const reset = rows.some((row) => row.reset); + return { + title, + headers: { + full: { + label: `Provider · ${item}`, + ...headers, + ...(usage ? { usage: used ? "Used/limit" : "Used" } : {}), + ...(reset ? { reset: "Resets in" } : {}), + }, + compact: { + label: "Provider", + ...headers, + ...(usage ? { usage: "Used" } : {}), + ...(reset ? { reset: "Resets" } : {}), + }, + }, + providers, + } satisfies TableGroup; + }; + const groups = [ + group("Quota limits", "window", true, { + bar: "Usage", + value: padLeft(percentHeader, percentWidth), + }), + group("Spending & balances", "item", false, { value: "Amount" }), + ].filter((entry) => entry.providers.length > 0); - const spare = width - used - flexWidth; - let start = ROW_INDENT; - const columns = sized.map((column, i) => { - const placed = { ...column, start, width: column.id === flexId ? flexWidth : column.width }; - start += placed.width + GAP + Math.floor(spare / gapCount) + (i < spare % gapCount ? 1 : 0); - return placed; + const columnIds: ColumnId[] = [ + "label", + ...(percentRows.length > 0 ? (["bar"] as const) : []), + "value", + ...(allRows.some((row) => row.usage) ? (["usage"] as const) : []), + ...(allRows.some((row) => row.reset) ? (["reset"] as const) : []), + ]; + const cells = (row: ReportQuotaRow, barWidth: number): ColumnTexts => ({ + label: ROW_INDENT + row.label, + ...(row.barPercent === undefined + ? { value: row.value } + : { bar: bar(row.barPercent, barWidth), value: padLeft(row.value, percentWidth) }), + usage: row.usage, + reset: row.reset, }); - return { indent: Math.floor((availableWidth - width) / 2), width, columns }; -} -/** - * Lays out one row in the columns: the label, then the bar, the percent, used/limit, and - * reset, each left-aligned at its column's start. A value row's text starts in the percent - * column (the value column), runs up to the next part the row shows (to the rows' right - * edge when there is none), and wraps there when it is too long. The notes follow, muted, - * from the bar's column, wrapped at the rows' right edge. - */ -export function layoutQuotaRow(row: ReportQuotaRow, fit: QuotaColumns): QuotaLineSegment[][] { - const flexStart = fit.columns[1].start; - // Every fit has a value column: the percent rows' labels, or the flex column without them. - const valueStart = fit.columns.find((column) => column.id === "value")!.start; - const cellText = (column: Column): string | undefined => { - switch (column.id) { - case "label": - return row.label; - case "bar": - return row.barPercent === undefined ? undefined : bar(row.barPercent, column.width); - case "value": - return row.barPercent === undefined ? undefined : row.value; - case "usage": - case "reset": - return row[column.id]; - } - }; - const parts: Array<{ start: number; text: string; muted: boolean }> = []; - let valueEnd = fit.width; - for (const [index, column] of fit.columns.entries()) { - const text = cellText(column); - if (!text) continue; - if (index > 0 && parts.length === 1) { - valueEnd = fit.columns[index - 1].start + fit.columns[index - 1].width; - } - parts.push({ - start: column.start, - text, - muted: column.id === "label" || column.id === "reset", - }); - } + for (let barWidth = QUOTA_TABLE_BAR_WIDTH; barWidth >= QUOTA_COMMAND_BAR_WIDTH; barWidth--) { + for (const variant of ["full", "compact"] as const) { + const lines = [ + ...groups.map((entry) => entry.headers[variant]), + ...allRows.map((row) => cells(row, barWidth)), + ]; + const widths = columnIds.map((id) => + Math.max(...lines.map((texts) => texts[id]?.length ?? 0)), + ); + const naturalWidth = widths.reduce((sum, w) => sum + w, 0) + 2 * (columnIds.length - 1); + if (naturalWidth > width) continue; - const valueLines = - row.barPercent !== undefined - ? [] - : row.value.length <= valueEnd - valueStart - ? [row.value] - : wrapDisplayText(row.value, valueEnd - valueStart); - if (valueLines.length > 0) { - parts.push({ start: valueStart, text: valueLines[0], muted: false }); - } - const lines: QuotaLineSegment[][] = [toSegments(parts)]; - for (const line of valueLines.slice(1)) { - lines.push([{ text: " ".repeat(valueStart) + line, muted: false }]); - } - for (const note of row.notes) { - for (const line of wrapDisplayText(note, fit.width - flexStart)) { - lines.push([{ text: " ".repeat(flexStart) + line, muted: true }]); + const gaps = spreadTableGaps(columnIds.length - 1, width - naturalWidth); + const starts = columnIds.map((_, i) => + widths.slice(0, i).reduce((sum, w, j) => sum + w + gaps[j], 0), + ); + const place = (texts: ColumnTexts, mutedIds: ColumnId[]): QuotaLineSegment[] => { + const segments: QuotaLineSegment[] = []; + let at = 0; + for (const [i, id] of columnIds.entries()) { + const text = texts[id]; + if (!text) continue; + segments.push({ text: " ".repeat(starts[i] - at) + text, muted: mutedIds.includes(id) }); + at = starts[i] + text.length; + } + return segments; + }; + return groups.map((entry) => ({ + title: entry.title, + lines: [ + { style: "header", segments: place(entry.headers[variant], []) }, + ...entry.providers.flatMap(({ provider, rows }) => [ + { style: "provider" as const, segments: [{ text: provider, muted: false }] }, + ...rows.flatMap((row) => [ + { style: "row" as const, segments: place(cells(row, barWidth), ["label", "reset"]) }, + // The notes sit under the row's bar (or amount), muted, wrapped at the edge. + ...row.notes + .flatMap((note) => wrapDisplayText(note, width - starts[1])) + .map((line) => ({ + style: "row" as const, + segments: [{ text: " ".repeat(starts[1]) + line, muted: true }], + })), + ]), + ]), + ], + })); } } - return lines; -} - -/** Joins placed parts into one line, filling the columns between them with spaces. */ -function toSegments( - parts: Array<{ start: number; text: string; muted: boolean }>, -): QuotaLineSegment[] { - const segments: QuotaLineSegment[] = []; - let at = 0; - for (const part of [...parts].sort((a, b) => a.start - b.start)) { - segments.push({ text: " ".repeat(part.start - at) + part.text, muted: part.muted }); - at = part.start + part.text.length; - } - return segments; + return undefined; } diff --git a/src/lib/quota-command-format.ts b/src/lib/quota-command-format.ts index 08034e45..d4814838 100644 --- a/src/lib/quota-command-format.ts +++ b/src/lib/quota-command-format.ts @@ -7,8 +7,17 @@ * - Includes session token summary (input/output per model) */ -import { type AccountingRowInterpretation, interpretAccountingRow } from "./accounting-format.js"; -import type { QuotaToastEntry, QuotaToastError, SessionTokensData } from "./entries.js"; +import { + type AccountingRowInterpretation, + formatAccountingWindowLabel, + interpretAccountingRow, +} from "./accounting-format.js"; +import type { + AccountingWindow, + QuotaToastEntry, + QuotaToastError, + SessionTokensData, +} from "./entries.js"; import { isPercentEntry, isValueEntry } from "./entries.js"; import { bar, @@ -34,10 +43,15 @@ import { import { SESSION_TOKEN_SECTION_HEADING } from "./session-tokens-format.js"; import type { QuotaToastConfig } from "./types.js"; -function formatCommandReset(iso?: string, spaced?: boolean): string { +/** The time until the reset, or "reset" when it is due; "" without a valid reset time. */ +function formatCommandCountdown(iso?: string, spaced?: boolean): string { if (!iso || !Number.isFinite(new Date(iso).getTime())) return ""; - const countdown = formatResetCountdown(iso, { spaced }); - return countdown === "reset" ? countdown : `reset ${countdown}`; + return formatResetCountdown(iso, { spaced }); +} + +function formatCommandReset(iso?: string, spaced?: boolean): string { + const countdown = formatCommandCountdown(iso, spaced); + return countdown === "reset" || !countdown ? countdown : `reset ${countdown}`; } export const QUOTA_COMMAND_BAR_WIDTH = 10; @@ -62,6 +76,32 @@ function getCommandWindowLabel(entry: QuotaToastEntry): string | null { return kind ? (COMMAND_WINDOW_LABELS[kind] ?? null) : null; } +/** The time windows, which name a quota alone in the TUI dialog. */ +const DIALOG_TIME_WINDOWS: ReadonlySet<AccountingWindow> = new Set([ + "rpm", + "hour", + "five_hour", + "day", + "week", + "month", + "year", +]); + +/** + * The TUI dialog's label for a row: a time window's quota drops "quota" ("5h quota" is + * "5h", "Weekly quota" is "Weekly"), since the dialog's table heads that column "window". + * Other labels, such as "Month spend" or "Fable weekly quota", stay whole. + */ +function getDialogMetricLabel(entry: QuotaToastEntry, metricLabel: string): string { + const metric = entry.semantic?.metric; + const window = !metric + ? getCommandWindowLabel(entry) + : metric.kind === "window" && DIALOG_TIME_WINDOWS.has(metric.window) + ? formatAccountingWindowLabel(metric.window) + : null; + return window && metricLabel === `${window} quota` ? window : metricLabel; +} + function getCommandMetricLabel(entry: QuotaToastEntry, semanticLabel: string): string { if (entry.semantic) return semanticLabel; @@ -165,15 +205,17 @@ export function buildQuotaCommandDocument(params: { const metricLabel = getCommandMetricLabel(row, interpretation.label); const label = padRight(metricLabel, labelWidth); const details = formatCommandDetails(row, rightWidth, params.resetTimeSpaced); + const dialogLabel = getDialogMetricLabel(row, metricLabel); // The RPC output must be plain JSON, so absent parts are left out, never undefined. const usage = row.right?.trim(); - const reset = formatCommandReset(row.resetTimeIso, params.resetTimeSpaced); + const countdown = formatCommandCountdown(row.resetTimeIso, params.resetTimeSpaced); + const reset = countdown === "reset" ? "now" : countdown; const usageAndReset = { ...(usage ? { usage } : {}), ...(reset ? { reset } : {}) }; if (interpretation.display.kind === "value") { lines.push(` ${label} ${interpretation.display.text}${details}`); rows.push({ - label: metricLabel, + label: dialogLabel, value: interpretation.display.text, ...usageAndReset, notes: [], @@ -197,9 +239,9 @@ export function buildQuotaCommandDocument(params: { lines.push(...basisDetails.map((detail) => ` ${detail}`)); const runway = isPercentEntry(row) ? formatQuotaRunway(row.runway) : ""; rows.push({ - label: metricLabel, + label: dialogLabel, barPercent: displayedPercent, - value: pctLabel, + value: `${displayedPercent}%`, ...usageAndReset, notes: [...(runway ? [`Runs out ${runway}`] : []), ...basisDetails], }); @@ -207,7 +249,16 @@ export function buildQuotaCommandDocument(params: { return { id: `group-${index}`, title: `→ ${formatGroupedHeader(group.group)}`, - blocks: [{ kind: "quota", lines, rows }], + blocks: [ + { + kind: "quota", + // The dialog's provider rows drop the brackets, as the compact line does. + provider: formatGroupedHeader(group.group).replace(/^\[([^\]]+)\]/u, "$1"), + percentMode: params.percentDisplayMode ?? "remaining", + lines, + rows, + }, + ], }; }); diff --git a/src/lib/report-document.ts b/src/lib/report-document.ts index e85d1630..70c3ad64 100644 --- a/src/lib/report-document.ts +++ b/src/lib/report-document.ts @@ -1,5 +1,6 @@ import { formatLocalCallTimestamp } from "./format-utils.js"; import { renderMarkdownTable, type WidthMode } from "./markdown-table.js"; +import type { PercentDisplayMode } from "./types.js"; /** * A report's first line. The text shows `line`. The TUI dialog has its own title, so it @@ -18,18 +19,19 @@ export type ReportKvRow = { }; /** - * One /quota row, split into the parts the TUI dialog lays out in columns. A percent row has - * `barPercent` and its percent label as `value`; a value row has only its text as `value`. + * One /quota row, split into the parts the TUI dialog lays out in its tables. A percent row + * has `barPercent` and its percent as `value`; a value row has only its text as `value`. */ export type ReportQuotaRow = { + /** The dialog's label, e.g. "5h" for the text's "5h quota" (its table says "window"). */ label: string; /** The displayed percent the bar fills, 0-100 (a percent row only). */ barPercent?: number; - /** The percent label, e.g. "85% left", or a value row's text, e.g. "USD 0.00". */ + /** The displayed percent, e.g. "85%", or a value row's text, e.g. "USD 0.00". */ value: string; /** Used over limit, e.g. "30.4/200". */ usage?: string; - /** The reset countdown, e.g. "reset 1d 8h 18m". */ + /** The time until the reset, e.g. "1d 8h 18m" ("now" when it is due). */ reset?: string; /** Lines under the row: the run-out projection and the accounting basis. */ notes: string[]; @@ -37,8 +39,18 @@ export type ReportQuotaRow = { export type ReportBlock = | { kind: "lines"; lines: string[] } - /** /quota rows: the text shows `lines`, the TUI dialog lays out `rows` in columns. */ - | { kind: "quota"; lines: string[]; rows: ReportQuotaRow[] } + /** + * One provider's /quota rows: the text shows `lines`, the TUI dialog lays out `rows` in + * its tables, under `provider` (e.g. "Copilot (individual)"). `percentMode` tells whether + * the percents are the part left or the part used. + */ + | { + kind: "quota"; + provider: string; + percentMode: PercentDisplayMode; + lines: string[]; + rows: ReportQuotaRow[]; + } | { kind: "kv"; rows: ReportKvRow[] } | { kind: "table"; @@ -143,6 +155,8 @@ function isReportBlock(value: unknown): boolean { return isStringArray(value.lines); case "quota": return ( + typeof value.provider === "string" && + (value.percentMode === "remaining" || value.percentMode === "used") && isStringArray(value.lines) && Array.isArray(value.rows) && value.rows.every(isReportQuotaRow) diff --git a/src/tui-v2.tsx b/src/tui-v2.tsx index 75e67bc6..57efc7e1 100644 --- a/src/tui-v2.tsx +++ b/src/tui-v2.tsx @@ -9,7 +9,7 @@ import { loadConfig } from "./lib/config.js"; import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js"; import { sanitizeDisplayText } from "./lib/display-sanitize.js"; import { fitTableToWidth } from "./lib/markdown-table.js"; -import { fitQuotaColumns, layoutQuotaRow, type QuotaColumns } from "./lib/quota-columns.js"; +import { layoutQuotaTables, type QuotaBlock, type QuotaTable } from "./lib/quota-columns.js"; import { parseQuotaSlashCommand, QUOTA_DIALOG_COMMANDS, @@ -20,6 +20,7 @@ import { type ReportBlock, type ReportDocument, type ReportQuotaRow, + type ReportSection, renderableSections, renderKvRow, } from "./lib/report-document.js"; @@ -308,52 +309,75 @@ function sanitizeQuotaRow(row: ReportQuotaRow): ReportQuotaRow { }; } -/** The /quota rows of a report, sanitized, for fitQuotaColumns and layoutQuotaRow. */ -function quotaRows(block: ReportBlock): ReportQuotaRow[] { - return block.kind === "quota" ? block.rows.map(sanitizeQuotaRow) : []; +/** A report's /quota blocks, sanitized, for layoutQuotaTables. */ +function quotaBlocks(section: ReportSection): QuotaBlock[] { + return section.blocks.flatMap((block) => + block.kind === "quota" + ? [ + { + ...block, + provider: sanitizeDisplayText(block.provider), + rows: block.rows.map(sanitizeQuotaRow), + }, + ] + : [], + ); } /** - * One block of a report. Lines and key-value rows wrap at words. A table keeps each row on - * one line and spans `width` columns when it fits (see fitTableToWidth), with the header - * row bold in OpenCode's primary accent (its text color for a selected tab). /quota rows - * sit in `quotaColumns` when the report's rows fit them, else they show their chat lines. - * Like OpenCode's sidebar, labels are muted and values are base. + * A /quota table (see layoutQuotaTables): its title bold, like a section title, then its + * header bold in OpenCode's primary accent, like the token tables, and its providers bold. + * In the rows, labels, reset times, and notes are muted; bars, percents, and values base. + */ +function QuotaTableView(props: { table: QuotaTable; theme: DialogTheme }): JSX.Element { + return ( + <box flexDirection="column"> + <text attributes={TextAttributes.BOLD} fg={props.theme.text.base}> + {props.table.title} + </text> + {props.table.lines.map((line) => + line.style === "row" ? ( + <text fg={props.theme.text.base} wrapMode="none"> + {line.segments.map((segment) => ( + <span style={{ fg: segment.muted ? props.theme.text.muted : props.theme.text.base }}> + {segment.text} + </span> + ))} + </text> + ) : ( + <text + attributes={TextAttributes.BOLD} + fg={ + line.style === "header" + ? props.theme.text.action.primary.selected + : props.theme.text.base + } + wrapMode={line.style === "header" ? "none" : undefined} + > + {line.segments.map((segment) => segment.text).join("")} + </text> + ), + )} + </box> + ); +} + +/** + * One block of a report. Lines, and the chat lines of /quota rows the dialog's tables do not + * fit, and key-value rows wrap at words. A table keeps each row on one line and spans `width` + * columns when it fits (see fitTableToWidth), with the header row bold in OpenCode's primary + * accent (its text color for a selected tab). Like OpenCode's sidebar, labels are muted and + * values are base. */ function ReportBlockView(props: { block: ReportBlock; theme: DialogTheme; width: number; - quotaColumns: QuotaColumns | undefined; }): JSX.Element { const block = props.block; - if (block.kind === "lines") { + if (block.kind === "lines" || block.kind === "quota") { return <text fg={props.theme.text.muted}>{sanitizeDisplayText(block.lines.join("\n"))}</text>; } - if (block.kind === "quota") { - // Read in the JSX, so the rows are laid out again when the terminal is resized. - return ( - <box flexDirection="column"> - {props.quotaColumns ? ( - quotaRows(block) - .flatMap((row) => layoutQuotaRow(row, props.quotaColumns!)) - .map((line) => ( - <text fg={props.theme.text.base} wrapMode="none"> - {line.map((segment) => ( - <span - style={{ fg: segment.muted ? props.theme.text.muted : props.theme.text.base }} - > - {segment.text} - </span> - ))} - </text> - )) - ) : ( - <text fg={props.theme.text.muted}>{sanitizeDisplayText(block.lines.join("\n"))}</text> - )} - </box> - ); - } if (block.kind === "kv") { return ( <box flexDirection="column"> @@ -400,10 +424,10 @@ function ReportBlockView(props: { /** * Draws a report document's sections with the same spacing as its plain text: one blank row * between the sections and between the blocks of a section, and a section title directly - * above its first block. Section titles are bold; a /quota provider title is in OpenCode's - * primary accent. When the /quota rows fit their columns (see fitQuotaColumns), the whole - * report takes the columns' width, centered, so its other lines wrap there too. The dialog - * shows the heading's subtitle under its own title, so the heading line is left out. + * above its first block. Section titles are bold. A /quota report's provider sections come + * first; when their rows fit the dialog's tables (see layoutQuotaTables), the tables take + * their place. The dialog shows the heading's subtitle under its own title, so the heading + * line is left out. */ function ReportDocumentView(props: { document: ReportDocument; @@ -411,42 +435,30 @@ function ReportDocumentView(props: { width: number; }): JSX.Element { const sections = renderableSections(props.document); - const rows = sections.flatMap((section) => section.blocks.flatMap(quotaRows)); - // Read in the JSX, so the columns are fitted again when the terminal is resized. - const columns = () => (rows.length > 0 ? fitQuotaColumns(rows, props.width) : undefined); + const blocks = sections.flatMap(quotaBlocks); + // Read in the JSX, so the tables are laid out again when the terminal is resized. + const tables = () => (blocks.length > 0 ? layoutQuotaTables(blocks, props.width) : undefined); return ( - <box - flexDirection="column" - gap={1} - paddingLeft={columns()?.indent} - width={columns() ? columns()!.indent + columns()!.width : undefined} - > - {sections.map((section) => ( - <box flexDirection="column"> - {section.title ? ( - <text - attributes={TextAttributes.BOLD} - fg={ - section.blocks.some((block) => block.kind === "quota") - ? props.theme.text.action.primary.selected - : props.theme.text.base - } - > - {sanitizeDisplayText(section.title)} - </text> - ) : null} - <box flexDirection="column" gap={1}> - {section.blocks.map((block) => ( - <ReportBlockView - block={block} - theme={props.theme} - width={props.width} - quotaColumns={columns()} - /> - ))} - </box> - </box> - ))} + <box flexDirection="column" gap={1}> + {[ + ...(tables() ?? []).map((table) => <QuotaTableView table={table} theme={props.theme} />), + ...sections + .filter((section) => !tables() || !section.blocks.some((block) => block.kind === "quota")) + .map((section) => ( + <box flexDirection="column"> + {section.title ? ( + <text attributes={TextAttributes.BOLD} fg={props.theme.text.base}> + {sanitizeDisplayText(section.title)} + </text> + ) : null} + <box flexDirection="column" gap={1}> + {section.blocks.map((block) => ( + <ReportBlockView block={block} theme={props.theme} width={props.width} /> + ))} + </box> + </box> + )), + ]} </box> ); } diff --git a/tests/quota-columns.test.ts b/tests/quota-columns.test.ts index 1382d8cf..8af11794 100644 --- a/tests/quota-columns.test.ts +++ b/tests/quota-columns.test.ts @@ -1,155 +1,271 @@ import { describe, expect, it } from "vitest"; import { - fitQuotaColumns, - layoutQuotaRow, - QUOTA_COLUMNS_BAR_MAX, - QUOTA_COLUMNS_MAX_WIDTH, - type QuotaColumns, + layoutQuotaTables, + QUOTA_TABLE_BAR_WIDTH, + type QuotaBlock, + type QuotaTable, } from "../src/lib/quota-columns.js"; import type { ReportQuotaRow } from "../src/lib/report-document.js"; -const copilot: ReportQuotaRow = { +const copilotQuota: ReportQuotaRow = { label: "Quota", barPercent: 85, - value: "85% left", + value: "85%", usage: "30.4/200", - reset: "reset 1d 8h 18m", + reset: "1d 7h 48m", notes: [], }; const fiveHour: ReportQuotaRow = { - label: "5h quota", + label: "5h", barPercent: 100, - value: "100% left", - reset: "reset 5h 0m", + value: "100%", + reset: "5h 0m", notes: [], }; const week: ReportQuotaRow = { - label: "Week quota", - barPercent: 50, - value: "50% left", - reset: "reset 3d 18h 21m", + label: "Week", + barPercent: 72, + value: "72%", + reset: "3d 17h 51m", notes: ["Runs out ≈ 2d 3h"], }; +const zenBudget: ReportQuotaRow = { + label: "Month budget", + barPercent: 40, + value: "40%", + notes: [], +}; const spend: ReportQuotaRow = { label: "Monthly spend", value: "USD 0.00", notes: [] }; +const balance: ReportQuotaRow = { label: "Current balance", value: "USD 0.00", notes: [] }; -/** A row's lines as plain text, each with the columns its muted runs cover. */ -function draw(row: ReportQuotaRow, fit: QuotaColumns): string[] { - return layoutQuotaRow(row, fit).map((line) => line.map((segment) => segment.text).join("")); +function block( + provider: string, + rows: ReportQuotaRow[], + percentMode: "remaining" | "used" = "remaining", +): QuotaBlock { + return { kind: "quota", provider, percentMode, lines: [], rows }; } -describe("quota columns", () => { - it("caps the rows' width and centers them in a wider dialog", () => { - const fit = fitQuotaColumns([copilot, fiveHour, week, spend], 111)!; +const report = [ + block("Copilot (individual)", [copilotQuota]), + block("OpenAI (Business)", [fiveHour, week]), + block("OpenCode Zen", [zenBudget, spend, balance]), +]; - expect(QUOTA_COLUMNS_MAX_WIDTH).toBe(84); - expect(fit.width).toBe(84); - // 111 - 84 = 27 spare columns: 13 on the left, 14 on the right. - expect(fit.indent).toBe(13); - }); +/** Each table as its title and lines, one string per line. */ +function draw( + tables: QuotaTable[] | undefined, +): Array<{ title: string; lines: string[] }> | undefined { + return tables?.map((table) => ({ + title: table.title, + lines: table.lines.map((line) => line.segments.map((segment) => segment.text).join("")), + })); +} - it("caps the bar and spreads the spare width over the gaps", () => { - const fit = fitQuotaColumns([fiveHour, week], 111)!; +/** A line with each text starting at its column. */ +function at(parts: Array<[number, string]>): string { + return parts.reduce((line, [start, text]) => line.padEnd(start) + text, ""); +} - // A 2-column row indent, label 10, percent 9, reset 16, and three 2-column gaps leave - // 41 of the 84 columns: the bar takes its 24 and the three gaps share the other 17. - expect(fit.columns).toEqual([ - { id: "label", start: 2, width: 10 }, - { id: "bar", start: 20, width: QUOTA_COLUMNS_BAR_MAX }, - { id: "value", start: 52, width: 9 }, - { id: "reset", start: 68, width: 16 }, - ]); - expect(draw(fiveHour, fit)).toEqual([ - ` 5h quota ${"█".repeat(24)} 100% left reset 5h 0m`, +/** The bar in a drawn line. */ +function barIn(line: string): string { + return line.match(/[█░]+/u)?.[0] ?? ""; +} + +describe("quota tables", () => { + it("groups percent rows under Quota limits and value rows under Spending & balances", () => { + const tables = layoutQuotaTables(report, 111)!; + + // A provider shows in each table it has rows in, in the report's order. + expect(tables.map((table) => table.title)).toEqual(["Quota limits", "Spending & balances"]); + expect( + tables.map((table) => + table.lines + .filter((line) => line.style === "provider") + .map((line) => line.segments[0].text), + ), + ).toEqual([["Copilot (individual)", "OpenAI (Business)", "OpenCode Zen"], ["OpenCode Zen"]]); + // Each table starts with one header row. + expect(tables.map((table) => table.lines.map((line) => line.style))).toEqual([ + ["header", "provider", "row", "provider", "row", "row", "row", "provider", "row"], + ["header", "provider", "row", "row"], ]); }); - it("left-aligns every column: label, percent, used/limit, reset, notes under the bar", () => { - const fit = fitQuotaColumns([copilot, fiveHour, week, spend], 111)!; - const lines = [copilot, fiveHour, week].flatMap((row) => draw(row, fit)); + it("spreads the spare width over the gaps, keeps the bar at 24 cells, and shares the columns", () => { + const tables = layoutQuotaTables(report, 111)!; - expect(lines).toEqual([ - ` Quota ${"█".repeat(20)}${"░".repeat(4)} 85% left 30.4/200 reset 1d 8h 18m`, - ` 5h quota ${"█".repeat(24)} 100% left reset 5h 0m`, - ` Week quota ${"█".repeat(12)}${"░".repeat(12)} 50% left reset 3d 18h 21m`, - " Runs out ≈ 2d 3h", + // Label 17 ("Provider · window"), bar 24, percent or amount 8, used 8, reset 10, and + // four 2-column gaps take 75 of 111 columns; the gaps share the other 36: 9 each. + expect(draw(tables)).toEqual([ + { + title: "Quota limits", + lines: [ + at([ + [0, "Provider · window"], + [28, "Usage"], + [63, "Left"], + [82, "Used"], + [101, "Resets in"], + ]), + "Copilot (individual)", + at([ + [0, " Quota"], + [28, `${"█".repeat(20)}${"░".repeat(4)}`], + [63, " 85%"], + [82, "30.4/200"], + [101, "1d 7h 48m"], + ]), + "OpenAI (Business)", + at([ + [0, " 5h"], + [28, "█".repeat(24)], + [63, "100%"], + [101, "5h 0m"], + ]), + at([ + [0, " Week"], + [28, `${"█".repeat(17)}${"░".repeat(7)}`], + [63, " 72%"], + [101, "3d 17h 51m"], + ]), + // A note sits under the bar. + at([[28, "Runs out ≈ 2d 3h"]]), + "OpenCode Zen", + at([ + [0, " Month budget"], + [28, `${"█".repeat(10)}${"░".repeat(14)}`], + [63, " 40%"], + ]), + ], + }, + { + title: "Spending & balances", + // The amounts start where the percents do; this table has no used or reset values. + lines: [ + at([ + [0, "Provider · item"], + [63, "Amount"], + ]), + "OpenCode Zen", + at([ + [0, " Monthly spend"], + [63, "USD 0.00"], + ]), + at([ + [0, " Current balance"], + [63, "USD 0.00"], + ]), + ], + }, ]); - // Every percent starts at the value column. - const valueStart = fit.columns.find((column) => column.id === "value")!.start; - expect(lines.slice(0, 3).map((line) => line.search(/\d+% left/))).toEqual([ - valueStart, - valueStart, - valueStart, - ]); - // Labels, reset times, and notes are muted; the bar, percent, and usage are base. - expect(layoutQuotaRow(copilot, fit)[0].map((segment) => segment.muted)).toEqual([ + // The last column ends at the edge. + expect(draw(tables)![0].lines[5]).toHaveLength(111); + expect(QUOTA_TABLE_BAR_WIDTH).toBe(24); + + // A wider dialog widens the gaps, never the bar. + const wide = draw(layoutQuotaTables(report, 150))!; + expect(wide[0].lines[5]).toHaveLength(150); + expect(barIn(wide[0].lines[2])).toHaveLength(24); + }); + + it("mutes labels, reset times, and notes; bars, percents, used, and amounts are base", () => { + const [quota, spending] = layoutQuotaTables(report, 111)!; + + expect(quota.lines[2].segments.map((segment) => segment.muted)).toEqual([ true, false, false, false, true, ]); - expect(layoutQuotaRow(week, fit)[1]).toEqual([ - { text: " Runs out ≈ 2d 3h", muted: true }, - ]); + expect(quota.lines[6].segments.map((segment) => segment.muted)).toEqual([true]); + expect(spending.lines[2].segments.map((segment) => segment.muted)).toEqual([true, false]); }); - it("starts a value row's text in the percent column, before its reset", () => { - const withReset = { ...spend, label: "Plan", reset: "reset 3d 0h" }; - const fit = fitQuotaColumns([copilot, spend, withReset], 111)!; + it("leaves out the Used and Resets in columns when no row has them", () => { + const tables = draw( + layoutQuotaTables([block("OpenAI", [{ ...fiveHour, reset: undefined }])], 60), + ); - expect(draw(spend, fit)).toEqual([` Monthly spend${" ".repeat(32)}USD 0.00`]); - expect(draw(withReset, fit)).toEqual([ - ` Plan${" ".repeat(41)}USD 0.00${" ".repeat(14)}reset 3d 0h`, - ]); - // The value starts where the percent starts, and the reset where the percent row's does. - const percentStart = draw(copilot, fit)[0].indexOf("85% left"); - expect(draw(spend, fit)[0].indexOf("USD 0.00")).toBe(percentStart); - expect(draw(withReset, fit)[0].indexOf("USD 0.00")).toBe(percentStart); - expect(draw(withReset, fit)[0].indexOf("reset")).toBe(draw(copilot, fit)[0].indexOf("reset")); - expect(layoutQuotaRow(spend, fit)[0]).toEqual([ - { text: " Monthly spend", muted: true }, - { text: `${" ".repeat(32)}USD 0.00`, muted: false }, + // Label 17, bar 24, percent 4, and two 2-column gaps: the gaps share the spare 11. + expect(tables).toEqual([ + { + title: "Quota limits", + lines: [ + at([ + [0, "Provider · window"], + [25, "Usage"], + [56, "Left"], + ]), + "OpenAI", + at([ + [0, " 5h"], + [25, "█".repeat(24)], + [56, "100%"], + ]), + ], + }, ]); }); - it("wraps a long value row's text inside its columns, from the percent column", () => { - const plan: ReportQuotaRow = { - label: "Plan", - value: "Pro | quota details unavailable for this organization right now", - notes: [], - }; - const fit = fitQuotaColumns([fiveHour, plan], 60)!; - - expect(draw(fiveHour, fit)[0].indexOf("100% left")).toBe(38); - expect(draw(plan, fit)).toEqual([ - ` Plan${" ".repeat(32)}Pro | quota details`, - `${" ".repeat(38)}unavailable for this`, - `${" ".repeat(38)}organization right now`, + it("lays out a report without percent rows as the spending table alone", () => { + const tables = draw(layoutQuotaTables([block("OpenCode Zen", [spend, balance])], 60)); + + expect(tables).toEqual([ + { + title: "Spending & balances", + lines: [ + at([ + [0, "Provider · item"], + [52, "Amount"], + ]), + "OpenCode Zen", + at([ + [0, " Monthly spend"], + [52, "USD 0.00"], + ]), + at([ + [0, " Current balance"], + [52, "USD 0.00"], + ]), + ], + }, ]); }); - it("gives a report without percent rows a value column that takes the free width", () => { - const balance = { ...spend, label: "Current balance" }; - const fit = fitQuotaColumns([spend, balance], 111)!; + it("heads the percents Used % when they show the part used", () => { + const used = [block("Copilot", [{ ...copilotQuota, barPercent: 15, value: "15%" }], "used")]; + const [quota] = draw(layoutQuotaTables(used, 111))!; - expect(fit.columns).toEqual([ - { id: "label", start: 2, width: 15 }, - { id: "value", start: 19, width: 65 }, - ]); - expect(draw(spend, fit)).toEqual([" Monthly spend USD 0.00"]); - expect(draw(balance, fit)).toEqual([" Current balance USD 0.00"]); + // The percents stay right-aligned under their header; used/limit is named apart. + expect(quota.lines[0]).toMatch(/Usage +Used % +Used\/limit +Resets in$/u); + expect(quota.lines[0].indexOf("Used %") + "Used %".length).toBe( + quota.lines[2].indexOf("15%") + "15%".length, + ); }); - it("shrinks the bar on a narrower dialog and gives up below the chat's bar width", () => { - // Everything but the bar takes 2 + 5 + 8 + 8 + 15 + 4 gaps of 2 = 46 columns. - expect(fitQuotaColumns([copilot], 60)?.columns[1]).toEqual({ - id: "bar", - start: 9, - width: 14, - }); - expect(fitQuotaColumns([copilot], 56)?.columns[1].width).toBe(10); - expect(fitQuotaColumns([copilot], 55)).toBeUndefined(); - expect(fitQuotaColumns([spend], 24)).toBeUndefined(); + it("turns the headers compact, then shrinks the bar, then gives up when the dialog is narrow", () => { + const rows = [block("Copilot", [copilotQuota])]; + // Full headers: label 17, bar 24, percent 4, used 8, reset 9, and four gaps: 70. + expect(draw(layoutQuotaTables(rows, 70))![0].lines[0]).toMatch(/^Provider · window/u); + // Compact headers: label 8, the rest as wide as before: 61. + const compact = draw(layoutQuotaTables(rows, 61))![0].lines; + expect(compact[0]).toBe( + at([ + [0, "Provider"], + [10, "Usage"], + [36, "Left"], + [42, "Used"], + [52, "Resets"], + ]), + ); + expect(barIn(compact[2])).toHaveLength(24); + // Narrower still, the bar shrinks, down to the chat's 10 cells; below that, the dialog + // shows the chat lines. + expect(barIn(draw(layoutQuotaTables(rows, 55))![0].lines[2])).toHaveLength(18); + expect(barIn(draw(layoutQuotaTables(rows, 47))![0].lines[2])).toHaveLength(10); + expect(layoutQuotaTables(rows, 46)).toBeUndefined(); }); }); diff --git a/tests/quota-command-format.test.ts b/tests/quota-command-format.test.ts index c78d7913..ecc41383 100644 --- a/tests/quota-command-format.test.ts +++ b/tests/quota-command-format.test.ts @@ -590,14 +590,76 @@ describe("formatQuotaCommand", () => { { label: "Quota", barPercent: 86, - value: "86% left", + value: "86%", usage: "42/300", - reset: "reset 12h0m", + reset: "12h0m", notes: ["Runs out lasts past reset"], }, { label: "Usage", value: "9 used", notes: [] }, ]); + expect(block.provider).toBe("Copilot"); + expect(block.percentMode).toBe("remaining"); // The RPC output must be plain JSON: no part is undefined. expect(JSON.parse(JSON.stringify(block.rows))).toStrictEqual(block.rows); }); + + it("names a time window's quota by its window alone in the dialog, and keeps other labels", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-15T12:00:00.000Z")); + const params = { + entries: [ + { + accounting: accounting("quota"), + name: "OpenAI", + label: "5h:", + percentRemaining: 100, + resetTimeIso: "2026-01-15T11:00:00.000Z", + }, + { + accounting: accounting("quota"), + name: "OpenAI", + semantic: { + metric: { kind: "window" as const, window: "week" as const }, + prominence: "primary" as const, + }, + percentRemaining: 72, + }, + { + accounting: accounting("quota"), + name: "OpenAI", + semantic: { + metric: { kind: "window" as const, window: "mcp" as const }, + prominence: "primary" as const, + }, + percentRemaining: 50, + }, + { + accounting: accounting("spend"), + name: "OpenAI", + group: "[OpenAI] (Business)", + label: "Monthly spend:", + kind: "value" as const, + value: "USD 0.00", + }, + ], + errors: [], + percentDisplayMode: "used" as const, + }; + + const document = buildQuotaCommandDocument(params); + const block = document.sections[0]?.blocks[0]; + if (block?.kind !== "quota") throw new Error("expected a quota block"); + // The dialog's provider rows drop the brackets of an OpenCode login's group. + expect(document.sections[1]?.title).toBe("→ [OpenAI] (Business)"); + expect(document.sections[1]?.blocks[0]).toMatchObject({ provider: "OpenAI (Business)" }); + expect(block.provider).toBe("OpenAI"); + // The chat keeps its labels; the dialog's are shorter only for time windows. + expect(block.lines.join("\n")).toContain("5h quota"); + expect(block.rows.map((row) => row.label)).toEqual(["5h", "Weekly", "MCP quota"]); + expect(document.sections[1]?.blocks[0]).toMatchObject({ rows: [{ label: "Month spend" }] }); + // A due reset shows "now"; with percentDisplayMode "used" the percents are the part used. + expect(block.rows[0]).toMatchObject({ barPercent: 0, value: "0%", reset: "now" }); + expect(block.rows[1]).toMatchObject({ barPercent: 28, value: "28%" }); + expect(block.percentMode).toBe("used"); + }); }); diff --git a/tests/report-document.test.ts b/tests/report-document.test.ts index 0c923c15..68f56809 100644 --- a/tests/report-document.test.ts +++ b/tests/report-document.test.ts @@ -273,10 +273,12 @@ describe("report-document", () => { { kind: "table", headers: ["Tok"], fullHeaders: ["Tokens"], rows: [], aligns: [] }, { kind: "quota", + provider: "Copilot", + percentMode: "remaining", lines: [" Quota █░ 50% left"], rows: [ - { label: "Quota", barPercent: 50, value: "50% left", notes: [] }, - { label: "Spend", value: "USD 0.00", usage: "1/2", reset: "reset 1h", notes: [] }, + { label: "Quota", barPercent: 50, value: "50%", notes: [] }, + { label: "Spend", value: "USD 0.00", usage: "1/2", reset: "1h", notes: [] }, ], }, ], @@ -326,7 +328,25 @@ describe("report-document", () => { sections: [ { id: "s", - blocks: [{ kind: "quota", lines: [], rows: [{ label: "Quota", value: "1%" }] }], + blocks: [ + { + kind: "quota", + provider: "Copilot", + percentMode: "remaining", + lines: [], + rows: [{ label: "Quota", value: "1%" }], + }, + ], + }, + ], + }, + { + sections: [ + { + id: "s", + blocks: [ + { kind: "quota", provider: "Copilot", percentMode: "left", lines: [], rows: [] }, + ], }, ], }, diff --git a/tests/tui-v2-commands.test.ts b/tests/tui-v2-commands.test.ts index d287191d..559ab15e 100644 --- a/tests/tui-v2-commands.test.ts +++ b/tests/tui-v2-commands.test.ts @@ -806,7 +806,7 @@ describe("V2 quota TUI commands", () => { expect(tableLines()).toEqual(["Model Tok Cost", "claude-opus 20K $10.00"]); }); - it("lays /quota rows out in centered columns, and falls back to the chat lines when narrow", async () => { + it("groups /quota rows into full-width tables, and falls back to the chat lines when narrow", async () => { stubRenderingReact(); const accounting = { resultType: "quota", @@ -824,6 +824,13 @@ describe("V2 quota TUI commands", () => { kind: "value", value: "USD 0.00", }, + { + accounting, + name: "OpenAI", + group: "OpenAI (Business)", + label: "5h:", + percentRemaining: 100, + }, ], errors: [{ label: "Z.ai", message: "Authentication expired" }], }); @@ -852,39 +859,76 @@ describe("V2 quota TUI commands", () => { bold: node.props.attributes === TextAttributes.BOLD, }; }); + const at = (parts: Array<[number, string]>) => + parts.reduce((line, [start, text]) => line.padEnd(start) + text, ""); - // 120 columns: the report gets 111; the rows take 84 of them, 13 blank columns left. - expect(body().props.paddingLeft).toBe(13); - expect(body().props.width).toBe(97); - // Provider titles are bold accent; labels muted, bar, percent, and values base. With - // no reset column, the three gaps share the spare 27 columns. The percent, used/limit, - // and the value row's text start at their columns' left edges. + // 120 columns: the report gets all 111, left-aligned. The columns (label 17, bar 24, + // percent or amount 8, used 6) leave 50 spare columns for the three gaps: 19, 19, 18. + expect(body().props.paddingLeft).toBeUndefined(); + expect(body().props.width).toBeUndefined(); const bar = `${"█".repeat(20)}${"░".repeat(4)}`; expect(texts()).toEqual([ - { text: "→ [Copilot]", fg: "accent", bold: true }, + { text: "Quota limits", fg: "base", bold: true }, { - text: ` Quota${" ".repeat(17)}${bar}${" ".repeat(11)}85% left${" ".repeat(11)}30/200`, + text: at([ + [0, "Provider · window"], + [36, "Usage"], + [79, "Left"], + [105, "Used"], + ]), + fg: "accent", + bold: true, + }, + { text: "Copilot", fg: "base", bold: true }, + { + text: at([ + [0, " Quota"], + [36, bar], + [79, " 85%"], + [105, "30/200"], + ]), fg: ["muted", "base", "base", "base"], bold: false, }, - { text: "→ [OpenCode Zen]", fg: "accent", bold: true }, + { text: "OpenAI (Business)", fg: "base", bold: true }, { - text: ` Month spend${" ".repeat(46)}USD 0.00`, + text: at([ + [0, " 5h"], + [36, "█".repeat(24)], + [79, "100%"], + ]), + fg: ["muted", "base", "base"], + bold: false, + }, + { text: "Spending & balances", fg: "base", bold: true }, + { + text: at([ + [0, "Provider · item"], + [79, "Amount"], + ]), + fg: "accent", + bold: true, + }, + { text: "OpenCode Zen", fg: "base", bold: true }, + { + text: at([ + [0, " Month spend"], + [79, "USD 0.00"], + ]), fg: ["muted", "base"], bold: false, }, { text: "Partial failures", fg: "base", bold: true }, { text: " Z.ai: Authentication expired", fg: "muted", bold: false }, ]); - // The value starts where the percent starts, across providers. - expect(texts()[3].text.indexOf("USD 0.00")).toBe(texts()[1].text.indexOf("85% left")); + // Every table line spans the report's width. + expect(texts()[3].text).toHaveLength(111); - // 45 columns: the report gets 38, which leaves the bar 5 of the 10 cells it needs. + // 45 columns: the report gets 38, too narrow even for a 10-cell bar, compact headers, + // and two-space gaps. The dialog shows the chat lines under bold provider titles. terminal.width = 45; - expect(body().props.paddingLeft).toBeUndefined(); - expect(body().props.width).toBeUndefined(); expect(texts().slice(0, 2)).toEqual([ - { text: "→ [Copilot]", fg: "accent", bold: true }, + { text: "→ [Copilot]", fg: "base", bold: true }, { text: " Quota █████████░ 85% left | 30/200", fg: "muted", diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 91115f8d..23080583 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -584,7 +584,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const quotaDialog = v2.dialog.mock.calls[0][0]; expect(quotaDialog.title).toBe("OpenCode Quota"); expect(quotaDialog.subtitle).toMatch(/^\d{2}:\d{2} \d{2}\/\d{2}\/\d{4}$/); - expect(serverOutput).toMatch(/^→ /); + expect(serverOutput).toMatch(/^Quota limits\n/); expect(serverOutput).not.toContain("(/quota)"); // /quota typed in the TUI prompt opens the same report in the dialog and posts nothing. expect(v2.typeCommand("/quota")).toBeUndefined(); @@ -593,19 +593,22 @@ describe("v4 Phase 5 cross-surface release evidence", () => { title: "OpenCode Quota", subtitle: quotaDialog.subtitle, }); - expect(v2.dialog.mock.calls[1][0].message).toMatch(/^→ /); + expect(v2.dialog.mock.calls[1][0].message).toMatch(/^Quota limits\n/); expect(client.session.prompt).not.toHaveBeenCalled(); expect(serverOutput).not.toContain("```"); expect(serverOutput).not.toMatch(/^#{1,6} /mu); - // The dialog lays the rows out in columns, centered: a 120-column terminal leaves them - // 84 of 111 columns, 13 blank on the left. Every bar has the same length. - expect(serverOutput).toMatch(/→ \[Team Accounting\]\n {2}Month quota/u); + // The dialog groups the rows into a quota table and a spending table, each with one + // header row and the providers as sub-headers. Every bar is 24 cells long. + expect(serverOutput).toMatch( + /^Quota limits\nProvider · window +Usage +Left +Used +Resets in\nTeam Accounting\n {2}Month +[█░]{24} +64% +64\/100 +\d+d /u, + ); const serverBars = serverOutput.match(/[█░]+/gu) ?? []; expect(serverBars.length).toBeGreaterThan(0); - expect(new Set(serverBars.map((bar) => Array.from(bar).length)).size).toBe(1); - expect(serverOutput).not.toMatch(/Month quota[^\n]* \| /u); - expect(serverOutput).toMatch(/Month quota\s+[█░]{10,24}\s+64% left\s+64\/100\s+reset /); - expect(serverOutput).toMatch(/Balance\s+\$12\.34/); + expect(serverBars.every((bar) => Array.from(bar).length === 24)).toBe(true); + expect(serverOutput).not.toMatch(/ {2}Month [^\n]* \| /u); + expect(serverOutput).toMatch( + /\nSpending & balances\nProvider · item +Amount\nTeam Accounting\n {2}Balance +\$12\.34\n/u, + ); assertFixtureContent(serverOutput); assertTreeSessionTokenTotals(serverOutput); expect(serverOutput).toContain("tree-model"); @@ -855,9 +858,8 @@ describe("v4 Phase 5 cross-surface release evidence", () => { await v2.quota.run(); const serverOutput = v2.dialog.mock.calls[0][0].message; expect(serverOutput).toContain("MiniMax Token Plan"); - expect(serverOutput).toContain("5h quota"); - expect(serverOutput).toContain("Weekly quota"); - expect(serverOutput).toContain("0% left"); + expect(serverOutput).toMatch(/\n {2}5h +░{24} +0% /u); + expect(serverOutput).toMatch(/\n {2}Weekly +░{24} +0% /u); expect(serverOutput).toContain("Remaining: -5 requests"); expect(serverOutput).toContain("Remaining: -20 requests"); expect(serverOutput).not.toContain("Invalid normalized provider result"); @@ -956,7 +958,7 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const serverOutput = v2.dialog.mock.calls[0][0].message; expect(serverOutput).toContain("Claude"); expect(serverOutput).toContain("Fable"); - expect(serverOutput).toContain("98% left"); + expect(serverOutput).toMatch(/Fable weekly quota +[█░]{24} +98% /u); v2.emit("session.execution.succeeded", "anthropic-fable-session"); await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledOnce()); @@ -1009,10 +1011,8 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const serverOutput = v2.dialog.mock.calls[0][0].message; expect(serverOutput).toContain("MiniMax Token Plan"); expect(serverOutput).toContain("(CN)"); - expect(serverOutput).toContain("5h quota"); - expect(serverOutput).toContain("Weekly quota"); - expect(serverOutput).toContain("33%"); - expect(serverOutput).toContain("46%"); + expect(serverOutput).toMatch(/\n {2}5h +[█░]{24} +33% /u); + expect(serverOutput).toMatch(/\n {2}Weekly +[█░]{24} +46% /u); expect(serverOutput).not.toContain("video"); expect(serverOutput).not.toContain("Invalid normalized provider result"); From 4b6904456e1df14c8cefac9070d38faa81ded773 Mon Sep 17 00:00:00 2001 From: "Shawn L. Kiser" <35721408+slkiser@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:41:35 +0200 Subject: [PATCH 97/97] test: expect uncached remote refetches on Windows in the cross-surface test --- tests/v4-phase5-cross-surface.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/v4-phase5-cross-surface.test.ts b/tests/v4-phase5-cross-surface.test.ts index 23080583..3dc7b80a 100644 --- a/tests/v4-phase5-cross-surface.test.ts +++ b/tests/v4-phase5-cross-surface.test.ts @@ -621,8 +621,11 @@ describe("v4 Phase 5 cross-surface release evidence", () => { const callsAfterFirstToast = vi.mocked(globalThis.fetch).mock.calls.length; v2.emit("session.execution.succeeded", "phase5-session"); await vi.waitFor(() => expect(v2.toast).toHaveBeenCalledTimes(2)); - // The failed accounting source is retried on the next V2 CLI event. - expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(callsAfterFirstToast + 1); + // POSIX caches the successful sources and retries only the failed 503 source. + // Windows has no protected identity key, so every remote source is fetched again. + expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes( + callsAfterFirstToast + (POSIX_IDENTITY_STORAGE ? 1 : PHASE5_QUOTA_PROVIDERS.length), + ); assertFixtureContent(getV2ToastMessage(v2.toast, 1)); const statusOutput = (await v2.tool.execute({}, { sessionID: "phase5-session" })).content;